CTFするぞ

あたまよくないけどがんばります

yoshi-camp 2020 winter 開催記

はじめに

yoshikingが絶対セキュリティキャンプに落とされる*1ことを受け、「yoshikingのための、セキュリティキャンプより高品質*2なcamp」として定期的に開催しているyoshi-campの第三回目が開催されました。 特に明示はしてませんが、個人的には

  • 調べても理解不能なことをテーマに
  • 1人2時間以上のハンズオンで
  • ゆるくわいわい勉強する

という趣旨でyoshi-campをやっています。 暗号担当が多いのでここ二回は暗号中心ですが、私はrev, pwn系を教えています。

第一回:

ptr-yudai.hatenablog.com

第二回:

ptr-yudai.hatenablog.com

第一部 Z3を完全に理解しよう by ptr-yudai

私の講義なので特に書きませんが、演習中に判明したこととして

  • 累乗はa*aでなくa**2のように書いた方が速い(というか解けなくなるパターンがある)
  • 成立する条件の個数に制約を付けたい場合はIntを使うと賢い
  • 有理数の制約はRealsから解にis_rationalを適用した方が速い場合がある

などがあるのでそこだけメモ。

講義内容は

  • Z3の使い方(Bool, Int, Real, BitVec, Enum, 制約, qf論理式)
  • Z3の内部での「値」の扱い(Application, Quantifier, Bounded/Free Variable)
  • 量子記号の使い方(公理の成立・不成立の証明)
  • 最適化(Optimizer)
  • 推論を利用したデバッグ(unsat core)
  • 戦略(Goal, Tactic)
  • Z3利用ミス集(BitVecの闇、制約の付け方による実行速度の変化)

でした。完全に理解してもらえたと思います。

第二部 BKZで勝つ by mitsu

LLLで解けない問題の中にBKZというアルゴリズムで解けるものがあるらしく、それの理論をお勉強しました。 講義中にはあまり勝った気がしなかったので復讐します。

LLLの復習

LLLと言えばもはやpwn担当の私でも知っているレベルで一般家庭に普及していますが、解ける条件とかは覚えてなかったので復習です。

LLLでは基底を簡約したいのですが、その前にサイズ簡約とはn次元格子Lの基底のグラムシュミット係数μi,j

|μi,j|12

を満たすことを指します。さらにその上で、Lovaszの条件k2,,nに対してδ||bk1||2||πk1(bk)||2 を満たすとき、Lの基底はδ-LLL簡約されています。(δは簡約パラメータでπは直交補空間への直交射影。)

LLL簡約で見つかるベクトルの大きさは

||b1||2(44δ1)n12λ1(L)

みたいな条件だそうです。(λは逐次最小。)覚える気は無いけど使う前に考えるようにします。

LLLのアルゴリズムですが、格子から基底を1つずつ取ってきて、次元を増やしながら簡約していっています。 部分格子をサイズ簡約してLovaszの条件を満たしていれば次元を増やし、満たしていなければ1つ前の基底と交換して次元を下げる、という方法を取っているそうです。 中身とか考えたこともなかった。

HKZ(Hermite-Korkine-Zolotareff)簡約

これも聞いたことがありませんでした。 気持ちとしては、最短な非自明ベクトルを取っていく簡約手法です。 HZK簡約すれば逐次最小のベクトルも高い精度で近似できますが、最短なベクトルが取れるという前提がアレなので実用性には欠けそうです。

格子の基底がサイズ簡約されており、かつすべての1inに対して||bi||=λ(πi(L))であるとき、HKZ簡約されていると言います。 構成法は非常に単純です。

  1. L上の最短ベクトルb1を取る
  2. π2(L)上の最短ベクトルb2を取り、π2(b2)=b2となるようなb2Lを取る。
  3. 以下同様

HKZ簡約したとき、逐次最小のベクトルを次のような高い精度で近似できるそうです。

4k+3λk(L)2||bk||2k+34λk(L)2

逐次最小の数え上げ

HKZ簡約はそもそも最短ベクトルを求める問題を含んでおり、そのアルゴリズムを考える必要があります。 これにはFincke-PohstやKannanのアルゴリズムと呼ばれる手法があるそうですが、講義中には「逐次最小の数え上げと木探索を利用したアルゴリズム」を利用しました。

まずは問題を簡単に定式化します。 ある数え上げ上界列R1R2Rnについて

||πk(v)||2Rn+1k2

を満たす格子ベクトルvの係数ベクトルを求めたいです。

証明はここには書きませんが、結論として

(vk+i=k+1nμi,jvi)2Rnk+12j=k+1n(vj+i=j+1nμi,jvi)2||bj||2||bk||2

