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):

# 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 1591is114(\x72). - Bytecode of
1591(0x637) is\x37\x06(little endian). - Bytecode of
NOPis 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.

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!