SECCON 2015 Online CTFに参加した話

解いた問題とか解法とか

Posted by midchildan on December 6, 2015

今年は@chikyu_koteiと一緒にSECCON 2015 Online CTFに参加した。

解いた問題

時間 得点 問題
12/05 17:39:08 100 (+100) Command-Line Quiz
12/05 17:47:01 150 (+50) Start SECCON CTF
12/05 19:26:39 250 (+100) SECCON WARS 2015
12/05 20:50:07 350 (+100) Connect the server
12/05 23:12:03 450 (+100) Unzip the file
12/05 23:54:49 750 (+300) Exec dmesg
12/06 12:53:37 850 (+100) Reverse-Engineering Android APK 1
12/06 12:58:22 900 (+50) Last Challenge (Thank you for playing)
12/06 13:57:59 1000 (+100) Steganography 1
12/06 14:55:59 1300 (+300) Decrypt it

Command-Line Quiz

telnet caitsith.pwn.seccon.jp
User:root
Password:seccon
すべての *.txt ファイルを読め

telnetで繋いでlsしてみたら 1.txt から 5.txt まであってその中にあるクイズに全て答えると flag の中身が見れるという形式だった。クイズの内容はhead, tail, grep, awkとsedを知ってますかという内容のものだった。

Start SECCON CTF

ex1
Cipher:PXFR}QIVTMSZCNDKUWAGJB{LHYEO
Plain: ABCDEFGHIJKLMNOPQRSTUVWXYZ{}

ex2
Cipher:EV}ZZD{DWZRA}FFDNFGQO
Plain: {HELLOWORLDSECCONCTF}

quiz
Cipher:A}FFDNEVPFSGV}KZPN}GO
Plain: ?????????????????????

換字暗号。以下の様なスクリプトを書いて解いた:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python
cipher = "PXFR}QIVTMSZCNDKUWAGJB{LHYEO"
plain  = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}"

decrypt_table = {}
for c, p in zip(cipher, plain):
    decrypt_table[c] = p

flag = ""
for c in "A}FFDNEVPFSGV}KZPN}GO":
    flag += decrypt_table[c]

print(flag)

SECCON WARS 2015

youtu.be/8SFsln4VyEk

動画を見たらStar Wars風の文章の中にうっすらとバーコードみたいなのが見えたから動画をダウンロードしてスクリプトで処理した。OpenCVで20秒から65秒までのフレームを全て取り出してからモノクロに変換してXORしたらQRコードが取り出せた。あとは適当なツールを使ってQRコードを解読したらフラグが取り出せた。以下がバーコード取り出しに使ったコード:

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
#!/usr/bin/python
import cv2

cap = cv2.VideoCapture('secconwars.mp4')
cap.set(cv2.CAP_PROP_POS_MSEC, 25000)

while True:
    success, frame = cap.read()
    if success
        break
qr_image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

while cap.get(cv2.CAP_PROP_POS_MSEC) < 65000:
    success, frame = cap.read()
    if not success:
        continue
    grayframe = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    cv2.bitwise_or(qr_image, grayframe, qr_image)

    cv2.imshow('qrcode', qrcode)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.imwrite('qrcode.png', qr_image)
cv2.waitKey(0)
cv2.destroyAllWindows()

以下がスクリプトで取り出せた画像:

Connect the server

@chikyu_koteiに頑張ってもらった。

Unzip the file

Unzip the file
unzip

unzip.zip のファイル一覧は以下の通りになってた:

  • backnumber08.txt
  • backnumber09.txt
  • flag

backnumber08.txtbacknumber09.txtは検索したら見つかったので選択型平文攻撃ができるツールを使うことでフラグを取り出すことができた。

Exec dmesg

秘密のメッセージをLinuxのisoイメージの中から見つけてください。
image.zip

まずは仮想マシンを作って起動した。問題名が”Exec dmesg”だったのでとりあえずdmesgを実行してみたものの…

1
2
tc@box:~$ dmesg
dmesg: applet not found

案の定ダメだった。手がかりを探ってみたところ…

1
2
tc@box:~$ ls -l $(which dmesg)
lrwxrwxrwx 1 root root 7 Nov 1 02:50 /bin/dmesg -> busybox

