SECCON 2019 Online CTF に参加した (Beeeeeeeeeer, Sandstorm, repair, PPKeyboard)

2019-10-20

SECCON 2019 Online CTFにp3r0zとして参加しました。2131点で42位でした。

僕はBeeeeeeeeeerとSandstormとrepairとPPKeyboardを解いたので、その解法を簡単に書きたいと思います。

Beeeeeeeeeer

問題ファイルをダウンロードしてみると、中身はシェルスクリプトのようです。 整形してみても、コマンド置換を使ったり$'string'を使ったりして難読化しています。 良い解き方が思い浮かばなかったので、set -xをして地道に1ステップずつ追っていきました。部分的にset -xを検出してexitするコードや、後半の方でshutdownを呼び出すコードが含まれているので注意しましょう(僕はdockerを使って実行していたので問題ありませんでした)。

最終的に要点だけ抜き出すとこんな感じになります。

# 1つ目のscriptのなかでexport
export S1=hogefuga
# 2つ目のscriptのなかでexport
export n=3
# 3つ目のscriptのなかで実行
read _____  # bash
echo SECCON{$S1$n$_____}
# => SECCON{hogefuga3bash}

Sandstorm

問題ファイルを落とすとこんな感じです。

exifを見てみましょう。

ExifTool Version Number         : 11.70
File Name                       : sandstorm.png
File Size                       : 62 kB
File Modification Date/Time     : 2019:10:19 17:30:43+09:00
File Access Date/Time           : 2019:10:20 19:40:51+09:00
File Inode Change Date/Time     : 2019:10:19 18:03:54+09:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 584
Image Height                    : 328
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Adam7 Interlace
Background Color                : 255 255 255
Image Size                      : 584x328
Megapixels                      : 0.192

InterlaceとしてAdam7が使われています。画像中でAdamとか書いてあるので、きっとインターレースに何か鍵があるのでしょう。

とりあえずインターレースで読み込む順番を再現してみます。Adam 7アルゴリズムはWikipediaを見るととても簡単に実装できそうです。

adam7.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import cv2
import numpy

image = cv2.imread('./sandstorm.png')

height, width = image.shape[:2]


def adam7(x_step, y_step):
    new_image = numpy.zeros(image.shape)
    # create green image
    new_image[:, :] = [0, 255, 0]
    for h in range(0, height, y_step):
        for w in range(0, width, x_step):
            new_image[h][w] = image[h][w]

    return new_image


steps = (
    (8, 8),
    (4, 8),
    (4, 4),
    (2, 4),
    (2, 2),
    (1, 2),
    (1, 1),
)

for i, step in enumerate(steps):
    im = adam7(*step)
    cv2.imwrite('im_{}.png'.format(i), im)

実行してみるとこんな感じです。

明らかに8×8で読み込んだとき(一番左上の画像)にQRコードのような模様が出ていて怪しいので、8×8で読み込むピクセルだけを集めた画像を作ります。

というわけで答えはSECCON{p0nlMpzlCQ5AHol6}です

repair

960×540という名前のついたよくわからないファイルが渡されます。 とりあえずstringsにかけてみると、Lavf58.28.100といった単語が出てくるので、おそらく動画や音声のファイルかなという推測します。 他にも、JUNKLISTといったキーワードを調べていくと、これはどうやらRIFF形式のファイルだということがわかります。 RIFFについては調べると詳しい情報がたくさん出てきます。

さて動画のヘッダーを作るには、前述のWebサイトの情報から、フレームのサイズやフレーム数、コーデックなどの情報が必要になります。フレームサイズはファイル名から960×540であると推測します。フレーム数も、動画ファイル後半にあるidx1セクション(動画のインデックスセクション)から257であることがわかりました。問題はコーデックです。

最初はいくつか手打ちでRIFFヘッダーを作って試しましたが、自分のヘッダーが間違っていて再生できないのか、あるいはコーデックが間違っていて再生できないのかわからないので、最終的にffmpegのAPIを使って、半分自動でコーデックを求めて、映像を画像に落とし込むスクリプトを書きました。

このスクリプトのfind_available_codecs関数を呼び出すと以下の結果が得られます。

['qpeg', 'smc', 'tiertexseqvideo', 'bintext', 'kgv1', 'cavs', 'wnv1', 'tmv', 'gdv', 'arbc', 'idf', 'rscc', 'asv2', 'msvideo1', 'amv', 'tscc
2', 'mpegvideo', 'ultimotion', 'mxpeg', 'vb', 'h261', 'mpeg1video', 'dfa', 'fic', 'xbin', 'flic', 'eatgv', 'sp5x', 'lscr', 'dirac', 'mvc1',
 'fraps', 'cinepak', 'mpeg2video']

