ふるつき

v(*'='*)v かに

picoctf2019 writeup

チーム zer0pts として picoctf2019 に参加していました。チームとしては shark on wire 2 Time's Up, Again! zero_to_hero が解けず 30751点で58位でした。私はCryptographyやReverse Engineeringを中心に解きました。面白かった問題についてwriteupを書きます。

[Rev] droidsN

本当は droids0 から droids4 まであるのですが、全て同じ解法で解きました。apkファイルが渡されるので、dex2jarやcfrをつかって.classファイルをデコンパイルします。どのapkにも FlagstaffHill.class というクラスがあり、 getFlagというメソッド内で、共有ライブラリ内のフラグを組み立てる関数を呼び出しています。この関数名を頼りに共有ライブラリを解析すると、どれも似たような構造になっていて、特定のデータ領域にあるデータと、引数で与えられる秘密の文字列をxorしたものがフラグになっています。引数で与えられる文字列がただしいかどうかのチェック処理も存在するので、チェックを通過するように文字列を組み立てて、実際にxorをとってみるとフラグを得ることが出来ました。

[Crypto] AES-ABC

次のコードで暗号化されたppm画像が渡されます。

#!/usr/bin/env python

from Crypto.Cipher import AES
from key import KEY
import os
import math

BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))


def to_bytes(n):
    s = hex(n)
    s_n = s[2:]
    if 'L' in s_n:
        s_n = s_n.replace('L', '')
    if len(s_n) % 2 != 0:
        s_n = '0' + s_n
    decoded = s_n.decode('hex')

    pad = (len(decoded) % BLOCK_SIZE)
    if pad != 0: 
        decoded = "\0" * (BLOCK_SIZE - pad) + decoded
    return decoded


def remove_line(s):
    # returns the header line, and the rest of the file
    return s[:s.index('\n') + 1], s[s.index('\n')+1:]


def parse_header_ppm(f):
    data = f.read()

    header = ""

    for i in range(3):
        header_i, data = remove_line(data)
        header += header_i

    return header, data
        

def pad(pt):
    padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
    return pt + (chr(padding) * padding)


def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt))

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    iv = os.urandom(16)
    blocks.insert(0, iv)
    
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)
 
    return iv, ct_abc, ct


if __name__=="__main__":
    with open('flag.ppm', 'rb') as f:
        header, data = parse_header_ppm(f)
    
    iv, c_img, ct = aes_abc_encrypt(data)

    with open('body.enc.ppm', 'wb') as fw:
        fw.write(header)
        fw.write(c_img)

どうやら各ブロックを暗号化した後、前のブロックとの和をとっているようです。というわけで、暗号化されたファイルで、前のブロックとの差分をうまくとれば、実質ECBモードによる暗号化と同じになります。そしてECBモードでは画像などを暗号化したとき、元の画像のパターンが残ってしまう問題がありました。

from Crypto.Util.number import long_to_bytes
f = open("body.enc.ppm", "rb")
h1 = f.readline()
h2 = f.readline()
h3 = f.readline()

xs = []
while True:
    data = int.from_bytes(f.read(16), "big")
    if data == 0:
        break
    xs.append(data)
ys = []
for i in range(1, len(xs)):
    y = xs[i] - xs[i - 1]
    if y < 0:
        y += int(pow(256, 16))
    y = long_to_bytes(y)
    while len(y) % 16 != 0:
        y = b"\0" + y
    ys.append(y)
with open("flag.ppm", "wb") as f2:
    f2.write(h1)
    f2.write(h2)
    f2.write(h3)
    f2.write(b"".join(ys))