ヾノ*>ㅅ<)ノシ帳

ノンジャンルのふりーだむなブログです。

HITCON CTF 2016 Quals Writeup (Reverse: Handcrafted-pyc&ROP)

I participated in HITCON CTF 2016 Quals (2016/10/8-10/9; 48 hours) as a member of Ping-Mic (1 people).

I solved following plobrems in this time:

  • Handcrafted-pyc (Reverse 50 pts.)
  • ROP (Reverse 250 pts.)

My team result is 126th place (350 pts):

f:id:katc:20161010121602p:plain

# Gophers in the Shell めっちゃうけるんですけどww

Here's writeups.

Handcrafted-pyc (Reverse 50 pts.)

decompress.py to convert crackme.py to crackme.pyc:

import marshal, zlib, base64
import imp

b64d = base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ==')
zd = zlib.decompress(b64d)
# open("marshal", "wb").write(zd)
ml = marshal.loads(zd)

with open('crackme.pyc','wb') as f:
    f.write(imp.get_magic() + b'\0' * 4 + zd)

disasm.py to disassemble:

# orig. http://nedbatchelder.com/blog/200804/the_structure_of_pyc_files.html
import dis, marshal, struct, sys, time, types

def show_file(fname):
    f = open(fname, "rb")
    magic = f.read(4)
    moddate = f.read(4)
    modtime = time.asctime(time.localtime(struct.unpack('I', moddate)[0]))
    print "magic %s" % (magic.encode('hex'))
    print "moddate %s (%s)" % (moddate.encode('hex'), modtime)
    code = marshal.load(f)
    show_code(code)
    
def show_code(code, indent=''):
    print "%scode" % indent
    indent += '   '
    print "%sargcount %d" % (indent, code.co_argcount)
    print "%snlocals %d" % (indent, code.co_nlocals)
    print "%sstacksize %d" % (indent, code.co_stacksize)
    print "%sflags %04x" % (indent, code.co_flags)
    show_hex("code", code.co_code, indent=indent)
    dis.disassemble(code)
    print "%sconsts" % indent
    for const in code.co_consts:
        if type(const) == types.CodeType:
            show_code(const, indent+'   ')
        else:
            print "   %s%r" % (indent, const)
    print "%snames %r" % (indent, code.co_names)
    print "%svarnames %r" % (indent, code.co_varnames)
    print "%sfreevars %r" % (indent, code.co_freevars)
    print "%scellvars %r" % (indent, code.co_cellvars)
    print "%sfilename %r" % (indent, code.co_filename)
    print "%sname %r" % (indent, code.co_name)
    print "%sfirstlineno %d" % (indent, code.co_firstlineno)
    show_hex("lnotab", code.co_lnotab, indent=indent)
    
def show_hex(label, h, indent):
    h = h.encode('hex')
    if len(h) < 60:
        print "%s%s %s" % (indent, label, h)
    else:
        print "%s%s" % (indent, label)
        for i in range(0, len(h), 60):
            print "%s   %s" % (indent, h[i:i+60])

show_file(sys.argv[1])

I found that password check routine can be bypassed by binary patching.

magic 03f30d0a
moddate 00000000 (Thu Jan  1 09:00:00 1970)
code
   argcount 0
   nlocals 0

... snipped ...
    
            737 LOAD_CONST               0 (None)
            740 NOP                 
            741 JUMP_ABSOLUTE          759
        >>  744 LOAD_GLOBAL              1 (raw_input)
            747 JUMP_ABSOLUTE         1480
        >>  750 LOAD_FAST                0 (password)
            753 COMPARE_OP               2 (==)
            756 JUMP_ABSOLUTE          767
        >>  759 ROT_TWO             
            760 STORE_FAST               0 (password)
            763 POP_TOP             
            764 JUMP_ABSOLUTE          744
        >>  767 POP_JUMP_IF_FALSE     1591
            770 LOAD_GLOBAL              0 (chr)
            773 LOAD_CONST              17 (99)

... snipped ...

So I replaced POP_JUMP_IF_FALSE 1591@767 to NOPs. We can check opecodes at CPython's opcode.h.

  • Bytecode of POP_JUMP_IF_FALSE 1591 is 114 (\x72).
  • Bytecode of 1591(0x637) is \x37\x06 (little endian).
  • Bytecode of NOP is 9 (\x09).