が成り立つようにk=nから順に決定していきます。 (不等式を満たすvn1が取れないときはk=nからやり直す。悲しいね。)

講義中に書く時間はなかったので、mitsu隊長が用意してくれたコードでベクトルを見つけ、sageのLLLと戦わせました。 私以外は割とLLLと同じかそれよりも良い数え上げ上界列を見つけていました。 どうして。

BKZ簡約

本題のBKZ簡約です。 気持ちはHKZ簡約の近似みたいな手法だと捉えています。

いつもどおり基底がサイズ簡約されていて、かつβ次元の格子Lk,k+β1の基底πk(bk),,πk(bk+β1)がHKZ簡約されているとき、BKZ簡約されていると言います。

β=nのときはBKZもHKZも同じですね。 ここで(ブロック)基底が直交補空間への射影であることを考えると、HKZ簡約する次元が低いので、単なるHKZ簡約に比べて計算量が減ることが分かります。

BKZ簡約では次の不等式が成り立つそうです。

||b1||γβn1β1λ1(L)

ここでγβはHermiteの定数と呼ばれ、大きいほどよい精度で近似できます。

BKZのアルゴリズムでは、まず入力の基底をLLL簡約し*3、部分格子のサイズhnに初期化します。 k=1から始めて各ループで部分射影格子Lk,k+β1の最短な非零ベクトルvを求め、||πk(v)||||bk||2が成り立っていれば次元を下げてLLLします*4。成り立っていなければカウンタをリセットして次数hの部分格子に対してLLL簡約する、という流れです。

よく分からんけど部分格子に対して最短ベクトルを求めていく感じですね。

最後にBKZが必要なknapsack問題が渡されるので解きました。 解きましたと言ってもBKZはかなり時間がかかるので講義中には解けてません :sob: 注意点としては、格子の並びによって解が変わるので基底をシャッフルしてゴリ押しました。

第三部 LLLで殴る(2) by theoremoon

theoremoonからは第二回に引き続きLLLの勉強です。 前回は「LLLとは」から教えてくれましたが、流石に今回は全員LLL使える前提でした。 普段使ってないので半分くらい忘れてたけど、まぁ第二部で若干思い出したのでOKOK。 *5

Coppersmithの定理

もはやCTFerなら誰もが一度は使った経験のあるCoppersmithの定理ですが、この講義では実装させられました。 事前課題で選択して解いたCoppersmithの定理を使う過去問(私はLayer7 CTF 2020のChild Coppersmithを選びました)を、自作small_rootで解けるようになればクリアです。

Coppersmithの定理のお気持ちとしては、modular多項式を整数係数多項式に変換して解こうという手法です。 つまり、f(x)0modNの解x0を用いた等式f(x0)kN=0で、k=0になればOKです。 これを満たすためには多項式の係数が小さい必要があります。理由は2つ。

  1. f(xX)の係数が小さければ、f(x0)0modNは整数係数多項式上でも満たされる →Howgrave-Graham's lemmaがこれを言っていて、条件は||f(xX)||<nω
  2. f(xX)の係数が小さければ、f(x)p(x)で割り切れない。(pはこの辺を参照) →条件は||f(xX)||<2ωXkW(この辺怪しいので間違ってるかも)

とにかくfを、根はそのままで小さい係数を持つように変形したいという話です。

ここで多項式は線形結合の形で表せるので、LLLを使って簡約すれば同じ格子上なので根はそのままで係数だけ小さくなります。 嬉しい。

ところが今回は方程式が1つしか与えられていません。 そこで、適当な変形で方程式の数をかさ増しする必要があります。 ずるい。

方程式の増やし方ですが、あくまで独立にする必要があるのでmodNmの世界に持ち上げて考えます。 f(x0)=kNの両辺をi乗すればfi=kiNiなので、Nmixiを掛ければ根はそのままです。 つまり、

gi,j(x)=Nmixifi

とすればgi,j(x0)0modNmですね。 また、fmNmを含むので、これにxtを掛けても方程式を増やせます。

したがって、次のような格子をLLL簡約すれば多項式の係数を小さくできます。

f:id:ptr-yudai:20201228151427p:plain

得られた係数ベクトルがHowgrave-Graham's lemmaを満たしているかを調べ、満たしていれば整数方程式を解けば良いです。

ここまで原理が分かれば実装できます。(実際にはLLL周りでバグらせて助けてもらった。) 可能性のある解を全部求められるgeneratorにしてみました。

