SECCON CTF 2022 Quals に参加した (babycmp, latexipy)
2022-11-15
SECCON CTF 2022 予選にp3r0zとして参加しました。908点で50位(国内15位)でした。
babycmp
$ ./chall.baby
Usage: ./chall.baby FLAG
$ ./chall.baby SECCON{test}
Wrong...
文字列を引数として与えると “Wrong…” もしくは “Correct!” のどちらかを表示するプログラムが与えられます。 “Correct!” を出力するような文字列を求めれば良いということですね。
他の Writeup で紹介されているような便利なツールを知らなかったため、 何も考えずにそのままアセンブリを読んで処理を把握してしまいました。
以下、どのようにアプローチしたかを簡単に書いておきます。
まずは Hopper で解析をして、なんとなくこのあたりに判定ロジックが含まれていそうだという認識を持ちます。 比較をしているアドレスに gdb でブレークポイントを貼って、引数を変化させた時の値の変化を見てみます。
* `AAA` の場合
> x/wx $r12
0x7fffffffd41c: 0x002d2416
> x/wx $r12+0x8
0x7fffffffd424: 0x524f4c4f
> x/wx $rsp+0x20
0x7fffffffcda0: 0x202f2004
> x/wx $rsp+0x28
0x7fffffffcda8: 0x357f1a44
* `AAB` の場合
> x/wx $r12
0x7fffffffd41c: 0x002e2416
> x/wx $r12+0x8
0x7fffffffd424: 0x524f4c4f
> x/wx $rsp+0x20
0x7fffffffcda0: 0x202f2004
> x/wx $rsp+0x28
0x7fffffffcda8: 0x357f1a44
* `AAAA` の場合
> x/wx $r12
0x7fffffffd41b: 0x222d2416
> x/wx $r12+0x8
0x7fffffffd423: 0x4f4c4f43
> x/wx $rsp+0x20
0x7fffffffcda0: 0x202f2004
> x/wx $rsp+0x28
0x7fffffffcda8: 0x357f1a44
以上の結果から
- 同じ文字数で1文字異なる入力を与えたときに値が1バイトしか変わっていない
- 入力文字列で変化した場所とメモリ中で値が変わっている場所の位置が揃っている
ということがわかります。 どうやらシンプルに文字単位で何かの変換が行われていそうです。
そのイメージを持った上でこの比較処理の直前にあるループ処理を読んでいくと、 単に文字列 “Welcome to SECCON 2022” と入力文字列が文字単位で xor されている、ということが分かりました。
|
|
SECCON{y0u_f0und_7h3_baby_flag_YaY}
latexipy
latexify を使った Python アプリケーション一式が渡されます。
Pythonの関数を入力として与えると、その関数をLaTeXの数式に落とし込んだ文字列を返してくれるようです。
app.py
を覗いてみると、入力として受け取ったPythonの関数を埋め込んだソースコードを生成し、
それを importlib
を使って動的にモジュールとして読み込むことで処理を実現しているようです。
ただし、任意のモジュールを import するときに任意コードが実行されると困るため、
入力として受け取った関数の名前を抽出する関数 get_fn_name
で validation も行っています。
以下、validationを行っている関数の概要です。
ast.parse
を使って、与えられた文字列から構文木を構築する- 構文木をもとに、以下の順番でチェックを行う
- root がモジュールになっていることをチェックする
- モジュールには関数定義がただ1つだけ含まれていることをチェックする
- 関数定義が
def \w+\((\w+(, \w+)*)?\):
であることをチェックする
- もしすべての条件にマッチした場合には、関数定義から関数名を返す
2番目の条件から、モジュールレベルで実行されるようなコードをそのまま書くことはできないようです。 また、3番目の条件の正規表現を見るとデフォルト引数を受け付けていないため1、 モジュールロード時点で評価されるような項を置くことは難しそうです。
当初は __builtins__
自体を上書き定義したり、builtins.__import__
をどうにかして定義してimportの挙動を変えようとしたり、
関数名を latexify
にしてシンボル名の上書きが発生するような状態で何かできないかを試していました。
(latexify自体に何か突破口がある可能性も考えましたが、一般的なライブラリであることから、決してそのライブラリの脆弱性をつくような物ではない、あり得るとしたら仕様として明記されているものくらいだというある種のメタ読みもしてしまいました。実際にlatexifyのコードも多少読みましたが、基本的には AST に落とし込んだ上で評価を行っているので、攻撃するのは難しそうです。)
この試行錯誤の途中で、Pythonのコメント行 #
については get_fn_name
関数で一切考慮されていないことに気が付きます。
これは ast.parse
が AST を構築するときにコメントについてはノードを構築しないのが理由です。
もちろん importlib
ではコメントも含めて Python のモジュールとして評価されるので、ここをうまくなんとか活かせればフラグの獲得に繋がりそうです。
誰も上げてなさそうなので SECCON 2022 の latexipy の解法ですhttps://t.co/UMsC6t8VrV
— Yuki Igarashi (@bonprosoft) November 13, 2022
最初は __builtins__を定義したり import hook をねじ込もうとしたり頑張った。途中でコメントが AST には反映されないことに気がついて、coding: utf-8 みたいなディレクティブでうまく頑張れないか探した
結果、raw_unicode_escape とかいうものを coding に指定すると、 \u で始まるものをその通りに扱ってくれるということを知った。
— Yuki Igarashi (@bonprosoft) November 13, 2022
改行文字を挟むことで、見た目はコメントアウトされていても Pythonが実行したときには改行が挟まっていて実行されちゃうファイルが作れる。
Pythonで見かける特殊なコメントとしてエンコーディングに関するディレクティブ # coding: utf-8
がありますが、そのあたりをうまくつかうことで攻撃できないでしょうか?
と思いながら調べた結果、どうやら coding: raw_unicode_escape
をつかうことでコード中にUnicode escapeを使えることがわかりました。
あとはこれを使ってコメント行の後に改行を挟んであげることで、モジュールとして使われたときに os.system
が走り、単に文字列評価をするとコメントとなるようなスクリプトを作ることができます。
|
|
$ cat exploit.py | nc latexipy.seccon.games 2337
Latexify as a Service!
E.g.
``
def solve(a, b, c):
return (-b + math.sqrt(b**2 - 4*a*c)) / (2*a)
``
ref. https://github.com/google/latexify_py/blob/v0.1.1/examples/equation.ipynb
Input your function (the last line must start with __EOF__):
SECCON{UTF7_is_hack3r_friend1y_encoding}
Result:
\mathrm{hoge}() \triangleq 42
SECCON{UTF7_is_hack3r_friend1y_encoding}
Pythonの関数定義のスコープはモジュールレベルです。すなわち、デフォルト引数のオブジェクトはモジュールロード時に一度だけ評価されます。 ↩︎