2024/06/16 SECCON Beginners CTF 2024 Writeup
SECCON Beginners
t6o_o6t.icon
CNDS 2024に参加していたときに日程被りに気づいた...
前半8時間くらいはまったく参加できなかった、もったいない..
assemble
Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!
はじめは何をするのかが分かりませんでした
ソースコードを読んだ
環境変数に設定されたflagを使った処理は、セッションのid属性が4になったタイミングで行われる
セッションのid属性は、数値ごとに設定された条件を満たすと増えていきそう
たとえばidを2にするには、raxの値を0x123にする、など
条件を満たすようにアセンブリを書いていく
少し詰まるのはid = 3のとき
一般的に、固定の文字列を表示する場合は別セクションにdbディレクティブで文字列を格納することが多いでしょう
しかし、今回の問題でこれを試したところ、実行してくれませんでした
適当なメモリアドレスを格納したレジスタを見つけて、その値をbuf引数(rsi)にmovします
https://scrapbox.io/files/666e9953544036001d4ffeeb.png
LINUX SYSTEM CALL TABLE FOR X86 64より
今回は事前に文字列をpushしたうえでmov rsi, rspしました
かなり詰まったのはid = 4のとき
問題文をよく読んでおらず、環境変数から読みだそうとしていた
アセンブリ内のpush命令をすべて削除したうえで、GDB上で実行してみた
rsp+0x18から環境変数が並んでいたので、これを使いたかった
今回の問題で試したところ、環境変数は出力されなかった..
ローカルで同じアセンブリを実行したときは上手く行ったが
→ 今回の問題はQilingで実行されているため、ローカルとは勝手が違いそう
問題文を読んだあと、flag.txtを読み取る方針に切り替えた
1. open(2)でファイルディスクリプタを取得する
./flag.txtという値を格納した
2. read(2)でファイル内容をbufに格納する
3. write(2)でbufの内容を標準出力する
以下、使用したアセンブリです
実際の解答画面では、 ; でコメントを書くと実行できないため取り除きます
code:writeup.s
; rspを保存しておく
mov r10, rsp
; ./flag.txtをstackに詰める
mov rax, 0x0000000000000074
push rax
mov rax, 0x78742e67616c662f
push rax
; open
mov rax, 2
mov rdi, rsp
mov rsi, 0
mov rdx, 0
syscall
; openの戻り値(rax)を保存
mov rbx, rax
; read(r10 (= ファイル名をpushする前のrsp の位置)から書き込んでもらう)
mov rax, 0
mov rdi, rbx
mov rsi, r10
mov rdx, 0x34
syscall
; write
mov rax, 1
mov rdi, 1
mov rbx, r10
mov rsi, rbx
mov rdx, 0x34
syscall
デバッグする際には、たとえば「openまで」「readまで」という単位で実行してみて、右のデバッグ画面を確認するのがおすすめです
openまで実行した場合は、openの戻り値が-1でない = 成功したかを確認できます
readまで実行した場合は、フラグの長さが確認できます。
ローカル環境であれば、straceを使ってシステムコール呼び出しの流れを確認できます
clamre
Woorker
getRank
1. まずブラウザで開いて、index.htmlに含まれているJavaScriptを読んでみる
GlobalなscopeでnumberToGuessが存在しているので、Devtoolsで確認すれば入力すべき値が分かりそう
2. 10回くらい繰り返す
3. rankが上がらないため、main.tsを確認する
{ input: (score) }という形式のJSONを送ることで、scoreに適当な整数を入れられそう
rankを1にするためには、10 ** 255を超えなければならない
4. 10 ** 226のscoreを送ってみる
t6o_o6t.iconはfetchで下記のようなリクエストを送った
code:payload_bad.js
fetch("...", { body: JSON.stringify({ score: "1" + "0".repeat(226) })})
しかし、次のようなスクリプトで、10で100回割られていた
code:reason.ts
if (score > 10 ** 255) {
// hmm...your score is too big?
// you need a handicap!
for (let i = 0; i < 100; i++) {
score = Math.floor(score / 10);
}
}
なので10 ** 356を送りたくなるが、300文字以上が送られるとInput too longという処理も行われている
5. コードを眺めてみる(10分くらい)
入力値のパースから判定まであまり間が無い
chall()だけを重点的に見ていくと良さそう
parseInt()は怪しくないか?
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/parseInt
mdnを確認したところ、いくつか特殊な仕様があることが分かった
不正に利用できる仕様がないかを確認していく
6. 使えそうな動作を2つ発見した
parseIntは、Numberで表現できないような長い文字列が与えられたとき、Infinityを返す
試してみると、Infinityは何度割ってもInfinityのままになる特別な値であった
code:research_infinity.js
for (let i = 150; i < 350; i++) { if ((10 ** i).toString() == "Infinity") { console.log(i); break; }}
このコードを実行して調べた結果、10 ** 309を表す文字列"1" + "0".repeat(309)を渡せばInfinityとしてparseされそう
しかし、今回は愚直にそれを行っても、300文字制限に抵触する
parseIntに0xで始まる文字列を渡すと、16進数としてパースされる
16進数であれば、10進数と同じ文字数でもより大きい数値を表せるはずである
7. 16進数を使って10 ** 309以上の値を表そう!
8. 結論
code:payload.js
fetch("...", { body: JSON.stringify({ score: "8e679c2f5e45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"})});