結構いっぱいあるやん…。とりあえずそれぞれのコーデックでデコードするプログラムを回して(前述のプログラムのmain関数を参照)画像をそれぞれ見たら、cinepakのときに正しくデコードできていました。

PPKeyboard

PE(64bit)とpcapが渡されます。とりあえずpcapを開いてみると、どうやらUSBの通信のようです。

PEも解析してみます。.rdataセクションを見てみると、DDJ-XP1という文字が見つかりました。どうやらDJ用途のMIDIコントローラーのようです。 また関数呼び出しを見てみると、WINMM.dllに含まれるmidiInOpenmidiInStartなどを呼び出している箇所があります。このことから、どうやらこのpcapはこのプログラムとMIDIコントローラーの通信をキャプチャしたものではないかと考えます。

MIDIコントローラーと通信を行うプログラムの解説記事を探してみると、MIDIキーボードとの通信は以下のレイアウトの構造体で行うようです。

typedef struct
{
    uint8_t header;
    uint8_t byte1;
    uint8_t byte2;
    uint8_t byte3;
} midiPacket;

// 特にNoteOn(押した状態)を通知する場合は…
typedef struct
{
    uint8_t header; // 0x9
    uint8_t byte1;  // 0x9n, n=channel number [0x0,0xF]
    uint8_t noteNumber;  // pitch [0, 127]
    uint8_t velocity; // velocity [0, 127]
} midiPacketNoteOn;

capからUSB URBのペイロードを抜き出してみると次のようなデータが得られます。

0997047f
09970400
0999087f
09990800
0997067f
09970600
0999057f
09990500
...

眺めてみると、前述のNoteOnのパケットと一致するので、これはMIDIコントローラーからの通信と考えて間違いなさそうです。

PE側の解析に戻りましょう。winmmを使ってMIDIコントローラーから信号を受信するには、 midiInOpen関数を呼び出すときの第3引数にハンドラーを指定します[参考]。ここに指定している関数(0x0140001070番地から始まる関数)の中身をPythonコードに落とし込んでみると以下のようになります(参考: MidiInProc callback function – MSDN)。

MidiInProc.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def handle(wMsg, dwInstance, dwParam1, dwParam2):
    # MIM_DATA = 0x3c3
    if wMsg != 0x3c3:
        return

    if dwParam2 > 0x7f0000:
        channel = dwParam2 & 0xff
        if channel == 0x97:
            sys.stdout.write('0x%x' % (((dwParam2 & 0xfff) - 0x97) >> 0x8))
        elif channel == 0x99:
            sys.stdout.write('%x ' % (((dwParam2 & 0xfff) - 0x99) >> 0x8))

この処理にあうように、ペイロードを加工&バイトオーダーを入れ替えて渡してあげると、以下の出力が得られます。

0x48 0x65 0x79 0x20 0x67 0x75 0x79 0x73 0x21 0x20 0x46 0x4c 0x41 0x47 0x20 0x69 0x73 0x20 0x53 0x45 0x43 0x43 0x4f 0x4e 0x7b 0x33 0x6e 0x37 0x33 0x72 0x33 0x64 0x5f 0x66 0x72 0x30 0x6d 0x5f 0x37 0x68 0x33 0x5f 0x70 0x33 0x72 0x66 0x30 0x72 0x6d 0x34 0x6e 0x63 0x33 0x5f 0x70 0x34 0x64 0x5f 0x6b 0x33 0x79 0x62 0x30 0x34 0x72 0x64 0x7d

あとは普通にデコードしてあげると以下のようになります。

Hey guys! FLAG is SECCON{3n73r3d_fr0m_7h3_p3rf0rm4nc3_p4d_k3yb04rd}

感想

repairで土曜日の夜を溶かしてしまいました(最初のほうで手打ちRIFFと格闘していたら3時間くらい時間を溶かしました、しかも再生できない)。 その後に作り始めた自動化のスクリプトは1時間ほどで作ることができて、さらにPyAVを使うとヘッダーの知識不要がほぼ不要になったので、やはり長いもの(ffmpeg)には巻かれるべきだなと思いました。

あと今回もpwnを解くことができませんでした。復習してちゃんと解けるようになりたいです(毎回言ってる気がする)。

このエントリーをはてなブックマークに追加
« Python 2系と3系のsys.stdinの違い