InterKosenCTF Writeup
- InterKosenCTF について
- InterKosenCTF Writeup(8問)
InterKosenCTF について
InterKosenCTFが開催されました。
2019年8月11日午前10時~2019年8月12日午後10時(36時間)
今回たまたま紹介があったので参加しました。 タイトルにKosenと入っているので、高専生だけかと思いましたが、特にそういった制限はないようです。
競技がスタートしてからしばらくの間、ずっと504が返ってきており、フラグが投入できない感じになっていました。 午後から安定してきて、快適に使えるようになっていました。運営さんお疲れ様です。
今回は、2人のチームで参加しました。結果は、20/91位で3214点でした。 すべての問題を見れていませんが、他のCTFと違い、易しめの問題が多くあった印象でした。 SECCON Beginnersと同じぐらいの難易度だと思います。
InterKosenCTF Writeup(8問)
私が実際に解いた8つの問題だけを紹介します。
uploader(warmup, web)
問題
uploader:
TAGS: warmup, web
URL: http://web.kosenctf.com:11000/
解答例
http://web.kosenctf.com:11000/ にアクセスすると、ファイルのアップロードとダウンロードができるWebサービスが表示されました。
左上に「view source」とあり、ソースコードを見ることができるようです。 ソースコードを読んでみると、以下のようにGETパラメータをそのままSQL文に入れている箇所がありました。 SQLインジェクションができそうです。
テーブル名も見えているので、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 :)」と表示されるようになっていました。
試しに、「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
ザっと見た感じ、ステガノグラフィ系の問題のようです。 ソースコードを読んでみるとsteg.pyは、以下の手順フラグを画像に隠していることが分かりました。
- 現在の時刻を基に乱数のseed値を生成する。
- フラグの文字列をビットに変換する。
- 0~2の乱数を生成し、0なら赤、1なら緑、2なら青の最下位ビットにフラグのビットを挿入する。
- 3を画像のピクセル数分繰り返す。
- 画像を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 runningdmesg | grep KosenCTF
after logging in.
解答例
大きめのファイルをダウンロードすると、vdi形式のファイル(lost_world.vdi)が出てきました。 問題にもある通り、パスワードを忘れてしまったPCにログインできれば、良いようです。 以下の手順でrootのパスワードを変更してログインすることにしました。
- VirtualBoxに「lost_world.vdi」とUbuntuのライブDVDを刺した状態にし、UbuntuのライブDVDを起動します。
- 「Ubuntuを試す」を選択する。
- Terminal(Ctrl-Alt-T)を起動する。
- rootユーザになる。
$ sudo su
- マウントすべきディスクを探す。
$ fdisk -l
- 対象のディスクをマウントする。
$ mkdir /mnt/lost_world $ mount /dev/sda1 /mnt/lost_world
- chrootでルートディレクトリを変更する。
$ chroot /mnt/lost_world
- rootのパスワードを任意の値に変更する。
$ passwd root
- 終了して再起動する。lost_world.vdi側を起動させる。
- rootユーザでログインします。
- 「dmesg | grep KosenCTF」と入力するとフラグが取得できます。
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」と表示されます。
今回は、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つのボタンを正しく入力すると、フラグが出てくるようです。
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); } }
実行すると、メッセージボックスにフラグが出力されます。
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)が出てきました。
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で開いてみました。
デコンパイル結果を読んでみると、/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」)。
今回は、CyberChefでXORキーを求めました。
求めたXORキー(2E 57 ad 2E FF C8 CA 49)を基にdocument.zipを復号します。
復号したzipファイルを展開すると、document.pdfが入っていました。 このファイルを開くと、フラグを確認することができました。
FLAG
KosenCTF{p00r_shr3dd3r}