(Last modified: )

SECCON 2020 Online CTF - Fixer writeup

この記事は、2020年10月11日に旧ブログに投稿したものです。

SECCON 2020 Online CTFにて出題されたFixerのwriteupです。

渡されるファイルはpycです。pycは元のコードに復元できるため、pycdcを用いて復元してみます。

1$ pycdc fixer.25f1fe08e6e0434bd5ae604e963f4b80.cpython-39.pyc
2# Source Generated with Decompyle++
3# File: fixer.25f1fe08e6e0434bd5ae604e963f4b80.cpython-39.pyc (Python 3.9)
4
5Unsupported opcode: LOAD_METHOD
6import re
7s = input()
8# WARNING: Decompyle incomplete

pycdcに対応していないopcodeが使われているため、逆コンパイルを行うことができないようです。 そこで、該当箇所は他のopcodeに置き換えることで、無理矢理逆コンパイルが通るようにします。

今回の場合、pycdcはLOAD_METHODとCALL_METHODに対応していなかったため、cpython/Include/opcode.hより、pycファイル中の0xA0と0xA1を0x65に置換しました。 これにより、pycdcで逆コンパイルを行うことができました。

 1$ pycdc fixer.modified.pyc
 2# Source Generated with Decompyle++
 3# File: fixer.modified.pyc (Python 3.9)
 4
 5import re
 6s = input()
 7m = s
 8if not m:
 9    print('invalid flag')
10else:
11    s = input
12    
13    f = lambda s: (lambda a: (lambda b = None: a == b)
14)(0x1F8DD85698FB84CC77D5D5046A176F6B51A9531952D4409D133FF48B68FL)((lambda a: None((lambda b = None: None((lambda c = None: b(b)(c)))
15))
16)((lambda f: (lambda b = None: (lambda c = None: (lambda d = None: if len(c) == 0:
17dNone(f(b)(c[1:])(d))(c[0]))
18)
19)
20))((lambda a: (lambda b = None: a * (lambda a: None((lambda b = None: None((lambda c = None: b(b)(c)))
21))
22)((lambda a: (lambda b = None: if b > 266:
23b - 10None(a(b + 11)))
24))(b) + b
25)
26))((lambda a: None((lambda b = None: None((lambda c = None: b(b)(c)))
27))
28)((lambda f: (lambda b = None: (lambda c = None: if len(c) == 0:
29[][
30None(ord(c[0]) - 65)] + f(b)(c[1:]))
31)
32))((lambda a: None((lambda b = None: None((lambda c = None: b(b)(c)))
33))
34)((lambda a: (lambda b = None: if b == 0:
351(None + 1) * a(b - 1) + 7 & 255)
36)))(s))(0))
37
38    if f(s):
39        print('correct')
40    else:
41        print('wrong')

インデントは適宜修正しつつ、disモジュールの出力結果が一致するように試行錯誤を重ねてコードを修正していきます。 はじめに、与えられたpycファイルをdisモジュールで逆アセンブリします。

1import dis, marshal
2
3with open("./fixer.25f1fe08e6e0434bd5ae604e963f4b80.cpython-39.pyc", "rb") as f:
4    code = marshal.load(f)
5dis.dis(code)

次に、適当な関数に逆コンパイルされたコードを入れて、それを逆アセンブリします。

1def func():
2    # 逆コンパイルされたソースコード
3
4dis.dis(func)

最後に、出力された逆アセンブリ結果が、与えられたpycの逆アセンブリ結果と同一の結果になるように、バイトコード命令一覧を参照しながらコードを書き換えていきます。