So replace 72 37 06 with 09 09 09 in crackme.pyc in my binary editor. I saved patched binary as crackme.patched.pyc.

binary patching

Finally, I got flag:

% python2 crackme.patched.pyc
password: bypassed
hitcon{Now you can compile and run Python bytecode in your brain!

flag = hitcon{Now you can compile and run Python bytecode in your brain!

ROP (Reverse 250 pts.)

Who doesn't like ROP? Let's try some new features introduced in 2.3.

First, I googled "iseq 2.3". I found that iseq is Ruby 2.3's feature and it is compiled by its Class: RubyVM::InstructionSequence. Second, i wrote this scripts to disassemble/run iseq file.

ROP.rb:

require 'iseq'

data = File.read('rop.iseq')
# data = Marshal.dump seq
# seq_loaded = Marshal.load data
# data = data.unpack("C*")
# puts data.class
new_iseq = RubyVM::InstructionSequence.load_from_binary data

if ARGV[0] == "disasm" then
    puts new_iseq.disasm
else
    new_iseq.eval
end

rop.sh:

#!/bin/sh
ruby ROP.rb

to disassemble: ruby ROP.rb disasm > ROP.disasm to run: ./rop.sh

Third, I hand decompiled iseq like following lines:

for main() ?:

0054 getglobal        $stdin
0056 opt_send_without_block <callinfo!mid:gets, argc:0, ARGS_SIMPLE>, <callcache>

line = gets(0)

0059 opt_send_without_block <callinfo!mid:chomp, argc:0, ARGS_SIMPLE>, <callcache>
0062 setlocal_OP__WC__0 3
0064 trace            1                                               (  39)
0066 getlocal_OP__WC__0 3
0068 putstring        "-"
0070 opt_send_without_block <callinfo!mid:split, argc:1, ARGS_SIMPLE>, <callcache>
0073 setlocal_OP__WC__0 2 # check
0075 trace            1                                               (  40)

check = line.split('-')

0077 getlocal_OP__WC__0 2
0079 opt_size         <callinfo!mid:size, argc:0, ARGS_SIMPLE>, <callcache>
0082 putobject        5
0084 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0087 branchif         94

if (size(check) == 5) {
    // at 94
}
else {
    gg() #game over
}

0094 trace            1                                               (  41)
0096 getlocal_OP__WC__0 2
0098 send             <callinfo!mid:all?, argc:0>, <callcache>, block in <compiled>

    == disasm: #<ISeq:block in <compiled>@<compiled>>=======================
    == catch table
    | catch type: redo   st: 0002 ed: 0011 sp: 0000 cont: 0002
    | catch type: next   st: 0002 ed: 0011 sp: 0000 cont: 0011
    |------------------------------------------------------------------------
    local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
    [ 2] x<Arg>     
    0000 trace            256                                             (  41)
    0002 trace            1
    0004 getlocal_OP__WC__0 2
    0006 putobject        /^[0-9A-F]{4}$/
    0008 opt_regexpmatch2 <callinfo!mid:=~, argc:1, ARGS_SIMPLE>, <callcache>
    0011 trace            512
    0013 leave     

0102 branchif         109

check.all?{|item| item ~= /^[0-9A-F]{4}$/} 

0109 trace            1                                               (  42)
0111 getlocal_OP__WC__0 2
0113 putobject_OP_INT2FIX_O_0_C_  # 0
0114 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0117 putobject        16
0119 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # base=16
0122 putobject        31337
0124 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0127 branchif         134

if (check[0].to_i(16) == 31337) {
    // at 134
}   
else {
    gg()
}

0136 getlocal_OP__WC__0 2
0138 putobject_OP_INT2FIX_O_1_C_  # 1
0139 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0142 opt_send_without_block <callinfo!mid:reverse, argc:0, ARGS_SIMPLE>, <callcache>
0145 putstring        "FACE"
0147 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0150 branchif         157

if (check[1].reverse == "FACE") {
    // at 157
}
else {
    gg()
}

0157 trace            1                                               (  44)
0159 putself          
0160 putobject        217  # a := 217
0162 getlocal_OP__WC__0 2
0164 putobject        2
0166 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0169 putobject        16 
0171 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # b := check[2].to_i(16)
0174 putobject        314159 # m := 314159
0176 opt_send_without_block <callinfo!mid:f, argc:3, FCALL|ARGS_SIMPLE>, <callcache> 

0179 putobject        28 # base=28
0181 opt_send_without_block <callinfo!mid:to_s, argc:1, ARGS_SIMPLE>, <callcache>
0184 opt_send_without_block <callinfo!mid:upcase, argc:0, ARGS_SIMPLE>, <callcache>
0187 putstring        "48D5"
0189 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0192 branchif         199

if (f(217, check[2].to_i(16), 314159).to_s(28).upcase == "48D5") {
    // at 199
}
else {
    gg()
}

>>> 0x48D5
18645

0201 getlocal_OP__WC__0 2
0203 putobject        3
0205 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0208 putobject        10 
0210 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # base=10
0213 opt_send_without_block <callinfo!mid:prime_division, argc:0, ARGS_SIMPLE>, <callcache>
0216 putobject        :first # n.prime_division no first-item
0218 send             <callinfo!mid:map, argc:0, ARGS_BLOCKARG>, <callcache>, nil
0222 opt_send_without_block <callinfo!mid:sort, argc:0, ARGS_SIMPLE>, <callcache>
0225 duparray         [53, 97]
0227 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0230 branchif         237

if (Prime.first(check[3]) == [53,97]) { // check[3] = 53 * 97 = 5141
    // at 237
}
else {
    gg()
}

0239 getlocal_OP__WC__0 2 # xs
0241 send             <callinfo!mid:map, argc:0>, <callcache>, block in <compiled>  map

    == disasm: #<ISeq:block in <compiled>@<compiled>>=======================
    == catch table
    | catch type: redo   st: 0002 ed: 0011 sp: 0000 cont: 0002
    | catch type: next   st: 0002 ed: 0011 sp: 0000 cont: 0011
    |------------------------------------------------------------------------
    local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
    [ 2] x<Arg>     
    0000 trace            256                                             (  46)
    0002 trace            1
    0004 getlocal_OP__WC__0 2
    0006 putobject        16
    0008 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache>
    0011 trace            512
    0013 leave  

0245 putobject        :^
0247 opt_send_without_block <callinfo!mid:inject, argc:1, ARGS_SIMPLE>, <callcache> xs.inject(:^)
0250 opt_send_without_block <callinfo!mid:to_s, argc:0, ARGS_SIMPLE>, <callcache>
0253 opt_send_without_block <callinfo!mid:sha1, argc:0, ARGS_SIMPLE>, <callcache>
0256 putstring        "947d46f8060d9d7025cc5807ab9bf1b3b9143304"
0258 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0261 branchif         268

// http://sha1.gromweb.com/?hash=947d46f8060d9d7025cc5807ab9bf1b3b9143304
// sha1("5671") = "947d46f8060d9d7025cc5807ab9bf1b3b9143304"
if (sha1(check.map{|item| item.to_i(16)}.inject(:^)) == "947d46f8060d9d7025cc5807ab9bf1b3b9143304") { 
    // congratz
}

for f():

== disasm: #<ISeq:f@<compiled>>=========================================
== catch table
| catch type: break  st: 0021 ed: 0086 sp: 0000 cont: 0086
| catch type: next   st: 0021 ed: 0086 sp: 0000 cont: 0018
| catch type: redo   st: 0021 ed: 0086 sp: 0000 cont: 0021
|------------------------------------------------------------------------
local table (size: 6, argc: 3 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 6] a<Arg>     [ 5] b<Arg>     [ 4] m<Arg>     [ 3] s          [ 2] r          
0000 trace            8                                               (  27)
0002 trace            1                                               (  28)
0004 putobject_OP_INT2FIX_O_1_C_ # 1
0005 setlocal_OP__WC__0 3 <s> 

s = 1

0007 trace            1                                               (  29)
0009 getlocal_OP__WC__0 6 <a>
0011 setlocal_OP__WC__0 2 <r>

r = a

0013 trace            1                                               (  30)
0015 jump             75
0017 putnil           
0018 pop              
0019 jump             75

0075 getlocal_OP__WC__0 5   <b>                                          (  30)
0077 putobject_OP_INT2FIX_O_0_C_  # 0
0078 opt_neq          <callinfo!mid:!=, argc:1, ARGS_SIMPLE>, <callcache>, <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0083 branchif         21

if (b != 0) {
    // at 21
}
else {
    return s 
}

0021 trace            1                                               (  31)
0023 getlocal_OP__WC__0 5 <b>
0025 putobject_OP_INT2FIX_O_0_C_ # = 0
0026 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache> # b[0]
0029 putobject_OP_INT2FIX_O_1_C_ # = 1
0030 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0033 branchunless     49

if (b[0] != 1) {
    // at 49
}

0035 getlocal_OP__WC__0 3 <s>
0037 getlocal_OP__WC__0 2 <r>
0039 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>, <callcache>
0042 getlocal_OP__WC__0 4 <m>
0044 opt_mod          <callinfo!mid:%, argc:1, ARGS_SIMPLE>, <callcache>
0047 setlocal_OP__WC__0 3 <s>

s = s * r % m

0049 trace            1                                               (  32)
0051 getlocal_OP__WC__0 5 <b>
0053 putobject_OP_INT2FIX_O_1_C_ # = 1
0054 opt_send_without_block <callinfo!mid:>>, argc:1, ARGS_SIMPLE>, <callcache>
0057 setlocal_OP__WC__0 5 <b>

b = b >> 1

0059 trace            1                                               (  33)
0061 getlocal_OP__WC__0 2 <r>
0063 getlocal_OP__WC__0 2 <r>
0065 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>, <callcache>
0068 getlocal_OP__WC__0 4 <m>
0070 opt_mod          <callinfo!mid:%, argc:1, ARGS_SIMPLE>, <callcache>
0073 setlocal_OP__WC__0 2 <r>

r = r * r % m

0075 getlocal_OP__WC__0 5   <b>                                          (  30)
0077 putobject_OP_INT2FIX_O_0_C_  # 0
0078 opt_neq          <callinfo!mid:!=, argc:1, ARGS_SIMPLE>, <callcache>, <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0083 branchif         21

if (b != 0) { // b = 16, 8, 4, 2, 1, 0
    // at 21
}
else {
    return s 
}

----

0085 putnil           
0086 pop              
0087 trace            1                                               (  35)
0089 getlocal_OP__WC__0 3 <s> return s 
0091 trace            16                                              (  36)
0093 leave                   

to get check[2], check[4] (xs[2], x[4]), i wrote f.rb and final.rb:

f.rb:

def f(a, b, m)
    s = 1
    r = a
    while b != 0 do
        if b[0] == 1 then
            s = s * r % m
        end
        b = b >> 1
        r = r * r % m
    end
    return s
end

(1..314159).each{|i|
    if f(217, i, 314159).to_s(28).upcase == "48D5" then
        puts "found"
        puts i.to_s(16) # hex(i)
    end
}

"""result
% ruby f.rb
found
1bd2
"""

final.rb:

require 'iseq'

xs = [31337.to_s(16), "FACE".reverse, "1bd2", "5141"]

# puts RubyVM::InstructionSequence.compile("puts xs.map{|item| item.inject(:^)}.to_s").disasm()

# a ^ b ^ c ^ d ^ x = e 
# x = e ^ (a ^ b ^ c ^ d)
result = xs.map{|item| item.to_i(16)}.inject(:^)
final = (result ^ 5671).to_s(16)

key = (xs + [final]).join('-').upcase
puts key

puts (xs + [final]).map{|item| item.to_i(16)}.inject(:^)

p IO.popen("./run.sh", "r+") {|io|
    io.puts key
    io.close_write
    io.gets
}

Finally, i got flag:

% ruby final.rb
7A69-ECAF-1BD2-5141-CA72
5671
"Congratz! flag is hitcon{ROP = Ruby Obsecured Programming ^_<}\n"

flag = hitcon{ROP = Ruby Obsecured Programming ^_<}


This CTF was good for me ^_^ Thanks!