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 NOP
s.
We can check opecodes at CPython's opcode.h.
- Bytecode of
POP_JUMP_IF_FALSE 1591
is114
(\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
.
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!