修正結果は次のようになります。

 1import re
 2s = input()
 3m = re.match('^SECCON{([A-Z]+)}$', s)
 4if not m:
 5    print('invalid flag')
 6else:
 7    s = m.group(1)
 8
 9f = lambda s: (lambda a: (lambda b: a==b))(13611142019359843741091679554812914051545792465993098606064046040462991)((lambda a: ((lambda b: a((lambda c: b(b)(c))))) \
10        ((lambda b: a((lambda c: b(b)(c)))))) \
11            ((lambda f: (lambda b: (lambda c: (lambda d: d if len(c) == 0 else b(f(b)(c[1:])(d))(c[0])))))) \
12                ((lambda a: (lambda b: a * (lambda a: ((lambda b: a((lambda c: b(b)(c)))))((lambda b: a((lambda c: b(b)(c))))))((lambda a: (lambda b: (b - 10) if b > 266 else a(a(b + 11)))))(b) + b))) \
13            ((lambda a: ((lambda b: a((lambda c: b(b)(c)))))((lambda b: a((lambda c: b(b)(c))))))((lambda f: (lambda b: (lambda c: [] if len(c) == 0 else [b(ord(c[0]) - 65)] + f(b)(c[1:]))))) \
14                ((lambda a: ((lambda b: a((lambda c: b(b)(c)))))((lambda b: a((lambda c: b(b)(c))))))((lambda a: (lambda b: 1 if b == 0 else (b + 1) * a(b - 1) + 7 & 255))))(s))(0))
15
16if f(s):
17    print('correct')
18else:
19    print('wrong')

難読化されていますが、以下の二点が分かります。

  • フラグは全て大文字
  • 入力値が13611142019359843741091679554812914051545792465993098606064046040462991に変換されれば良い

フローを掴むため、それぞれの無名関数に対して、順番に番号を付けていきます。 そして、無名関数が呼び出される度に、番号と引数の中身をprint関数で出力することにします。 なお、14〜17番目の無名関数は実行回数が多いため、print関数を省略しています。

1        (lambda a: print(0) or ((lambda b: print(1) or  a((lambda c: print(2) or  b(b)(c)))))((lambda b: print(3) or  a((lambda c: print(4) or  b(b)(c)))))) \
2            (lambda f: print(5) or  (lambda b: print(6) or  (lambda c: print(7, c) or (lambda d: print(8) or  d if len(c) == 0 else b(f(b)(c[1:])(d))(c[0]))))) \
3                ((lambda a: print(9) or  (lambda b: print(10, (lambda a: print(11) or  (lambda b: print(12) or  a((lambda c: print(13) or  b(b)(c))))((lambda b:  a(lambda c: b(b)(c))))) (lambda a: lambda b: (b - 10) if b > 266 else a(a(b + 11)))(b)) or a * (lambda a: print(11) or  (lambda b: print(12) or  a((lambda c: print(13) or  b(b)(c))))((lambda b:  a(lambda c: b(b)(c))))) (lambda a: lambda b: (b - 10) if b > 266 else a(a(b + 11)))(b) + b))) \
4            ((lambda a: print(18) or  ((lambda b: print(19) or  a((lambda c: print(20) or  b(b)(c)))))((lambda b: print(21) or  a((lambda c: print(22) or  b(b)(c))))))((lambda f: print(23) or  (lambda b: print(24) or  (lambda c: print(25) or  [] if len(c) == 0 else [b(ord(c[0]) - 65)] + f(b)(c[1:]))))) \
5                ((lambda a: print(26) or  ((lambda b: print(27) or  a((lambda c: print(28) or  b(b)(c)))))((lambda b: print(29) or  a((lambda c: print(30) or  b(b)(c))))))((lambda a: print(31) or  (lambda b: print(32) or  1 if b == 0 else (b + 1) * a(b - 1) + 7 & 255))))(s))(0)

上記コードで適当な入力値を与えた際の結果から、以下のフローで文字列が変換されていることが推測できます。

  1. 18〜32番目の無名関数で、入力した文字を対応表に基づいて変換される
  2. 2〜9番目の無名関数で、変換された文字列から一文字ずつ抽出される
  3. 9〜13番目の無名関数で、抽出された文字列に対して、何らかの変換処理が行われる

