2024/06/23 WaniCTF Writeup
beginnerから解いていかないと後で損をする..
beginnerは最終的に得点が大きく下がるので、難問のFirst Bloodを目指さない限り最初に解くのが最適ではないか
Reversing
[rev] lambda
code:lambda.py
(lambda _0: _0(input))(lambda _1: (lambda _2: _2('Enter the flag: '))(lambda _3: (lambda _4: _4(_1(_3)))(lambda _5: (lambda _6: _6(''.join))(lambda _7: (lambda _8: _8(lambda _9: _7((chr(ord(c) + 12) for c in _9))))(lambda _10: (lambda _11: _11(''.join))(lambda _12: (lambda _13: _13((chr(ord(c) - 3) for c in _10(_5))))(lambda _14: (lambda _15: _15(_12(_14)))(lambda _16: (lambda _17: _17(''.join))(lambda _18: (lambda _19: _19(lambda _20: _18((chr(123 ^ ord(c)) for c in _20))))(lambda _21: (lambda _22: _22(''.join))(lambda _23: (lambda _24: _24((_21(c) for c in _16)))(lambda _25: (lambda _26: _26(_23(_25)))(lambda _27: (lambda _28: _28('16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r'))(lambda _29: (lambda _30: _30(''.join))(lambda _31: (lambda _32: _32((chr(int(c,36) + 10) for c in _29.split('_'))))(lambda _33: (lambda _34: _34(_31(_33)))(lambda _35: (lambda _36: _36(lambda _37: lambda _38: _37 == _38))(lambda _39: (lambda _40: _40(print))(lambda _41: (lambda _42: _42(_39))(lambda _43: (lambda _44: _44(_27))(lambda _45: (lambda _46: _46(_43(_45)))(lambda _47: (lambda _48: _48(_35))(lambda _49: (lambda _50: _50(_47(_49)))(lambda _51: (lambda _52: _52('Correct FLAG!'))(lambda _53: (lambda _54: _54('Incorrect'))(lambda _55: (lambda _56: _56(_41(_53 if _51 else _55)))(lambda _57: lambda _58: _58)))))))))))))))))))))))))))
はじめは人力でやろうと思ったが、難しい & 量が多いt6o_o6t.icon
先にlambdifierをGit cloneしておき、次のコマンドを実行
$ python3 ./lambdifier.py -d ./lambda.py > lambda.out.py
code:lambda.out.py
_1 = input
_3 = 'Enter the flag: '
_5 = _1(_3)
_7 = ''.join
_10 = lambda _9: _7((chr(ord(c) + 12) for c in _9))
_12 = ''.join
_14 = (chr(ord(c) - 3) for c in _10(_5))
_16 = _12(_14)
_18 = ''.join
_21 = lambda _20: _18((chr(123 ^ ord(c)) for c in _20))
_23 = ''.join
_25 = (_21(c) for c in _16)
_27 = _23(_25)
_29 = '16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r'
_31 = ''.join
_33 = (chr(int(c, 36) + 10) for c in _29.split('_'))
_35 = _31(_33)
_39 = lambda _37: lambda _38: _37 == _38
_41 = print
_43 = _39
_45 = _27
_47 = _43(_45)
_49 = _35
_51 = _47(_49)
_53 = 'Correct FLAG!'
_55 = 'Incorrect'
_57 = _41(_53 if _51 else _55)
これを整形して意味をエスパーした結果:
code:shaped.py
entered = input('Enter the flag: ')
def shift12(_9):
return ''.join(chr(ord(c) + 12) for c in _9)
shifted9 = ''.join(chr(ord(c) - 3) for c in shift12(entered))
def xor123(_20):
return ''.join(chr(123 ^ ord(c)) for c in _20)
shifted9_xored123 = ''.join(xor123(c) for c in shifted9)
disarranged_flag = '16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r'
base36 = ''.join(chr(int(c, 36) + 10) for c in disarranged_flag.split('_'))
if shifted9_xored123 == base36:
print('Correct FLAG!')
else:
print('Incorrect')
上記に示した変数 base36の内容は、入力値enteredの内容に関わらず固定です。
また、shifted9_xored123は、ユーザの入力値の「各文字コードを+9ずらし、123でXOR」した文字列と言えます。
この操作を「shift9 and xor123」とします。
したがって、適切な入力値を得るには、base36に対し、「shift9 and xor123」の逆操作を行えば良さそうです。
すなわち、base36に対して操作 xor123 をしたのち、文字コードを-9ずらします。
ゆえに、以下のようなSolverを考えることができます。
code:solver.py
# 変数 base36 を計算する
disarranged_flag = '16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r'
# base36 に xor123
# xored123 に shift9 の逆操作
# 操作結果を計算する
print("".join(result))
このPythonファイルを実行すれば、フラグを得ることができます。
[rev] home
ELFバイナリが与えられます。
シンボルがstripされていないため、Ghidraでmain関数の内容を確認できます。
次のように整形しました。
manを読みながら変数名や型を補いました。
code:home.c
int main(void) {
int ret;
char *absPath;
long traceSucceeded;
absPath = getcwd(cwd,0x400);
if (absPath == (char *)NULL) {
perror("Error");
ret = 1;
} else {
absPath = strstr(cwd,"Service");
if (absPath == (char *)NULL) {
puts(";)");
} else {
puts("Check passed!");
traceSucceeded = ptrace(PTRACE_TRACEME,0,0,0);
if (traceSucceeded == -1) {
puts("Debugger detected!");
ret = 1;
goto endMain;
}
constructFlag();
}
ret = 0;
}
return ret;
}
今回の解法では、gdbを使いましたt6o_o6t.icon
理由
露骨にAnti Debugが書かれている → Anti Debugをbypassする問題の可能性がありそう 次の記事が見つかりましたt6o_o6t.icon
「ptrace」の見出しにある内容を読むと、001019eeにある e8 8d f6 ff ffをNOP命令で潰せばAnti Debugを無効化できそうだと考えられます 解法
1. chal_homeのコピーを作成する
2. コピーしたファイルに対し、Stirlingを使って、ptraceの呼び出し部分をNOP命令で潰す 3. コピーしたファイルをgdb上で実行する
4. 次のように操作する
1. startでmain関数の先頭まで実行
2. b *constructFlagでconstructFlag()にブレークポイントを設置 3. cで実行を続行
4. constructFlag()の先頭に到達するので、x/10i $pcを繰り返して関数の末尾を見つける
5. 末尾にブレークポイントを設置
6. cで実行を続行
7. constructFlag()の末尾に到達するので、フラグを頑張って見つける
どう見つけたかを忘れたが、x/100s $bpみたいにやっても見つかりそう
[rev] thread
ELFバイナリが与えられます。
Ghidraを使ってmain関数を次のように静的解析しました。
https://scrapbox.io/files/6678332d98bae1001de66836.png
1. スレッドを0x2d個作成する。
2. あるルーチン f を、引数にスレッドのindex(上記の解析ではj)を渡して実行
3. 計算が終わったら、上記の解析におけるinputAsIntとDAT_00104020を比較して終了。
スレッドが実行するルーチンは、次のような内容でした。
引数名をargとしています。
code:thread.c
i = 0;
while (i < 3) {
pthread_mutex_lock(&mutex);
if (r == 0) {
}
if (r == 1) {
}
if (r == 2) {
}
i++;
pthread_mutex_unlock(&mutex);
}
各スレッドはDAT_00104020[arg]やinputAsInt[arg]など、スレッドごとに異なる領域を使って計算をするので、計算結果はスレッドの実行順に依りません。
ここから、次のように考えました
DAT_00104020は.dataセクションにあり、初期値が分かる
その値を抜き出してinitial_dとする
すべてのルーチンが完了したときのDAT_00104020の値をfinal_dとする
initial_dのi番目の要素について、次のように考える
initial_d[i] + iを3で割ったあまりが0なら..
inputAsInt[i]に対しては、次の順序で操作が行われるはずだ
1. 3をかける
2. 5を足す
3. 0x7fのXORを取る
割ったあまりが1なら..
1. 5を足す
2. 0x7fのXORを取る
3. 3をかける
割ったあまりが2の場合も同様のことを考えられる
ここで、final_d[i] == initial_d[i] + 3の関係が成り立つ
今回はfinal_d[i] == inputAsInt[i]を成り立たせたい
つまり、initial_d[i] + 3 == inputAsInt[i]を成り立たせたい
そこで、次のことを考える
initial_d[i] + iを3で割ったあまりが0のとき
inputAsInt[i]には、3をかけ、5を足し、0x7fのXORが取られるはず
フラグを得るための正しい入力文字列をxとする。
x[i]を得るには、((x[i] * 3) + 5) ^ 0x7f = initial_d[i] + 3という方程式?を解けばよい
これは、x[i] = (((initial_d[i] + 3) ^ 0x7f) - 5) // 3で得られる
// 3をしてしまって大丈夫か不安になるが、実際にsolverを書くと3で割り切れないことはなかったt6o_o6t.icon
割ったあまりが1, 2のときも同様に考える
以上の考えをすべてのiに対して適用すれば、Solverを書くことができる
Solver:
code:solver.py
Forensics
[for] codebreaker
上から黒いX印で覆い隠されたQRコード画像が与えられます。
「broken qrcode」で検索して出てきたページでポチポチ打ち込みましたt6o_o6t.icon
https://scrapbox.io/files/667838c1e556f5001dff9303.png
白い部分と黒い部分は、色が明らかな点。
灰色の部分は元画像で覆い隠されており白黒が不明だった点。
単純作業に40分くらいかかったので、より早い解法を知りたい..t6o_o6t.icon