少しググってみたらbusyboxは様々なコマンドを一つのバイナリにコンパイルしていて、busyboxのバイナリにシムリンクを張ることで個々のコマンドを呼び出せることが分かった。ここでbusyboxのソースコードをダウンロードして applets/applet_tables.c を読んでみたところ、 struct bb_applet applets[] にコマンド名とコマンドに対応する関数が関連付けられていることが分かった。dmesgの場合、 "dmesg" という文字列と dmesg_main という関数が対応付けられていた。ただし、条件付きコンパイルでdmesgを除外するように指定された場合はdmesgのエントリは applets[] に入らない。ここまで分かってから仮想マシンに戻ってbusyboxのバイナリに dmesg という文字列が無いか確認してみたが、存在しなかった。ならば dmesg_main 関数が存在すかどうか調べるために、もう一回busyboxのソースコードを確認した。すると util-linux/dmesg.cdmesg_main が定義されていて、 "s+:n+", "cs:n:r", "klogctl" という文字列リテラルがあるのを確認した。それを手がかりにしてもう一回busyboxのバイナリを調べてみたらそれらしい文字列を発見した:

1
2
00070330 69 6e 67 00 73 2b 3a 6e  2b 00 63 73 3a 6e 3a 00  |ing.s+:n+.cs:n:.|
00070340 6b 6c 6f 67 63 74 6c 00  0a 6d 6f 64 65 20 22 25  |klogctl..mode "%|

この文字列を参照してる関数の先頭アドレスがdmesg_mainであるはずなので、あとはバイナリーエディターを使って applets[] のエントリーを書き換えてdmesgのエントリーを加えれば良い…はずでした。どうやってバイナリーエディターを仮想マシンにインストールするか調べてみたら、仮想マシンで動かしてるtiny core linuxにはAppbrowserというパッケージマネージャーがあることが判明。どうやら tce-ab コマンドを使って util-linux というパッケージをインストールすればdmesgが使えるようになることが分かった。でもバイナリーの問題だしまさか外からdmesgを引っ張ってくるだけでフラグを取ることはできないだろうと思ったが…

1
2
tc@box:~$ dmesg | grep "SECCON"
[9.492432] SECCON{elf32-i386}

あっさりフラグが出てきてしまった(完)

Reverse-Engineering Android APK 1

じゃんけんに1000回連続で勝ち続けよ
rps.apk

まずはdex2jarを使ってjavaのクラスファイルを取り出し、IntelliJで逆コンパイルしてみた:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example.seccon2015.rock_paper_scissors;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import java.util.Random;

public class MainActivity extends Activity implements OnClickListener {
    Button P;
    Button S;
    int cnt = 0;
    int flag;
    private final Handler handler = new Handler();
    int m;
    int n;
    Button r;
    private final Runnable showMessageTask = new Runnable() {
        public void run() {
            TextView var1 = (TextView)MainActivity.this.findViewById(2131492946);
            MainActivity var2;
            if(MainActivity.this.n - MainActivity.this.m == 1) {
                var2 = MainActivity.this;
                ++var2.cnt;
                var1.setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
            } else if(MainActivity.this.m - MainActivity.this.n == 1) {
                MainActivity.this.cnt = 0;
                var1.setText("LOSE +0");
            } else if(MainActivity.this.m == MainActivity.this.n) {
                var1.setText("DRAW +" + String.valueOf(MainActivity.this.cnt));
            } else if(MainActivity.this.m < MainActivity.this.n) {
                MainActivity.this.cnt = 0;
                var1.setText("LOSE +0");
            } else {
                var2 = MainActivity.this;
                ++var2.cnt;
                var1.setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
            }

            if(1000 == MainActivity.this.cnt) {
                var1.setText("SECCON{" + String.valueOf((MainActivity.this.cnt + MainActivity.this.calc()) * 107) + "}");
            }

            MainActivity.this.flag = 0;
        }
    };

    static {
        System.loadLibrary("calc");
    }

    public MainActivity() {
    }

    public native int calc();

    public void onClick(View var1) {
        if(this.flag != 1) {
            this.flag = 1;
            ((TextView)this.findViewById(2131492946)).setText("");
            TextView var3 = (TextView)this.findViewById(2131492944);
            TextView var4 = (TextView)this.findViewById(2131492945);
            this.m = 0;
            this.n = (new Random()).nextInt(3);
            int var2 = this.n;
            var4.setText((new String[]{"CPU: Paper", "CPU: Rock", "CPU: Scissors"})[var2]);
            if(var1 == this.P) {
                var3.setText("YOU: Paper");
                this.m = 0;
            }

            if(var1 == this.r) {
                var3.setText("YOU: Rock");
                this.m = 1;
            }

            if(var1 == this.S) {
                var3.setText("YOU: Scissors");
                this.m = 2;
            }

            this.handler.postDelayed(this.showMessageTask, 1000L);
        }
    }

