読者です 読者をやめる 読者になる 読者になる

WTF!?

オンサイトCTFのWriteupとか書いてく.

年賀状CTF(お年玉付き)解説

年賀状CTFの解説をしていこうと思います.

Write upはBono(@Bono_iPad)さんが書いてくださっているので,

想定解を問題のソースなどを交えて紹介していきます.

BonoさんのWrite up

年賀状CTF2016 (by @_N4NU_) writeup by Bono-iPad

問題

年賀状CTF(お年玉付き) - WTF!?

Stage1

ジャンルとしては Forensic + Reversing ( + Steganography ) ですかね.

年賀状の画像が渡されるところから始まります.

fileコマンドを叩くと

$ file wc16g_tf_0003.png
wc16g_tf_0003.png: PNG image data, 1748 x 1181, 8-bit/color RGBA, non-interlaced

binwalkで叩くと

$ binwalk wc16g_tf_0003.png

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 1748 x 1181, 8-bit/color RGBA, non-interlaced
54            0x36            Zlib compressed data, default compression
2662          0xA66           Zlib compressed data, default compression
3402577       0x33EB51        gzip compressed data, from Unix, last modified: 2015-12-31 14:26:13

何か後ろにくっついてますね.

gzipで展開するとtarであることが分かるのでtarで展開すると,stage1というフォルダが現れました.

$ ls stage1
stage1.pyc  stage2.tar.gz.gpg

stage1.pycの元のコードは

#!/usr/bin/env python2
import sys
from os.path import getsize, basename
import random
try:
    from PIL import Image
except ImportError:
    import Image

if len(sys.argv) < 3:
    sys.stderr.write('Usage: {0:s} png_file message\n'.format(sys.argv[0]))
    sys.exit(1)

im = Image.open(sys.argv[1])
width, height = im.size

message = sys.argv[2]

for i in xrange(len(message)):
    pix = list(im.getpixel((i % width, i / width)))
    pix[random.randint(0, 3)] ^= ord(message[i])
    im.putpixel((i % width, i / width), tuple(pix))

im.save(basename(sys.argv[1]).split('.')[0].encode('rot13') + '.png')

pythonのデコンパイラを使うとほぼ同じ内容のコードを復元できると思います.

こいつを少し変えるとsolverになります.

#!/usr/bin/env python2
import sys
from os.path import getsize, basename
try:
    from PIL import Image
except ImportError:
    import Image

if len(sys.argv) < 3:
    sys.stderr.write('Usage: {0:s} original_file stegged_file\n'.format(sys.argv[0]))
    sys.exit(1)

im1 = Image.open(sys.argv[1])
im2 = Image.open(sys.argv[2])

width, height = im1.size

message = ''

i = 0
while not '\x00' in message: 
    pix1 = list(im1.getpixel((i % width, i / width)))
    pix2 = list(im2.getpixel((i % width, i / width)))
    message += chr(max([t[0] ^ t[1] for t in zip(pix1, pix2)]))
    i += 1

print message.strip('\x00')

こいつにネットから探してきた元の画像とwc16g_tf_0003.pngを渡せばflagが出てきます.

Congratz!!! Password: D3c0mpilin6_4nd_R3v3rs1n6_pyc_15_700_345y

Stage2

ジャンルとしては (Javascript) Reversing です.

この問題はStage1〜Stage3の中で最も年賀状CTFらしいネタが詰まっています.

問題のLife GameはMonkey Xという,クロスプラットフォームプログラミング言語を用いて作成しました.

なおLife Game本体はサンプルコード*1から引用しました.

実行すると普通のLife Gameが始まります.

Javascriptで動いているのでコードを見てみると難読化がかけられているので,

気合で人力デコードしたり,ブラウザにデコードさせたり,難読化解除ツールを使ったりしてコードを復元します.

元のコードを解析すると盤面がある条件を満たした状態でキーボードのFを押すとflagが現れることが分かります.

実はこのある条件を再現すると2016という文字が浮かび上がります.

Stage3

ジャンルは Reversing です.

$ file stage3
stage3: ELF 32-bit LSB  executable, Intel 80386, version 1 (GNU/Linux), statically linked, stripped

Statically LinkedでStrippedという面倒な設定ですが,

その代わり32bitだったり元のコードが1612バイトと小さかったりすることで,

ある程度の難しさに調整しています.

こいつはUPXでパックされていますが,

単純にupx -dではアンパックできないようになっています.

UPXがパックされるとき独自のヘッダーがくっつき,

upx -dではその独自のヘッダーを解釈してアンパックしています.

この独自ヘッダーの一部を書き換えてやると動くのにupx -dでアンパックできないというバイナリが出来ます.

想定解ではメモリ上に展開された状態でダンプを取って解析することを想定していました.

このプログラムの元ソースは次の通りです.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char key_matrix[4][4];
unsigned char flag_matrix[4][4];
unsigned char enc_matrix[4][4] = {0};

unsigned char correct_matrix[4][4] = {{0xd0, 0x3f, 0x3e, 0x08},
                                      {0x5b, 0x37, 0x79, 0x1e},
                                      {0x46, 0xe1, 0x10, 0x50},
                                      {0xb1, 0xe6, 0x73, 0xa7}};

int
is_valid_matrix()
{
    for(int i = 0; i < 4; i++) {
        for(int j = 0; j < 4; j++) {
            if(enc_matrix[i][j] != correct_matrix[i][j]){
                return 0;
            }
        }
    }
    return 1;
}

void
mul_matrix()
{
    for(int i = 0; i < 4; i++) {
        for(int j = 0; j < 4; j++) {
            for(int k = 0; k < 4; k++) {
                enc_matrix[i][j] += key_matrix[i][k] * flag_matrix[k][j];
            }
        }
    }
}

void
gen_key_matrix()
{
    srand(735);
    for(int i = 0; i < 4; i++) {
        for(int j = 0; j < 4; j++) {
            key_matrix[i][j] = rand() % 0x100;
        }
    }
}

void
flag2matrix(unsigned char *flag)
{
    for(int i = 0; i < 4; i++) {
        for(int j = 0; j < 4; j++) {
            flag_matrix[i][j] = flag[i * 4 + j];
        }
    }
}

int
is_valid_flag(unsigned char* flag)
{
    if(strlen(flag) != 16) return 0;
    if(flag[4] != '-' || flag[11] != '-') return 0;
    flag2matrix(flag);
    gen_key_matrix();
    mul_matrix();
    if(is_valid_matrix()) return 1;
    return 0;
}

int
main(int argc, char* argv[])
{
    if(argc != 2)
    {
        fprintf(stderr, "Usage: %s flag\n", argv[0]);
        exit(1);
    }

    if (is_valid_flag(argv[1]))
    {
        puts("Congrats! You got the amazon code.");
    } else {
        puts("Invalid. Try again.");
    }

    return 0;
}

randで生成した行列と入力したアマゾンギフト券のコードを行列として解釈した2行列の積が

correct_matrixになればいいというプログラムです.

想定解ではkey_matrixの逆行列を求めてcorrect_matrixにかけてflagを復元することを想定していました.

まとめ

結構な人に参加していただけたようで主催及び作問者としてはとてもうれしく思っています.

来年の年賀状CTFは2017/1/1 12:00(JST)に開催しようと考えています.

また来年は1位の人だけでなく3位の人ぐらいまでお年玉を出せればいいなと考えています.