RITSEC CTF 2018 Writeup

この大会は2018/11/17 2:00(JST)~2018/11/19 14:00(JST)に開催されました。
今回もチームで参戦。結果は 4236点で952チーム中18位でした。
自分で解けた問題をWriteupとして書いておきます。

Litness Test (Misc 1)

問題にフラグが書いてある。

RITSEC{welc0me_t0_th3_CTF!}

Talk to me (Misc 10)

Discordの#announcementsチャネルでフラグを見つけた。

RITSEC{its_like_irc-but_with_2_much_javascript}

Check out this cool filter (Misc 200)

Youtubeのタイトルは「Eiffel 65 - Blue (Da Ba Dee)」。Blueが関係あるのかもしれない。
Blueの値を取得してみると、同じ数値の並びが繰り返されている。
繰り返しを外し、シフトするとフラグになりそうだったので試してみる。

from PIL import Image

img = Image.open('CheckOutThisFilter.png').convert('RGB')
w, h = img.size

codes = []
for y in range(0, h):
    for x in range(0, w):
        r, g, b = img.getpixel((x, y))
        codes.append(b)

codes = codes[:51]

flag = ''
for code in codes:
    flag += chr(code - 13)

print flag
RITSEC{TIL_JPEG_COMPRESSION_MESSES_WITH_RGB_VALUES}

music.png (Misc 300)

赤のみで値を文字にすると、同じ文字列の繰り返しになっている。
緑のみ、青のみも同様。繰り返しなくして、連結してみる。

from PIL import Image

img = Image.open('music.png').convert('RGB')
w, h = img.size

r_str = ''
g_str = ''
b_str = ''
for y in range(0, h):
    for x in range(0, w):
        r, g, b = img.getpixel((x, y))
        r_str += chr(r)
        g_str += chr(g)
        b_str += chr(b)

s = r_str[:32] + g_str[:38] + b_str[:66]
print s

結果は以下の通り。

(t<<3)*[8/9,1,9/8,6/5,4/3,3/2,0][[0xd2d2c7,0xce4087,0xca32c7,0x8e4008][t>>14&3.1]>>(0x3dbe4687>>((t>>10&15)>9?18:t>>10&15)*3&7.1)*3&7.1]

これで検索してみると、以下のページが見つかった。

https://gist.github.com/djcsdy/2875542

値はほぼ同じ。値を入れ替え、ブラウザのデベロッパーツールでConsoleから実行してみたが、うまくいかない。上記のページの値で実行し直し、BASE64データを取得する。デコードして保存するとwavファイルになった。
この曲名当ての問題のようなので、アプリに音楽を聞かせ、曲名が「Never Gonna Give You Up」であることがわかった。

RITSEC{never_gonna_give_you_up}

I am a Stegosaurus (Forensics 250)

TweakPNGで開こうとすると、以下のようなメッセージあり。

Incorrect crc for IHDR chunk (is 93cf1eca, should be 01aae416)

IHDRチャンクのCRCを正しく修正する。画像の下の方にフラグが書いてある。

RITSEC{th1nk_0uts1d3_th3_b0x}

Space Force (Web 100)

' or 1=1 # と入力すると、全データが表示される。Shipname「LbtebKe6yrU8vEnx」のCaptainがフラグだった。

RITSEC{hey_there_h4v3_s0me_point$_3ny2Lx}

CictroHash (Crypto 150)

PDFを読み、ハッシュ処理をスクリプトにする。4バイトのハッシュということもあり、短い文字で衝突しそうなので、ブルートフォースする。

import itertools

def lol(val):
    s = bin(val)[2:].zfill(8)
    s = s[1:] + s[0]
    return int(s, 2)

def rol(val):
    s = bin(val)[2:].zfill(8)
    s = s[-1] + s[:-1]
    return int(s, 2)

def alpha(w):
    return [w[1], w[0]]

def beta(w):
    w[0][0] ^= w[1][3]
    w[0][1] ^= w[1][2]
    w[0][2] ^= w[1][1]
    w[0][3] ^= w[1][0]
    return w