    protected void onCreate(Bundle var1) {
        super.onCreate(var1);
        this.setContentView(2130968600);
        this.P = (Button)this.findViewById(2131492941);
        this.S = (Button)this.findViewById(2131492943);
        this.r = (Button)this.findViewById(2131492942);
        this.P.setOnClickListener(this);
        this.r.setOnClickListener(this);
        this.S.setOnClickListener(this);
        this.flag = 0;
    }
}

すると48〜50行目にフラグを表示するコードがあることが分かったので、そのまま書き換えようとしたけどjavaのクラスファイルを直接編集する機能は流石になかった。しょうがないのでapktoolを使ってDalvik向けのアセンブリコードを取り出し、上の48〜50行目に該当するアセンブリコードを下のように書き換えた。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const/16 v1, 0x3e8

iget-object v2, p0, Lcom/example/seccon2015/rock_paper_scissors/MainActivity$1;->this$0:Lcom/example/seccon2015/rock_paper_scissors/MainActivity;

iget v2, v2, Lcom/example/seccon2015/rock_paper_scissors/MainActivity;->cnt:I

if-ne v1, v1, :cond_0

.line 50
new-instance v1, Ljava/lang/StringBuilder;

invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V

const-string v2, "SECCON{"

invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

move-result-object v1

iget-object v2, p0, Lcom/example/seccon2015/rock_paper_scissors/MainActivity$1;->this$0:Lcom/example/seccon2015/rock_paper_scissors/MainActivity;

iget v2, v2, Lcom/example/seccon2015/rock_paper_scissors/MainActivity;->cnt:I

const/16 v2, 0x3e8

iget-object v3, p0, Lcom/example/seccon2015/rock_paper_scissors/MainActivity$1;->this$0:Lcom/example/seccon2015/rock_paper_scissors/MainActivity;

invoke-virtual {v3}, Lcom/example/seccon2015/rock_paper_scissors/MainActivity;->calc()I

move-result v3

add-int/2addr v2, v3

mul-int/lit8 v2, v2, 0x6b

invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;

move-result-object v2

invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

move-result-object v1

const-string v2, "}"

invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

move-result-object v1

invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

move-result-object v1

invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

少し補足すると、書き換えたのは7行目と24行目である。7行目で条件分岐を潰し、24行目で勝利した回数の代わりに定数1000をレジスタ v2 に代入してる。あとは改変したアセンブリコードを使って再度apkを作り直してエミュレーターで実行したらフラグを表示できた。以下がフラグを表示したときの様子である。

Last Challenge (Thank you for playing)

ex1
Cipher:PXFR}QIVTMSZCNDKUWAGJB{LHYEO
Plain: ABCDEFGHIJKLMNOPQRSTUVWXYZ{}

ex2
Cipher:EV}ZZD{DWZRA}FFDNFGQO
Plain: {HELLOWORLDSECCONCTF}

quiz
Cipher:A}FFDNEA}}HDJN}LGH}PWO
Plain: ??????????????????????

Start SECCON CTFとほぼ同じだった。

Steganography 1

Find image files in the file
MrFusion.gpjb
Please input flag like this format–>SECCON{*** ** **** ****}

少しググってみたところ、多くの画像フォーマットは始まりのバイト列と終わりのバイト列が決まっていて、終わりに相当するバイト列以降に現れたバイト列は大抵画像ビューワーなどによって無視されることが分かった。マジックナンバーと言われているこうしたバイト列をバイナリエディターを使って探し、それ以前に登場するバイト列を全て消すことでGIF画像、JPEG画像、PNG画像をそれぞれ4つずつに加えて3つのBMP画像を取り出すことに成功した。GIMPでこれらを合成した結果、以下の画像が得られた。

Decrypt it

$ ./cryptooo SECCON{*************************}
Encrypted(44): waUqjjDGnYxVyvUOLN8HquEO0J5Dqkh/zr/3KXJCEnw=

what’s the key?
cryptooo.zip

@chikyu_koteiが最後の数十分で頑張って解いてくれた。

結果

合計1300点、143位だった(完)