[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更改。
关键的漏洞函数已经分析完了,剩下的函数分不分析都差不多了。
现在开始利用漏洞 大概思路是这样的
首先利用off-by-one漏洞泄露libc的地址再说
然后利用fastbin attack来修改__malloc_hook的指向为one_gagedt