def gamma(w):
    new_w = [[0, 0, 0, 0], [0, 0, 0, 0]]
    new_w[0][3] = w[0][0]
    new_w[1][2] = w[0][1]
    new_w[1][3] = w[0][2]
    new_w[1][1] = w[0][3]
    new_w[0][1] = w[1][0]
    new_w[1][0] = w[1][1]
    new_w[0][2] = w[1][2]
    new_w[0][0] = w[1][3]
    return new_w

def delta(w):
    w[0][0] = lol(w[0][0])
    w[1][0] = lol(w[1][0])
    w[0][2] = lol(w[0][2])
    w[1][2] = lol(w[1][2])
    w[0][1] = rol(w[0][1])
    w[1][1] = rol(w[1][1])
    w[0][3] = rol(w[0][3])
    w[1][3] = rol(w[1][3])
    return w

def round(w):
    w = alpha(w)
    w = beta(w)
    w = gamma(w)
    w = delta(w)
    return w

def f(w):
    for i in range(50):
        w = round(w)
    return w

def xor_pre(w, p):
    for i in  range(4):
        w[0][i] ^= p[i]
    return w

def str_to_blocks(s):
    while True:
        if len(s) % 4 == 0:
            break
        s += '\x00'
    blocks = []
    for i in range(len(s) / 4):
        block = []
        for j in range(4):
            block.append(ord(s[i*4+j]))
        blocks.append(block)
    return blocks

def block_to_hex(block):
    h = ''
    for code in block:
        h += ('%x' % code).zfill(2)
    return h

def get_hash(msg):
    w = [[31, 56, 156, 167], [38, 240, 174, 248]]
    blocks = str_to_blocks(msg)
    for block in blocks:
        w = xor_pre(w, block)
        w = f(w)
    return block_to_hex(w[1])

chars = ''.join([chr(i) for i in range(33, 127)])
dic = {}
found = False
for i in range(1, 6):
    for c in itertools.product(chars, repeat=i):
        text = ''.join(c)
        print text
        h = get_hash(text)
        if h in dic:
            found = True
            print 'Found!'
            print '1:', text
            print '2:', dic[h]
            print 'hash:', h
            break
        else:
            dic[h] = text
    if found:
        break

実行結果は以下の通り。

Found!
1: !!!U
2: tt!
hash: 1ffb77c0
$ curl -X POST http://fun.ritsec.club:8003/checkCollision --header "Content-Type: application/json" --data '{"str1": "!!!U", "str2": "tt!"}'
{
  "flag": "RITSEC{I_am_th3_gr3@t3st_h@XOR_3v@}", 
  "success": true
}
RITSEC{I_am_th3_gr3@t3st_h@XOR_3v@}

Nobody uses the eggplant emoji (Crypto 200)

絵文字を使った換字式暗号だとわかった。まず絵文字を半角英字に置き換える。

ABCDEFCGHAIJCDEFCKLMCNELGHDCEBCGHMCBOKPCBALQGCDEFCRFQGCKIQNMLCGHMQMCGHLMMCSFMQGAEIQTCNHKGCAQCDEFCIKRMUCNHKGCAQCDEFLCSFMQGUCNHKGCAQCGHMCKALCQVMMWCXMOEYAGDCEBCKICFIOKWMICQNKOOENTCDEFLCBOKPCAQZCKBLAYKI_EL_MFLEVMKI_QNKOOEN_NEN_GHMLMQ_K_WABBMLMIYMC

