--- tags: crypto, ctf --- # Crypto CTF 2020 - Amsterdam * 会場(今も鯖が生きている): <https://cryp.toc.tf/> * これまでに解いた問題: https://hackmd.io/@Xornet/BkemeSAhU ## Writeup ### Outline 秘密のパラメータn, kが与えられ、nをデクリメントしながら、kと組み合わせ(combination)を取ったものと平文を比べていく。 もし平文の方が大きかったら暗号化配列に1を追加し、平文からその組み合わせを引く、ついでにkもデクリメントする。そうでなかったら0を追加する(正確には元々0で埋め尽くされた配列があるので単に1に変えるか何も変えないかの違いである)。 この配列をビット列の2進数表記とみなして整数にする。これを2で割るのを繰り返しながら同時に4の剰余を取った余りで決定される3進数表記された暗号文を作る。 その暗号文が与えられるのでこの操作を逆にたどり、n, kを特定して暗号文作成プロセスを再現する。 ### Source 問題のソースコードは次の通り、そこまで長くないので貼る(若干コメントを加えている) ```python= #!/usr/bin/env python3 from Crypto.Util.number import * from functools import reduce import operator from secret import flag, n, k # combination (n C k) def comb(n, k): if k > n : return 0 k = min(k, n - k) u = reduce(operator.mul, range(n, n - k, -1), 1) # n! // (n-k)!! d = reduce(operator.mul, range(1, k + 1), 1) # k! return u // d def encrypt(msg, n, k): msg = bytes_to_long(msg.encode('utf-8')) if msg >= comb(n, k): return -1 m = ['1'] + ['0' for i in range(n - 1)] for i in range(1, n + 1): if msg >= comb(n - i, k): m[i-1]= '1' msg -= comb(n - i, k) k -= 1 m = int(''.join(m), 2) i, z = 0, [0 for i in range(n - 1)] c = 0 while (m > 0): if m % 4 == 1: c += 3 ** i m -= 1 elif m % 4 == 3: c += 2 * 3 ** i m += 1 m //= 2 i += 1 return c enc = encrypt(flag, n, k) print('enc =', enc) ``` (tab文字でインデントしてるせいで`comb`関数コピペしたら動かなくて困った) `comb`関数から見ていく。こいつは`n! // (n-k)!! // k!`を計算して返す、つまり ${}_n \mathrm{C} _k$ を返す。 続いて`encrypt`関数内だが、秘密になっている`n, k`のcombinationを取っている箇所が多い。ひとまず長さがnで先頭以外全部0の配列`m`が21行目で作られている。 続いて`range(1, n+1)`の範囲で`i`を回し、もし`msg >= comb(n - i, k)`であれば`m[i-1]`を`"1"`に変更する。そして`msg`から`comb(n-i, k)`を引いている。 これはビットの配列なのでくっつけて2進表記とみなして`m`に再代入している。 ここまでが暗号化の第1パートで次が第2パートになる。 続いて`m`を4で割った余りによって対応する操作が`m`が正の間だけ行われる。 もし余りが1なら暗号文(初期値0)に`3**i`を足している。なお`i`はwhileループの回数(初期値0)である。ついでに`m`をデクリメントしている。 余りが3なら暗号文に`2*3**i`を足している。ついでに`m`をインクリメントしている。 これらの場合もそうでない場合も`m`はかならず2で割られる。 こうして足された暗号文が最終的に出力される。 では、この手順を逆に辿って平文を導出することを目指す。 ### `m`の逆算 暗号文`c`の逆算は`m`の剰余によって決まった。ここのコードを見ると`x*3**i`が`c`に足されており、`x`は次のようになっている(疑似matchコード)。 ``` x = match (m % 4) { 1 => 1, 3 => 2, _ => 0, } ``` `i`はループ毎に1増えるため、`c`の最終結果を3進数で展開すれば各ループで`x`が何であったかが判明する。 また、同じく`m`を4で割ったときの余りに対応して`m`も変化するため、導出した`x`を逆にたどれば`m`の逆算も出来る。 また`m`は元々ビットの配列だったので、2進数表現に直せばそちらも手に入る。以後はこの配列を`m`とする。 ### `n, k`の特定 次の復号手順を経る為に`m`から`n, k`を特定する。`n`は`m`の要素数なので簡単に求めることが出来る。ということで`k`を求めるだけだが、その前に`comb`の性質を幾つか挙げる。 1. `comb(n, n) = 1` 2. `comb(n, k) = 0 (n < k)` 3. `comb(n, k) = 1 (k <= 0)` いずれも`comb`の定義から導出出来る。 ここで上の手順を経た`m`を表示すると次のようになる。 ``` ['1', '0', '0', ... (snip) '1', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0'] ``` 末尾に`"0"`が続いていることがわかる。暗号化時に`m`を生成する時、`"1"`になる条件は`msg >= comb(n - i, k)`であった。ここで`i`は`range(1, n+1)`のイテレータで回っている事を鑑みると、最後は`i = n`になる。ということは`msg >= comb(0, k)`になる。 `m`の最後は`"0"`なのでこの条件は`False`であることから`msg < comb(0, k)`になる。 さて、`msg`が減るのは先程の条件だったが、`msg >= comb(n, k)`時に`msg -= comb(n, k)`となるので`msg`が負になることはあり得ない。よって最後のループでは`0 <= msg < comb(0, k)`が成立する。 この不等式と先程挙げた3つの性質のうち、2より`k > 0`であることはあり得ない。よって`k <= 0`となる。性質3より`0 <= msg < 1`が成り立つので`msg = 0`である。 `msg`が減算される時の条件に戻るとこの時`m[i-1] = "1"`になるので`msg`がどこで0になったかが判明する(ついでに`k`もデクリメントされる)。 ここで`k`が最終的に負になるかどうかを考える。もし、負になる場合、`msg -= comb(n-i, k)`は`msg -= 1`となる。そして次のような表が得られる。 |`k`|`comb(n-i, k)`|constraints| ----|----|----| |1|`n-i`|`msg >= n-i`| |0|1|`msg >= 1`| |-1|1|`msg >= 1`| |...||| もし`k=1`の時の`msg`の減算で`msg = 0`とならなければ、その後0になるまで1が引かれ続ける事になる。したがって、`m[i-1] = "1"`の操作が2回以上連続して終わるはずである。 ところが`m`の終端を見るとそんなことは無い、よって`k=1`で無事に`msg = 0`になったことになる(同時に`k`も減算されて0になる)。 これ以後、`msg >= comb(n-i, k)`の条件は`0 >= 1`となるので常に偽である。すなわち`k, msg`は0のままだし、`msg[i-1]`も`"0"`から更新されない。 ということで`m`中の`"1"`の数を数えればそれが`k`に一致する...と言いたいところなのだが、最初の`"1"`に関しては元からそうなので`msg, k`の減算が発生したのかどうかはわからない。 冒頭の`msg >= comb(n, k)`時に`return -1`というコードに到達していないことと、それ以降もしばらく`msg >= comb(n-i, k)`を満たしていないことから`msg >= comb(n-1, k)`も満たさないだろうという仮定をして`"1"`の数より1小さい量を`k`とした。 `n, k`の両者を求めた(正確には仮定だが)のであとはこれを逆向きに辿って`msg`を復元する。`msg >= comb(n-i, k)`なら`m[i] = "1"`をして`msg -= comb(n-i, k)`もするので`m[i] = "1"`だったら平文(初期値0)に`comb(n-i, k)`を足すだけである。 これを`long_to_bytes`でバイト列にしたらフラグを含んだものが表示された。 ## Code ```python= from Crypto.Util.number import * from functools import reduce import operator from xlog import XLog logger = XLog("EXPLOIT") # combination (n C k) def comb(n, k): if k > n: return 0 k = min(k, n - k) u = reduce(operator.mul, range(n, n - k, -1), 1) # n! // (n-k)!! d = reduce(operator.mul, range(1, k + 1), 1) # k! return u // d def recalc_c(m): c = 0 i = 0 while m > 0: if m % 4 == 1: c += 3 ** i m -= 1 elif m % 4 == 3: c += 2 * 3 ** i m += 1 m //= 2 i += 1 return c if __name__ == '__main__': enc = 5550332817876280162274999855997378479609235817133438293571677699650886802393479724923012712512679874728166741238894341948016359931375508700911359897203801700186950730629587624939700035031277025534500760060328480444149259318830785583493 enc_org = enc # mの逆算 cs = [] while enc > 0: cs.append(enc % 3) enc //= 3 s = 0 for i, c in enumerate(cs): s += (3**i) * c assert s == enc_org m = 0 for c in reversed(cs): m *= 2 if c == 1: m += 1 elif c== 2: m -= 1 assert recalc_c(m) == enc_org m = list(bin(m)[2:]) n = len(m) k = sum(x == "1" for x in m) - 1 print(m) print(n) print(k) msg = 0 for i in range(1, n+1): if m[i-1] == "1" and i != 1: msg += comb(n-i, k) k -= 1 print(long_to_bytes(msg)) ``` ## Flag 当日は出ていませんが、問題サーバーもバリデータも生きていたのでやりました `CCTF{With_Re3p3ct_for_Sch4lkwijk_dec3nt_Encoding!}` スクリプトを走らせた結果はこちら ```python= $ xplt ['1', '0', '0', '0', '0', '0', '1', '0', '1', '0', '0', '0', '0', '0', '1', '1', '1', '1', '0', '1', '1', '1', '1', '0', '0', '0', '0', '0', '1', '1', '0', '0', '1', '0', '1', '1', '0', '0', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '0', '1', '1', '1', '1', '0', '0', '1', '1', '1', '0', '1', '0', '1', '1', '1', '1', '1', '0', '1', '0', '0', '1', '1', '0', '1', '0', '0', '0', '0', '0', '1', '1', '1', '0', '1', '0', '1', '1', '1', '0', '0', '0', '1', '0', '1', '1', '0', '0', '0', '0', '0', '0', '1', '0', '1', '1', '1', '0', '1', '0', '0', '1', '0', '0', '1', '1', '0', '0', '0', '0', '1', '1', '0', '1', '0', '0', '0', '1', '1', '1', '1', '1', '0', '1', '0', '0', '0', '0', '0', '1', '1', '0', '1', '0', '1', '0', '1', '1', '1', '1', '1', '0', '0', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0', '0', '0', '1', '1', '0', '0', '0', '0', '0', '1', '0', '0', '0', '1', '1', '0', '0', '1', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '0', '1', '1', '0', '1', '1', '1', '1', '1', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '0', '1', '0', '0', '1', '1', '0', '0', '0', '0', '1', '1', '1', '0', '0', '0', '0', '1', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '1', '1', '0', '0', '0', '0', '1', '0', '0', '1', '1', '1', '1', '0', '0', '1', '0', '0', '0', '0', '1', '0', '0', '0', '1', '0', '1', '1', '0', '0', '0', '0', '1', '0', '1', '1', '1', '1', '1', '1', '0', '1', '0', '0', '1', '0', '1', '0', '1', '0', '1', '0', '1', '0', '0', '0', '0', '0', '1', '1', '0', '1', '0', '1', '0', '1', '0', '0', '1', '0', '0', '1', '0', '0', '0', '0', '1', '1', '0', '1', '0', '1', '0', '0', '0', '1', '0', '0', '0', '0', '0', '1', '1', '1', '0', '1', '0', '1', '1', '1', '0', '0', '0', '0', '0', '1', '1', '0', '1', '1', '0', '1', '1', '1', '1', '0', '1', '1', '1', '0', '0', '1', '0', '0', '1', '1', '0', '0', '0', '0', '1', '0', '1', '1', '1', '0', '1', '0', '0', '1', '0', '1', '1', '1', '0', '0', '0', '1', '1', '0', '1', '1', '0', '0', '1', '1', '1', '0', '1', '0', '0', '1', '0', '0', '0', '0', '0', '0', '1', '1', '0', '0', '0', '1', '0', '0', '0', '0', '0', '0', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', '1', '1', '0', '0', '0', '0', '0', '0', '0', '1', '1', '0', '0', '0', '0', '1', '0', '1', '0', '1', '1', '1', '1', '0', '0', '1', '0', '1', '1', '1', '1', '1', '1', '1', '1', '0', '0', '0', '0', '1', '0', '0', '0', '0', '1', '0', '0', '0', '0', '0'] 493 217 b'..:: CCTF{With_Re3p3ct_for_Sch4lkwijk_dec3nt_Encoding!} ::..' ``` ## 感想 `k`は負にならないだろうというGuessをして無事に解けましたがせっかくなので証明を与えました、この証明が1番大変でしたが面白かったです。 あと最後の`n-i`を`n-1`とTypoして無限に時間を溶かした(30分ぐらい)上にこのTypoはよくやるので気をつけたいです。