9/16 から 17 にかけて開催されていた SECCON CTF 2023 の予選に 0nePadding として参加しました。
個人では Rev の問題を 2 問解き、最終順位は国内 35 位、全体で 87 位でした。
SECCON の国内本選の参加ラインである国内順位での上位 10 チームまではまだまだ壁が厚いですが、引き続き精進していこうと思います。
解いてない問題の復習には時間がかかりそうなので、とりあえず解けた問題の Writeup を書きます。
もくじ
jumpout(Rev)
Sequential execution
今回の Rev の Warmup 問です。
Flag 取得の難易度的には簡単でしたが、Writer の方がバイナリやデバッガに精通しているんだろうなということを強く感じる非常に興味深い問題でした。(この問題どうやって作ってるのか、ソースコードがあれば見たい、、、)
バイナリ自体は入力値として受け取った Flag を検証するだけのシンプルなプログラムのようでしたので、Ghidra で解析していきます。
いつも通りバイナリを entry から解析し、main 関数を特定したところ、FLAG:
というテキストを用いて何かしらの処理を呼び出しているようですが、Listing ウインドウの結果が壊れているせいか上手く処理をデコンパイルできていません。
ただし、main 関数で参照している 0x1011d0 の関数を参照すると、上手くデコンパイルできてはいないものの、スタック内の(おそらく)入力文字を check 関数に与えていることがわかります。(check 関数は自分で rename した関数)
check 関数は以下のような関数でした。
undefined8 check(char *param_1)
{
long lVar1;
size_t sVar2;
ulong uVar3;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
sVar2 = strlen(param_1);
if (sVar2 == 0x1d) {
do {
uVar3 = 0;
do {
FUN_00101360(param_1[uVar3],uVar3 & 0xffffffff);
uVar3 = uVar3 + 1;
} while (uVar3 != 0x1d);
} while( true );
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 1;
}
この処理から、Flag の正しい文字数は 0x1d であることがわかります。
また、入力値の 1 文字とインデックスを引数として、 FUN_00101360 という関数を呼び出していることがわかります。
FUN00101360 は以下のような関数で、入力値の 1 文字とインデックス、0x55 と (&DAT00104010)[インデックス] の 4 つを XOR した値を返します。
uint FUN_00101360(uint param_1,uint param_2)
{
long in_FS_OFFSET;
if (*(long *)(in_FS_OFFSET + 0x28) != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail((long)(int)param_2,param_2,7);
}
return param_1 ^ param_2 ^ 0x55 ^ (uint)(byte)(&DAT_00104010)[(int)param_2];
}
しかし、先ほど確認した check 関数のデコンパイル結果を見てわかる通り、デコンパイル結果内には FUN_00101360 の戻り値を扱う処理が見当たりません。
恐らく返された値を何らかのハードコードされた値と比較することで検証しそうだという予想が立ちますが、こちらの関数もデコンパイル結果が壊れているようで、正しい処理を参照できる状態になっていないようでした。
仕方がないので check 関数のアセンブリを読んでみると、DAT_00104030 のデータをロードしており、あとで使用しようとしている可能性が高いことがわかりました。
若干 guess が入っていますが、このデータを比較対象として以下の Solver を作成したところ Flag を取得することができました。
d = [ 0xf6, 0xf5, 0x31, 0xc8, 0x81, 0x15, 0x14, 0x68, 0xf6, 0x35, 0xe5, 0x3e, 0x82, 0x09, 0xca, 0xf1, 0x8a, 0xa9, 0xdf, 0xdf, 0x33, 0x2a, 0x6d, 0x81, 0xf5, 0xa6, 0x85, 0xdf, 0x17 ]
e = [ 0xf0, 0xe4, 0x25, 0xdd, 0x9f, 0x0b, 0x3c, 0x50, 0xde, 0x04, 0xca, 0x3f, 0xaf, 0x30, 0xf3, 0xc7, 0xaa, 0xb2, 0xfd, 0xef, 0x17, 0x18, 0x57, 0xb4, 0xd0, 0x8f, 0xb8, 0xf4, 0x23, 0x00 ]
for i in range(0x1d):
print(chr(0x55^ i ^ d[i] ^ e[i]), end="")
コンテスト中は上記の Solver までスムーズに到達したのですが、あとから見返してみるとかなり読みづらいバイナリで、動的解析が前提になっていそうでした。
とはいえ大筋は変わらないので、check 関数まで特定した後でデバッガで詳細な挙動を追っていけば同じ結論に到達します。
Sickle(Rev)
Pickle infected with COVID-19
実力不足なのでこの問題のような気合いで読む系の Rev 問を解くのに時間がかかり、結局コンテストの終了ギリギリでようやく Flag を通すことができました。
問題バイナリとして以下のスクリプトが与えられました。
import pickle, io
payload = b'\x8c\x08builtins\x8c\x07getattr\x93\x942\x8c\x08builtins\x8c\x05input\x93\x8c\x06FLAG> \x85R\x8c\x06encode\x86R)R\x940g0\n\x8c\x08builtins\x8c\x04dict\x93\x8c\x03get\x86R\x8c\x08builtins\x8c\x07globals\x93)R\x8c\x01f\x86R\x8c\x04seek\x86R\x94g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__add__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__mul__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x06__eq__\x86R\x940g3\ng5\n\x8c\x08builtins\x8c\x03len\x93g1\n\x85RM@\x00\x86RM\x05\x01\x86R\x85R.0g0\ng1\n\x8c\x0b__getitem__\x86R\x940M\x00\x00\x940g2\ng3\ng0\ng6\ng7\n\x85R\x8c\x06__le__\x86RM\x7f\x00\x85RMJ\x01\x86R\x85R.0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM@\x00\x86RMU\x00\x86RM"\x01\x86R\x85R0g0\ng0\n]\x94\x8c\x06append\x86R\x940g8\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\nfrom_bytes\x86R\x940M\x00\x00p7\n0g9\ng11\ng6\n\x8c\x08builtins\x8c\x05slice\x93g4\ng7\nM\x08\x00\x86Rg4\ng3\ng7\nM\x01\x00\x86RM\x08\x00\x86R\x86R\x85R\x8c\x06little\x86R\x85R0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RMw\x00\x86RM\xc9\x01\x86R\x85R0g0\n]\x94\x8c\x06append\x86R\x940g0\ng12\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__xor__\x86R\x940I1244422970072434993\n\x940M\x00\x00p7\n0g13\n\x8c\x08builtins\x8c\x03pow\x93g15\ng10\ng7\n\x85Rg16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RM\x83\x00\x86RM\xa7\x02\x86R\x85R0g0\ng12\n\x8c\x06__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R.'
f = io.BytesIO(payload)
res = pickle.load(f)
if isinstance(res, bool) and res:
print("Congratulations!!")
else:
print("Nope")
これを実行すると、ハードコードされたバイナリが pickle.load(f)
でデシリアライズされる際に実行され、Flag 文字列の要求と検証が行われることがわかりました。
pickle というライブラリは初めて使ったのですが、Python オブジェクトのシリアライズとデシリアライズが可能なモジュールのようです。
また、pickle の load メソッドでデシリアライズを行う場合は、実際のところ内部で VM として処理されるため、任意のコード実行が可能な機能を持っているそうです。
この問題でも、payload として定義された一連のバイナリデータがロードされることで、Flag の入力を受け取って検証する処理が実行されます。
参考:pickle --- Python オブジェクトの直列化 — Python 3.11.5 ドキュメント
解析のためにはまず payload のディスアセンブルを行う必要があります。
ディスアセンブルは pickletools.dis()
を使用することで行うことができます。
import pickletools
payload = b'\x8c\x08builtins\x8c\x07getattr\x93\x942\x8c\x08builtins\x8c\x05input\x93\x8c\x06FLAG> \x85R\x8c\x06encode\x86R)R\x940g0\n\x8c\x08builtins\x8c\x04dict\x93\x8c\x03get\x86R\x8c\x08builtins\x8c\x07globals\x93)R\x8c\x01f\x86R\x8c\x04seek\x86R\x94g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__add__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__mul__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x06__eq__\x86R\x940g3\ng5\n\x8c\x08builtins\x8c\x03len\x93g1\n\x85RM@\x00\x86RM\x05\x01\x86R\x85R.0g0\ng1\n\x8c\x0b__getitem__\x86R\x940M\x00\x00\x940g2\ng3\ng0\ng6\ng7\n\x85R\x8c\x06__le__\x86RM\x7f\x00\x85RMJ\x01\x86R\x85R.0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM@\x00\x86RMU\x00\x86RM"\x01\x86R\x85R0g0\ng0\n]\x94\x8c\x06append\x86R\x940g8\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\nfrom_bytes\x86R\x940M\x00\x00p7\n0g9\ng11\ng6\n\x8c\x08builtins\x8c\x05slice\x93g4\ng7\nM\x08\x00\x86Rg4\ng3\ng7\nM\x01\x00\x86RM\x08\x00\x86R\x86R\x85R\x8c\x06little\x86R\x85R0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RMw\x00\x86RM\xc9\x01\x86R\x85R0g0\n]\x94\x8c\x06append\x86R\x940g0\ng12\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__xor__\x86R\x940I1244422970072434993\n\x940M\x00\x00p7\n0g13\n\x8c\x08builtins\x8c\x03pow\x93g15\ng10\ng7\n\x85Rg16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RM\x83\x00\x86RM\xa7\x02\x86R\x85R0g0\ng12\n\x8c\x06__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R.'
pickletools.dis(payload, annotate=True)
参考:pickletools --- pickle 開発者のためのツール群 — Python 3.11.5 ドキュメント
しかし、途中で STOP 命令が記述されているせいで、pickletools ではオフセット 0x105 までしか読み取ることができません。
しかも、payload の範囲を Slice で指定しても、POP や GET の命令を pickletools 側で解釈できなくなってしまうためにエラーが発生してディスアセンブルに失敗します。
そこで、この payload をディスアセンブルできる他のツールを探したところ、r2pickledec という radare2 のプラグインを見つけました。
参考:doyensec/r2pickledec: Pickle decompiler plugin for Radare2
このツールは不具合が多くインストール時やインストール後も様々な問題が発生しましたが、とりあえず最新の radare2 をソースコードからビルドした後にプラグインをインストールすることで、payload の全文をディスアセンブルすることに成功しました。
ここでディスアセンブルした結果に解析コメントを付けたものは以下の通りです。
0x000 8c086275696c. short_binunicode "builtins" ; 0x2
0x00a 8c0767657461. short_binunicode "getattr" ; 0xc
0x013 93 stack_global
0x014 94 memoize
0x015 32 dup
memo0: getattr()
// getattr()
// getattr()
0x016 8c086275696c. short_binunicode "builtins" ; 0x18
0x020 8c05696e7075. short_binunicode "input" ; 0x22 ; 2'"'
0x027 93 stack_global
0x028 8c06464c4147. short_binunicode "FLAG> " ; 0x2a ; 2'*'
0x030 85 tuple1
0x031 52 reduce
// getattr()
// getattr()
// input("FLAG> ")
0x032 8c06656e636f. short_binunicode "encode" ; 0x34 ; 2'4'
0x03a 86 tuple2
0x03b 52 reduce
0x03c 29 empty_tuple
0x03d 52 reduce
0x03e 94 memoize
0x03f 30 pop
memo0: getattr()
memo1: Flag
0x040 67300a get "0" ; 0x41 ; 2'A'
0x043 8c086275696c. short_binunicode "builtins" ; 0x45 ; 2'E'
0x04d 8c0464696374 short_binunicode "dict" ; 0x4f ; 2'O'
0x053 93 stack_global
0x054 8c03676574 short_binunicode "get" ; 0x56 ; 2'V'
0x059 86 tuple2
0x05a 52 reduce
0x05b 8c086275696c. short_binunicode "builtins" ; 0x5d ; 2']'
0x065 8c07676c6f62. short_binunicode "globals" ; 0x67 ; 2'g'
0x06e 93 stack_global
0x06f 29 empty_tuple
0x070 52 reduce
0x071 8c0166 short_binunicode "f" ; 0x73 ; 2's'
0x074 86 tuple2
0x075 52 reduce
0x076 8c047365656b short_binunicode "seek" ; 0x78 ; 2'x'
0x07c 86 tuple2
0x07d 52 reduce
// dict.get(builtins.globals(), f).seek()
0x07e 94 memoize
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek()
0x07f 67300a get "0" ; 0x80
0x082 8c086275696c. short_binunicode "builtins" ; 0x84
0x08c 8c03696e74 short_binunicode "int" ; 0x8e
0x091 93 stack_global
// getattr()
// builtins.int()
// __add__
0x092 8c075f5f6164. short_binunicode "__add__" ; 0x94
0x09b 86 tuple2
// getattr()
// (builtins.int(), __add__)
0x09c 52 reduce
0x09d 94 memoize
0x09e 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek()
memo3: add()
0x09f 67300a get "0" ; 0xa0
0x0a2 8c086275696c. short_binunicode "builtins" ; 0xa4
0x0ac 8c03696e74 short_binunicode "int" ; 0xae
0x0b1 93 stack_global
0x0b2 8c075f5f6d75. short_binunicode "__mul__" ; 0xb4
0x0bb 86 tuple2
0x0bc 52 reduce
0x0bd 94 memoize
0x0be 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek()
memo3: add()
memo4: mul()
0x0bf 67300a get "0" ; 0xc0
0x0c2 8c086275696c. short_binunicode "builtins" ; 0xc4
0x0cc 8c03696e74 short_binunicode "int" ; 0xce
0x0d1 93 stack_global
0x0d2 8c065f5f6571. short_binunicode "__eq__" ; 0xd4
0x0da 86 tuple2
0x0db 52 reduce
0x0dc 94 memoize
0x0dd 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
0x0de 67330a get "3" ; 0xdf
0x0e1 67350a get "5" ; 0xe2
0x0e4 8c086275696c. short_binunicode "builtins" ; 0xe6
0x0ee 8c036c656e short_binunicode "len" ; 0xf0
0x0f3 93 stack_global
0x0f4 67310a get "1" ; 0xf5
0x0f7 85 tuple1
0x0f8 52 reduce
// add()
// eq()
// builtins.len(flag)
0x0f9 4d4000 binint2 0x40 ; '@'
0x0fc 86 tuple2
// add()
// eq()
// (builtins.len(flag), 0x40)
0x0fd 52 reduce
0x0fe 4d0501 binint2 0x105
0x101 86 tuple2
// add()
// (bool, 0x015)
0x102 52 reduce
// add(bool, 0x105)
0x103 85 tuple1
// jmp
// 0x106
0x104 52 reduce
0x105 2e stop -> Fail
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
0x106 30 pop
0x107 67300a get "0" ; 0x108
0x10a 67310a get "1" ; 0x10b
0x10d 8c0b5f5f6765. short_binunicode "__getitem__" ; 0x10f
// getattr()
// Flag
// __getitem__
0x11a 86 tuple2
0x11b 52 reduce
// getattr(Flag, __getitem__)
0x11c 94 memoize
0x11d 30 pop
0x11e 4d0000 binint2 0x0
0x121 94 memoize
# loop 開始
0x122 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0
0x123 67320a get "2" ; 0x124
0x126 67330a get "3" ; 0x127
0x129 67300a get "0" ; 0x12a
0x12c 67360a get "6" ; 0x12d
0x12f 67370a get "7" ; 0x130
// jmp
// add()
// getattr()
0x132 85 tuple1
0x133 52 reduce
// jmp
// add()
// getattr()
// Flag[0], __le__
0x134 8c065f5f6c65. short_binunicode "__le__" ; 0x136
0x13c 86 tuple2
0x13d 52 reduce
// jmp
// add()
// getattr((Flag[0], __le__))
0x13e 4d7f00 binint2 0x7f ; '\x7f'
0x141 85 tuple1
0x142 52 reduce
// jmp
// add()
// (Flag[0] <= 0x7f)
// 0x14a
0x143 4d4a01 binint2 0x14a
0x146 86 tuple2
0x147 52 reduce
0x148 85 tuple1
0x149 52 reduce
0x14a 2e stop
// jmp
// add()
// (Flag[0] <= 0x7f)
// 0x14a
0x14b 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0
0x14c 67320a get "2" ; 0x14d
0x14f 67330a get "3" ; 0x150
0x152 67340a get "4" ; 0x153
0x155 67350a get "5" ; 0x156
0x158 67330a get "3" ; 0x159
0x15b 67370a get "7" ; 0x15c
0x15e 4d0100 binint2 0x1
0x161 86 tuple2
0x162 52 reduce
0x163 70370a put "7" ; 0x164
// jmp
// add()
// mul()
// eq()
// 1
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 1
0x166 4d4000 binint2 0x40 ; '@'
0x169 86 tuple2
// jmp
// add()
// mul()
// eq()
// (1, 0x40)
0x16a 52 reduce
0x16b 4d5500 binint2 0x55 ; 'U'
0x16e 86 tuple2
0x16f 52 reduce
// jmp
// add()
// mul((eq(1, 0x40), 0x55))
0x170 4d2201 binint2 0x122
0x173 86 tuple2
// jmp
// add()
// (mul((eq(1, 0x40), 0x55)), 0x122)
# i が 0x40 になるまでループ
# すべての文字が ASCII 空間にあることを確認
0x174 52 reduce
0x175 85 tuple1
0x176 52 reduce
// jmp
// add((mul((eq(1, 0x40), 0x55)), 0x122))
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0x40
0x177 30 pop
0x178 67300a get "0" ; 0x179
0x17b 67300a get "0" ; 0x17c
0x17e 5d empty_list
0x17f 94 memoize
// getattr()
// getattr()
// empty_list
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0x40
memo8: []
0x180 8c0661707065. short_binunicode "append" ; 0x182
0x188 86 tuple2
0x189 52 reduce
// getattr()
// getattr((empty_list, append))
0x18a 94 memoize
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0x40
memo8: []
memo9: memo8 への append
0x18b 30 pop
// getattr()
0x18c 67380a get "8" ; 0x18d
0x18f 8c0b5f5f6765. short_binunicode "__getitem__" ; 0x191
0x19c 86 tuple2
0x19d 52 reduce
0x19e 94 memoize
// getattr([], __getitem__)
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0x40
memo8: []
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
0x19f 30 pop
0x1a0 67300a get "0" ; 0x1a1
0x1a3 8c086275696c. short_binunicode "builtins" ; 0x1a5
0x1ad 8c03696e74 short_binunicode "int" ; 0x1af
0x1b2 93 stack_global
0x1b3 8c0a66726f6d. short_binunicode "from_bytes" ; 0x1b5
0x1bf 86 tuple2
0x1c0 52 reduce
// getattr(int, from_bytes)
0x1c1 94 memoize
0x1c2 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0x40
memo8: []
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
0x1c3 4d0000 binint2 0x0
0x1c6 70370a put "7" ; 0x1c7
# ループ
0x1c9 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0
memo8: []
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
0x1ca 67390a get "9" ; 0x1cb
0x1cd 6731310a get "11" ; 0x1ce
0x1d1 67360a get "6" ; 0x1d2
0x1d4 8c086275696c. short_binunicode "builtins" ; 0x1d6
0x1de 8c05736c6963. short_binunicode "slice" ; 0x1e0
0x1e5 93 stack_global
0x1e6 67340a get "4" ; 0x1e7
0x1e9 67370a get "7" ; 0x1ea
0x1ec 4d0800 binint2 0x8
// m8.append()
// int.from_bytes()
// flag[x]
// slice
// mul()
// i = 0
// 0x8
0x1ef 86 tuple2
0x1f0 52 reduce
// m8.append()
// int.from_bytes()
// flag[x]
// slice
// mul(i, 0x8)
0x1f1 67340a get "4" ; 0x1f2
0x1f4 67330a get "3" ; 0x1f5
0x1f7 67370a get "7" ; 0x1f8
0x1fa 4d0100 binint2 0x1
// m8.append()
// int.from_bytes()
// flag[x]
// slice
// mul(i, 0x8)
// mul()
// add()
// i = 0
// 1
0x1fd 86 tuple2
0x1fe 52 reduce
0x1ff 4d0800 binint2 0x8
0x202 86 tuple2
0x203 52 reduce
0x204 86 tuple2
0x205 52 reduce
0x206 85 tuple1
// m8.append()
// int.from_bytes()
// flag[x]
// slice[mul(i, 0x8), mul(add(i + 1), 8)]
0x207 52 reduce
// m8.append()
// int.from_bytes()
// flag[i*8:(i+1)*8]
0x208 8c066c697474. short_binunicode "little" ; 0x20a
0x210 86 tuple2
0x211 52 reduce
// m8.append()
// int.from_bytes(flag[i*8:(i+1)*8], little)
0x212 85 tuple1
0x213 52 reduce
0x214 30 pop
# 8 バイトずつ Flag 文字を int 化して抜き出して m8 に追加
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0
memo8: []
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
0x215 67320a get "2" ; 0x216
0x218 67330a get "3" ; 0x219
0x21b 67340a get "4" ; 0x21c
0x21e 67350a get "5" ; 0x21f
0x221 67330a get "3" ; 0x222
0x224 67370a get "7" ; 0x225
0x227 4d0100 binint2 0x1
0x22a 86 tuple2
0x22b 52 reduce
// jmp
// add()
// mul()
// eq()
// add(i + 1)
0x22c 70370a put "7" ; 0x22d
0x22f 4d0800 binint2 0x8
0x232 86 tuple2
0x233 52 reduce
0x234 4d7700 binint2 0x77 ; 'w'
0x237 86 tuple2
0x238 52 reduce
0x239 4dc901 binint2 0x1c9
0x23c 86 tuple2
0x23d 52 reduce
# i を上書き
// jmp
// add(mul(eq(add(i + 1), 0x8), 0x77), 0x1c9)
0x23e 85 tuple1
0x23f 52 reduce
# 8 バイトずつ Flag 文字を int 化して抜き出して m8 に追加
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 8
memo8: [<Flag の 8 バイトごとに little>]
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
# i が 8 ならループを抜ける
0x240 30 pop
0x241 67300a get "0" ; 0x242
0x244 5d empty_list
0x245 94 memoize
// getattr()
// empty_list
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 8
memo8: arr1 [<Flag の 8 バイトごとに little>]
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
memo12: arr2 []
0x246 8c0661707065. short_binunicode "append" ; 0x248
0x24e 86 tuple2
0x24f 52 reduce
// getattr(empty_list, append)
0x250 94 memoize
0x251 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 8
memo8: arr1 [<Flag の 8 バイトごとに little>]
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
memo12: arr2 []
memo13: memo12 への append
0x252 67300a get "0" ; 0x253
0x255 6731320a get "12" ; 0x256
0x259 8c0b5f5f6765. short_binunicode "__getitem__" ; 0x25b
0x266 86 tuple2
0x267 52 reduce
0x268 94 memoize
0x269 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 8
memo8: arr1 [<Flag の 8 バイトごとに little>]
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
memo12: arr2 []
memo13: memo12 への append
memo14: index を指定して memo12 から値を取得
0x26a 67300a get "0" ; 0x26b
0x26d 8c086275696c. short_binunicode "builtins" ; 0x26f
0x277 8c03696e74 short_binunicode "int" ; 0x279
0x27c 93 stack_global
0x27d 8c075f5f786f. short_binunicode "__xor__" ; 0x27f
0x286 86 tuple2
0x287 52 reduce
// getattr(int, xor)
0x288 94 memoize
0x289 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 8
memo8: arr1 [<Flag の 8 バイトごとに little>]
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
memo12: arr2 []
memo13: memo12 への append
memo14: index を指定して memo12 から値を取得
memo15: int.xor()
0x28a 493132343434. int "1244422970072434993" ; 0x28b
0x29f 94 memoize
0x2a0 30 pop
0x2a1 4d0000 binint2 0x0
0x2a4 70370a put "7" ; 0x2a5
0x2a7 30 pop
memo0: getattr()
memo1: Flag
memo2: dict.get(builtins.globals(), f).seek() -> jmp
memo3: add()
memo4: mul()
memo5: eq()
memo6: getattr(Flag, __getitem__)[x] -> 多分入力値の取得
memo7: i = 0
memo8: arr1 [<Flag の 8 バイトごとに little>]
memo9: memo8 への append
memo10: index を指定して memo8 から値を取得
memo11: int.from_bytes()
memo12: arr2 []
memo13: memo12 への append
memo14: index を指定して memo12 から値を取得
memo15: int.xor()
memo16: key = 1244422970072434993
0x2a8 6731330a get "13" ; 0x2a9
0x2ac 8c086275696c. short_binunicode "builtins" ; 0x2ae
0x2b6 8c03706f77 short_binunicode "pow" ; 0x2b8
0x2bb 93 stack_global
// arr2.append()
// pow
0x2bc 6731350a get "15" ; 0x2bd
0x2c0 6731300a get "10" ; 0x2c1
0x2c4 67370a get "7" ; 0x2c5
0x2c7 85 tuple1
0x2c8 52 reduce
0x2c9 6731360a get "16" ; 0x2ca ; "16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01"
0x2cd 86 tuple2
0x2ce 52 reduce
// arr2.append()
// pow()
// xor(arr1[i], 1244422970072434993))
0x2cf 493635353337. int "65537" ; 0x2d0
0x2d6 493138343436. int "18446744073709551557" ; 0x2d7
0x2ec 87 tuple3
0x2ed 52 reduce
// arr2.append()
// pow(
(xor(arr1[i], 1244422970072434993), 65537, 18446744073709551557)
)
0x2ee 85 tuple1
0x2ef 52 reduce
0x2f0 30 pop
# 結果を memo12 に append
0x2f1 6731340a get "14" ; 0x2f2 ; "14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01"
0x2f5 67370a get "7" ; 0x2f6
0x2f8 85 tuple1
// index を指定して memo12 から値を取得
// i = 0
0x2f9 52 reduce
0x2fa 7031360a put "16" ; 0x2fb
0x2fe 30 pop
# 直前の暗号化処理を行った値を次の XOR キーに指定
0x2ff 67320a get "2" ; 0x300
0x302 67330a get "3" ; 0x303
0x305 67340a get "4" ; 0x306
0x308 67350a get "5" ; 0x309
0x30b 67330a get "3" ; 0x30c
0x30e 67370a get "7" ; 0x30f
0x311 4d0100 binint2 0x1
0x314 86 tuple2
0x315 52 reduce
0x316 70370a put "7" ; 0x317
0x319 4d0800 binint2 0x8
0x31c 86 tuple2
0x31d 52 reduce
0x31e 4d8300 binint2 0x83
0x321 86 tuple2
0x322 52 reduce
0x323 4da702 binint2 0x2a7
0x326 86 tuple2
0x327 52 reduce
0x328 85 tuple1
0x329 52 reduce
0x32a 30 pop
0x32b 67300a get "0" ; 0x32c
0x32e 6731320a get "12" ; 0x32f
0x332 8c065f5f6571. short_binunicode "__eq__" ; 0x334 ; "__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x33a 86 tuple2
0x33b 52 reduce
0x33c 28 mark
0x33d 493832313533. int "8215359690687096682" ; 0x33e ; "8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x352 493138363236. int "1862662588367509514" ; 0x353 ; "1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x367 493833353037. int "8350772864914849965" ; 0x368 ; "8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x37c 493131363136. int "11616510986494699232" ; 0x37d ; "11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x392 493337313136. int "3711648467207374797" ; 0x393 ; "3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x3a7 493937323231. int "9722127090168848805" ; 0x3a8 ; "9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R."
0x3bc 493136373830. int "16780197523811627561" ; 0x3bd ; "16780197523811627561\nI18138828537077112905\nl\x85R."
0x3d2 493138313338. int "18138828537077112905" ; 0x3d3
0x3e8 6c list
0x3e9 85 tuple1
0x3ea 52 reduce
0x3eb 2e stop
pickle によるデシリアライズは VM として動作します。
基本操作は、「値をスタックに追加する」、「スタックの 1 つもしくは 2 つの値をタプル化する」、「スタックの 2 つの値(呼び出し可能タプルと引数タプル)を使用してコード実行」、「スタック上の値をメモに保存」の 4 つのみです。
解析時のメモにコメントしている通り、上から愚直に「スタックにデータを追加 → タプル化 → 関数呼び出し」のような処理を順に繰り返していくと、最終的に以下のような処理を行っていることを特定できました。
- 64 文字の Flag を 8 バイトごとに分割して little エンディアンで int 化して配列に格納
-
int 化した 8 つの値を以下の式で加工する。(この時、最初の key は 1244422970072434993 であるが、それ以降の key は直前に暗号化した結果が格納される)
pow((xor(arr1[i], key), 65537, 18446744073709551557))
- 正しい Flag を入力した場合は、演算結果がハードコードされた long 値と一致する
以上の結果から、ハードコードされた 8 つの long 値から平文を復号できれば Flag を取得することができそうです。
ここで問題になるのは、秘密鍵の特定方法でした。
暗号化の処理から、明らかに RSA 暗号を使用していると判断できます。
しかし、秘密鍵は与えられておらず、n にあたる 18446744073709551557 も素数の積ではないため p,q の特定には至りませんでした。
チームメンバーに相談したところ、pow(e, -1, n)
の値が秘密鍵として復号に使えないかというドンピシャなアドバイスをもらいました。
このアイデアを元に以下の Solver を作成した結果、暗号文を復号して正しい Flag を取得することができました。
from Crypto.Util.number import long_to_bytes
ans = [
8215359690687096682,
1862662588367509514,
8350772864914849965,
11616510986494699232,
3711648467207374797,
9722127090168848805,
16780197523811627561,
18138828537077112905
]
key = 1244422970072434993
e = 65537
n = 18446744073709551557
d = pow(65537, -1, 18446744073709551556)
flag = []
for i in range(1, 8):
m = pow(ans[i], d, n)
flag.append((m ^ ans[i-1]).to_bytes(8,'little'))
# SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
ちなみに、他の方の Writeup を読んだところ、flickling というツールを使うと、以下のようなデコンパイル結果を得られるようでした。
こちらの方が圧倒的に読みやすく、自分の苦労がなんだったのかという気持ちになりました。
_var0 = input('FLAG> ')
_var1 = getattr(_var0, 'encode')
_var2 = _var1()
_var3 = getattr(dict, 'get')
_var4 = globals()
_var5 = _var3(_var4, 'f')
_var6 = getattr(_var5, 'seek')
_var7 = getattr(int, '__add__')
_var8 = getattr(int, '__mul__')
_var9 = getattr(int, '__eq__')
_var10 = len(_var2)
_var11 = _var9(_var10, 64)
_var12 = _var7(_var11, 261)
_var13 = _var6(_var12)
_var14 = getattr(_var2, '__getitem__')
_var15 = _var14(0)
_var16 = getattr(_var15, '__le__')
_var17 = _var16(127)
_var18 = _var7(_var17, 330)
_var19 = _var6(_var18)
_var20 = _var7(0, 1)
_var21 = _var9(_var20, 64)
_var22 = _var8(_var21, 85)
_var23 = _var7(_var22, 290)
_var24 = _var6(_var23)
_var25 = getattr([], 'append')
_var26 = getattr([], '__getitem__')
_var27 = getattr(int, 'from_bytes')
_var28 = _var8(0, 8)
_var29 = _var7(0, 1)
_var30 = _var8(_var29, 8)
_var31 = slice(_var28, _var30)
_var32 = _var14(_var31)
_var33 = _var27(_var32, 'little')
_var34 = _var25(_var33)
_var35 = _var7(0, 1)
_var36 = _var9(_var35, 8)
_var37 = _var8(_var36, 119)
_var38 = _var7(_var37, 457)
_var39 = _var6(_var38)
_var40 = getattr([], 'append')
_var41 = getattr([], '__getitem__')
_var42 = getattr(int, '__xor__')
_var43 = _var26(0)
_var44 = _var42(_var43, 1244422970072434993)
_var45 = pow(_var44, 65537, 18446744073709551557)
_var46 = _var40(_var45)
_var47 = _var41(0)
_var48 = _var7(0, 1)
_var49 = _var9(_var48, 8)
_var50 = _var8(_var49, 131)
_var51 = _var7(_var50, 679)
_var52 = _var6(_var51)
_var53 = getattr([], '__eq__')
_var54 = _var53([8215359690687096682, 1862662588367509514, 8350772864914849965, 11616510986494699232, 3711648467207374797, 9722127090168848805, 16780197523811627561, 18138828537077112905])
result0 = _var54
参考:SECCON CTF 2023 Quals Writeup - Qiita
とはいえ難解なアセンブリを読み解いたことで少しレベルアップしたような気はするので結果オーライです。
まとめ
国内 35 位、まだまだ本選への道のりは遠いですが引き続き精進していきたいです。
今回 Rev 問を全完していれば本選出場も狙える得点を得られていたようですので、来年は Rev を全完できるように頑張っていこうと思います笑