Google CTF 2021 のCryptoの問題はどれも楽しかったです。着目すべき点はわかりやすいけど解ききるのは大変という印象でした。チームメイトのS3v3ru5と一緒に取り組んでチームとしてはCryptoは全部とけて非常によかったです。
[crypto] tiramisu
go製のecho serverで、ECDHによって共通鍵を生成して暗号化されたメッセージをやりとりします。暗号文にはHMACによる認証符号を付与していて、認証が通らない場合はセッションを確立できません。また、フラグはサーバの秘密鍵 d
から生成される鍵をつかってAESで暗号化されています。
脆弱性
ECDHの段階でサーバからは secp224r1 に準拠した鍵が送られてきます。また提供されるクライアントプログラムでもsecp224r1の鍵を生成してサーバに送っているのですが、プログラムやprotobuf*1をみるとどうやらsecp256r1の鍵を送ることもできるようでした。しかしプログラム上ではsecp224r1が送られてくることだけを想定しているらしく、次のようなコードがありました。
// Load peer key.ecnrypted peer, err := proto2ecdsaKey(clientHello.Key) if err != nil { return err } // Key sanity checks. if !peer.Curve.IsOnCurve(peer.X, peer.Y) { return fmt.Errorf("point (%X, %X) not on curve", peer.X, peer.Y) } // Compute shared secret. P := server.key.Params().P D := server.key.D.Bytes() sharedX, _ := server.key.ScalarMult(new(big.Int).Mod(peer.X, P), new(big.Int).Mod(peer.Y, P), D) masterSecret := make([]byte, server.key.Params().BitSize/8) sharedX.FillBytes(masterSecret)
注目するのは sharedX
を計算している部分で、ここではクライアント側が送信した公開鍵を倍して共通鍵を生成しようとしているのですが、この演算はsecp224r1上で行われています。しかしここにsecp256r1上の座標を送ることもできます。InvalidCurveAttackが行えそうですね。
InvalidCurveAttackというのはサーバがある点の演算を行うときにその点が楕円曲線上に存在するかをきちんと確認できていないときに行える攻撃で、楕円曲線上の加算・乗算ではワイエルシュトラス標準形でいうところのを用いないことから、サーバ側が正規の楕円曲線 上での演算をしているつもりでも上での計算をさせることができる、というものです。secp224r1は安全な楕円曲線なので位数の小さい点というものは存在しませんが、の値を操作してうまい点をつくると小さい位数の点での演算を行わせることができます。
dの特定
InvalidCurveAttackをうまく使うことができれば目的であったサーバの秘密鍵を求めることができそうです。典型的には曲線 上の小さい位数を持つ点を送り、を得て離散対数問題を解くことでを計算するのですが、今回は共通鍵の計算ということで計算後のsharedX
を直接得ることはできません。しかし小さい位数上ではは のどれかになりますから、これを全探索して通信が確立できるかをみることで、擬似的にブルートフォースアタックによって小さいインスタンスの離散対数問題が解けていることになります。要するに位数の点を送り、を全探索して送って、通信が確立できたらが成り立っていると言えます。
今、小さい位数について考えていましたが中国剰余定理をつかって異なるたくさんの剰余についてのを得ることでが復元できそうです
小さい位数の点の発見
では小さい位数を持つ点はどのように発見すればよいのでしょうか。これは簡単で、の値をランダムに探索して楕円曲線の位数が小さい素因数を含むかどうかを見れば良いです。 のように素因数分解ができるとき、上の適当な点を持ってきてを計算すればの位数は になります。このあたりについては yoshi-gcc log - ふるつき に書いています
今回気をつけるべきなのは、このように生成した点がsecp256r1上の点である必要がある、ということです。というのも↑のコード辺にあるように、サーバは送られてきた点が楕円曲線上に存在するかをIsOnCurve
メソッドによって調べています。しかしIsOnCurve
はあたえられた座標がであるかどうかはチェックしていませんから*2、secp224r1の剰余の法の上ではに、secp256r1の剰余の法の上ではsecp256r1上の適当な点になるような巨大な値を渡せば問題は解決です。そして、このような点は中国剰余定理で求めることができます
dの特定
しかしここまでやってもまだ少し問題があります。それは今回の問題では共通鍵としてsharedX
を使っている、という部分です。何が問題なのかといえば、とは同じX座標をもつので、ブルートフォースで得られたという式が実はかもしれない、という点です。当然ここが異なれば中国剰余定理で復元できる最終的なにも差がうまれますから、これは重大な問題です。
しかしこれを区別する良い方法は思いつかなかったので符号は全探索することにしました。これにかかる時間はでは中国剰余定理で復元するときのペアの数です。ここであまりに小さい剰余をつかっているとペアの数が60個とかになって到底計算できないので、↑のスクリプトでは多少時間がかかっても剰余を1000〜10000とある程度大きくしています。
exploit
長くなったのでここにおいておきます
the solution script of tiramisu from Google CTF 2021 · GitHub
[crypto] H1
こちらもECDHによって鍵を共有する問題です。こちらはAliceとBobがECDHによって鍵を共有しており、またいくつかのメッセージを相互に送り合っています。メッセージは暗号化されていますが、フラグの部分以外はわかっています。また、メッセージには署名が行われています
脆弱性
ECDSA時のの生成に脆弱性があります。生成はこう↓なっているのですが、この方法だとのエントロピーは8 * 16 = 256bit程度しかありません。の値が小さいとHNPなどでを復元して秘密鍵を求めることができます。
def RNG(nbits, a, b): nbytes = nbits // 8 B = os.urandom(nbytes) return a * sum([B[i] * b ** i for i in range(len(B))]) % 2**nbits def Sign(msg, d): h = hashlib.sha512(msg) z = Transform(int.from_bytes(h.digest(), 'big'), h.digest_size*8) k = RNG(n.bit_length(), 16843009, 4294967296) x1, y1, z1 = Multiply(G, k) r = (x1 * pow(z1, -2, mod) % mod) % n s = pow(k, -1, n) * (z + r * d) % n return r, s
kを求める
今回はBobからメッセージが2回送られていることを利用して、 という式をたててLLLでときました。ただし は8bitの値16個をRNG
関数と同様に結合する形にして、33 x 32の格子を構成するような感じです。小さい変数をたくさん作ることになったのでMultivariate CoppersmithよりはLLLのほうが扱いやすそう、と判断しました。
LLLの部分ではrkm0959さんのInequality Solving with CVPのライブラリを使いました。便利
Qaを求める
を求めるパートは自明なので飛ばして、共通鍵を得るためにAliceの公開鍵を求めます。これは署名が正しいと仮定するとメッセージと署名から公開鍵を逆算することができます。このプログラムでは計算がヤコビアンで行われていて散々バグらせましたが、最終的にecdsaライブラリのrecover_public_keys
を使う方法でうまくやりました
exploit
いろいろ試行錯誤していたあとが残っていますが整形も面倒なのでそのまま
https://gist.github.com/theoremoon/26c4d4cad1039cae1dba1a17e9ad3de9