このフローを仮定すると、フラグを導出するためには、最初に換字式暗号の対応表を入手する必要がありそうです。 上記のコードを実行し、ABCDEFGHIJKLMNOPQRSTUVWXYZを入力してみます。 すると、7番目の無名関数の引数に、変換後と思われるリストが出力されています。

 1$ python3.9 hoge.py
 2ABCDEFGHIJKLMNOPQRSTUVWXYZ
 3
 422
 521
 623
 724
 825
 97 [1, 9, 34, 143, 210, 243, 172, 103, 166, 131, 168, 231, 194, 163, 148, 71, 190, 99, 96, 135, 26, 67, 12, 39, 214, 195]
102
113
125
136
14

これより、対応表を入手できました。

10番目の無名関数を見てみると、引数aと別の無名関数の返り値を乗算し、加算している箇所があります。このことから、aとbと無名関数の返り値は数値(またはシーケンス)であるはずです。

1(lambda b: a * (lambda a: ((lambda b: a((lambda c: b(b)(c)))))((lambda b: a((lambda c: b(b)(c))))))((lambda a: (lambda b: (b - 10) if b > 266 else a(a(b + 11)))))(b) + b)

この無名関数がキーポイントであると推測し、乗算される数であるaの値と、乗算する無名関数の値、そして加算するbの値をprint関数で見ていくことにします。

1(lambda b: print(a) or a * (lambda a: ((lambda b: a((lambda c: b(b)(c)))))((lambda b: a((lambda c: b(b)(c))))))((lambda a: (lambda b: (b - 10) if b > 266 else a(a(b + 11)))))(b) + b)
1(lambda b: print((lambda a: (lambda b: a((lambda c: b(b)(c))))((lambda b:  a(lambda c: b(b)(c))))) (lambda a: lambda b: (b - 10) if b > 266 else a(a(b + 11)))(b)) or a * (lambda a: (lambda b: a((lambda c: b(b)(c))))((lambda b:  a(lambda c: b(b)(c))))) (lambda a: lambda b: (b - 10) if b > 266 else a(a(b + 11)))(b) + b)
1(lambda b: print(b) or a * (lambda a: ((lambda b: a((lambda c: b(b)(c)))))((lambda b: a((lambda c: b(b)(c))))))((lambda a: (lambda b: (b - 10) if b > 266 else a(a(b + 11)))))(b) + b)

すると、aは文字数に応じて数値が増加しており、無名関数の方は常に257、そしてbは単一換字式暗号変換後の数値であることがわかります。

このことから、暗号文を257で除算し剰余すれば解けそうです。

bcを用いて解いていきます。

 1$ bc
 2bc 1.07.1
 3Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
 4This is free software with ABSOLUTELY NO WARRANTY.
 5For details type `warranty'. 
 6define func(number) {
 7   number % 257
 8   if (number/257 == 0) {
 9        return
10   }
11   return func(number/257)
12}
13func(13611142019359843741091679554812914051545792465993098606064046040462991)
14194
15214
1634
17131
18166
19231
20131
2134
22195
23210
24168
2599
26143
27163
28163
2912
30195
3126
32172
3396
34210
35195
36190
3796
38168
39168
4071
41168
42195
431
440

そして、対応表に沿って変換していきます。

1>>> ''.join(map(lambda x:"A B C D E F G H I J K L M N O P Q R S T U V W X Y Z".split(" ")[[1, 9, 34, 143, 210, 243, 172, 103, 166, 131, 168, 231, 194, 163, 148, 71, 190, 99, 96, 135, 26, 67, 12, 39, 214, 195].index(x)], map(int, "194 214 34 131 166 231 131 34 195 210 168 99 143 163 163 12 195 26 172 96 210 195 190 96 168 168 71 168 195 1".split(" "))))
2'MYCJILJCZEKRDNNWZUGSEZQSKKPKZA'

SECCON{MYCJILJCZEKRDNNWZUGSEZQSKKPKZA}

Powered by Hugo & Kiss.