def my_small_roots(f, X, m, t):
"""
My first small_roots implementation
Args:
f: Polynomial
X: Maximum value of x
m: Extended degree
t: Magic parameter (f^m, xf^m to x^(t-1)f^(m-1))
"""
N = f.parent().modulus()
d = f.degree()
f = f.change_ring(ZZ)
x = f.parent().gen()
"""
Step 1. 方程式をかさ増しする
"""
g = {}
for j in range(d):
for i in range(m):
g_ij = N^(m-i) * (x*X)^j * f(x*X)^i
g[j*m + i] = list(g_ij)
for i in range(t):
h_i = (x*X)^i * f(x*X)^m
g[d*m + i] = list(h_i)
"""
Step 2. 格子を錬成する
"""
B = Matrix(ZZ, d*m+t, d*m+t)
for i in range(d*m+t):
for j in range(d*m+t):
if j < len(g[i]):
B[i,j] = g[i][j]
"""
Step 3. LLLで小さい多項式係数を求める
"""
known = set()
l = B.LLL()
for row in l:
if row.is_zero():
continue
# Howgrave-Graham's theorem
if RR(row.norm()) >= RR(N^m / sqrt(len(row))):
continue
"""
Step 4. 方程式を戻す
"""
poly = sum([ZZ(k / X^i)*x^i for i,k in enumerate(row)])
roots = poly.roots()
if roots:
for root in roots:
if root[0] not in known:
known.add(root[0])
yield root[0]

これを使ってChild Coppersmithを解いてみます。

with open("enc.txt", "r") as f:
N, e, c = eval(f.readline())
hint = eval(f.readline())
low_size = hint.bit_length()
kbits = 512 * 2 - low_size
PR.<x> = PolynomialRing(Zmod(N), implementation='NTL')
f = x*2^low_size + hint
f = f.monic()
set_verbose(2)
for root in my_small_roots(f, 2^kbits, m=13, t=39):
pq = root*2^low_size + hint
p = N / pq
q = pq / p
piN = p * (p-1) * (q-1)
d = inverse_mod(e, piN)
print(bytes.fromhex(hex(pow(c, d, N))[2:]))

やったぁ。

verbose 1 (7: child_coppersmith.sage.py, my_small_roots) LLL of 52x52 matrix (algorithm fpLLL:wrapper)
verbose 1 (7: child_coppersmith.sage.py, my_small_roots) LLL finished (time = 39.072761)
b'LAYER7{Now_you_can_say_you_are_child!}'

実装は思ったより単純でしたが、ちゃんと解けたので感動しちゃった。

LCG

線形合同法の問題。 線形合同法というとたまにCTFで出てきますし、私も6個くらい出力があれば係数が全部求まることを知っていました。 しかし、今回は疑似乱数生成器の出力の一部分だけが見られるという、極めて現実的な状況を想定しています。

演習問題では64-bitの素数を法とするLCGがあり、その出力の上位32-bitが10個渡されました。(10個というのは適当で特に理由は無いらしい。)

LCGは次のような漸化式で考えられます。

xn=axn1+bmodm

これを解くと、次のように表せます。

xn=anx0+i=0n1aibmodm

以降右辺第二項をf(n)と置きます。 上の数列より次のような関係が成り立つことが分かります。

f:id:ptr-yudai:20201228151458p:plain

ベクトルをx、行列をLと置くと、Lx=0modmですね。 ここでLをLLL簡約してBにすると、やはりBx=0modmです。

一方、x=2kyf+zです。(ffの列ベクトル、zは今回未知の32ビットの値のベクトルです。)

したがって、ある整数列ベクトルlが存在して、

B(2kyf+z)=ml

を満たすことが分かります。変形すると

Bz=mlB(2kyf)

ですが、今zBも小さいのでBz0に近似します。(は?)

するとlはだいたいm1B(2kyf)となります。

ここまでに分かった値を使えば、

z=B1(mlB(2kyf))

が分かりますね。

実際に実装すると、ちゃんとLCGのパラメータを求められました。

LWE

時間が無かったのでかなり省略された感じ。 暗号担当の人々がbabaiとかなんたらtheoremとかCVPとか意味分からん単語で会話してて正直何も分からなかったです。

幻の第四部 PRNGと戯れる

yoshikingが講義資料作れなかったので戯れられませんでした。 @yoshiking youtubeに講義アップロードして。

おわりに

弊チーム、暗号担当多すぎでは? pwn, rev担当2人ずつくらい欲しいから転生して。

*1:日本語ができないので課題に答えられない

*2:当社比

*3:これは効率よく最短ベクトルを求めるための前処理らしい

*4:一次従属になる可能性があるのでMLLLを使うらしい

*5:余談:ワドルドゥ隊長は中国語で【瓦豆魯篤(wǎdòulǔdǔ)】と書くことも学びました。