quipqiup(https://quipqiup.com/)で復号してみるが、ダメだった。以下のように最後の_をスペースに置き換えて試す。

ABCDEFCGHAIJCDEFCKLMCNELGHDCEBCGHMCBOKPCBALQGCDEFCRFQGCKIQNMLCGHMQMCGHLMMCSFMQGAEIQTCNHKGCAQCDEFCIKRMUCNHKGCAQCDEFLCSFMQGUCNHKGCAQCGHMCKALCQVMMWCXMOEYAGDCEBCKICFIOKWMICQNKOOENTCDEFLCBOKPCAQZCKBLAYKI EL MFLEVMKI QNKOOEN NEN GHMLMQ K WABBMLMIYMC

結果以下のようになった。

IFS?OUSJHIN?S?OUSARESBORJH?SOFSJHESF?A?SFIRKJS?OUS?UKJSANKBERSJHEKESJHREES?UEKJIONK?SBHAJSIKS?OUSNA?E?SBHAJSIKS?OURS?UEKJ?SBHAJSIKSJHESAIRSKPEEDS?E?OCIJ?SOFSANSUN?ADENSKBA??OB?S?OURSF?A?SIK?SAFRICAN OR EUROPEAN KBA??OB BOB JHEREK A DIFFERENCES

"AFRICAN", "EUROPEAN"や"A DIFFERNCE"は合っていそう。最後は複数形はおかしいので、Sは保留。ここから変換を試しながら、穴を埋めていく。最終コードは以下の通り。

import string

enc = 'ABCDEFCGHAIJCDEFCKLMCNELGHDCEBCGHMCBOKPCBALQGCDEFCRFQGCKIQNMLCGHMQMCGHLMMCSFMQGAEIQTCNHKGCAQCDEFCIKRMUCNHKGCAQCDEFLCSFMQGUCNHKGCAQCGHMCKALCQVMMWCXMOEYAGDCEBCKICFIOKWMICQNKOOENTCDEFLCBOKPCAQZCKBLAYKI_EL_MFLEVMKI_QNKOOEN_NEN_GHMLMQ_K_WABBMLMIYMC'

cipher = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
plain  = 'if youthnkarewlgsmq.,pdvc:'

table = string.maketrans(cipher, plain)
msg = enc.translate(table)
print msg

復号結果は以下の通り。

if you think you are worthy of the flag first you must answer these three questions. what is you name, what is your quest, what is the air speed velocity of an unladen swallow. your flag is: african_or_european_swallow_wow_theres_a_difference
RITSEC{african_or_european_swallow_wow_theres_a_difference}

Who drew on my program? (Crypto 350)

AES CBCモードなので、以下のようになる。

[平文1ブロック目] ^ IV                  --(暗号化)--> [暗号文1ブロック目]
[平文2ブロック目] ^ [暗号文1ブロック目] --(暗号化)--> [暗号文2ブロック目]

平文全体、暗号文1ブロック目の一部と暗号文2ブロック目はわかっている。鍵が14バイトわかっているので、この条件を満たすよう残りはブルートフォースで一致するものを求める。最終的にIVを求めると、フラグがわかる。

from Crypto.Cipher import AES
import binascii
import string

def str_xor(s1, s2):
    return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(s1, s2))

def decrypt(ciphertext, passphrase, IV):
    aes = AES.new(passphrase, AES.MODE_CBC, IV)
    return aes.decrypt(ciphertext)

pre_KEY = '9aF738g9AkI112'
plain = 'The message is protected by AES!'
cipher = '9exxxxxxxxxxxxxxxxxxxxxxxxxx436a808e200a54806b0e94fb9633db9d67f0'

found = False
for i in range(32, 127):
    for j in range(32, 127):
        KEY = pre_KEY + chr(i) + chr(j)
        tmp_cipher = cipher.replace('x', '0')
        tmp_cipher = binascii.unhexlify(tmp_cipher)
        dec = decrypt(tmp_cipher, KEY, '0' * 16)
        if dec[16] == plain[16] and dec[30:] == plain[30:]:
            found = True
            break
    if found:
        break

aes = AES.new(KEY, AES.MODE_ECB)
cipher2 = binascii.unhexlify(cipher[32:])
cipher1 = str_xor(aes.decrypt(cipher2), plain[16:])
IV = str_xor(aes.decrypt(cipher1), plain[:16])
print IV
RITSEC{b4dcbc#g}