All Articles

SECCON CTF 2023 QUALS Writeup

9/16 から 17 にかけて開催されていた SECCON CTF 2023 の予選に 0nePadding として参加しました。

個人では Rev の問題を 2 問解き、最終順位は国内 35 位、全体で 87 位でした。

image-20230917221935162

SECCON の国内本選の参加ラインである国内順位での上位 10 チームまではまだまだ壁が厚いですが、引き続き精進していこうと思います。

解いてない問題の復習には時間がかかりそうなので、とりあえず解けた問題の Writeup を書きます。

もくじ

jumpout(Rev)

Sequential execution

今回の Rev の Warmup 問です。

Flag 取得の難易度的には簡単でしたが、Writer の方がバイナリやデバッガに精通しているんだろうなということを強く感じる非常に興味深い問題でした。(この問題どうやって作ってるのか、ソースコードがあれば見たい、、、)

バイナリ自体は入力値として受け取った Flag を検証するだけのシンプルなプログラムのようでしたので、Ghidra で解析していきます。

いつも通りバイナリを entry から解析し、main 関数を特定したところ、FLAG: というテキストを用いて何かしらの処理を呼び出しているようですが、Listing ウインドウの結果が壊れているせいか上手く処理をデコンパイルできていません。

image-20230917223341237

ただし、main 関数で参照している 0x1011d0 の関数を参照すると、上手くデコンパイルできてはいないものの、スタック内の(おそらく)入力文字を check 関数に与えていることがわかります。(check 関数は自分で rename した関数)

image-20230917224759189

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 の入力を受け取って検証する処理が実行されます。

参考:OpCodes · Pickle.jl

参考: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 までしか読み取ることができません。

image-20230917233959543

しかも、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 つのみです。

解析時のメモにコメントしている通り、上から愚直に「スタックにデータを追加 → タプル化 → 関数呼び出し」のような処理を順に繰り返していくと、最終的に以下のような処理を行っていることを特定できました。

  1. 64 文字の Flag を 8 バイトごとに分割して little エンディアンで int 化して配列に格納
  2. int 化した 8 つの値を以下の式で加工する。(この時、最初の key は 1244422970072434993 であるが、それ以降の key は直前に暗号化した結果が格納される)

    pow((xor(arr1[i], key), 65537, 18446744073709551557))

  3. 正しい 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 を全完できるように頑張っていこうと思います笑