Google CTF 2021 のCryptoの問題はどれも楽しかったです。着目すべき点はわかりやすいけど解ききるのは大変という印象でした。チームメイトのS3v3ru5と一緒に取り組んでチームとしてはCryptoは全部とけて非常によかったです。
[crypto] tiramisu
go製のecho serverで、ECDHによって共通鍵を生成して暗号化されたメッセージをやりとりします。暗号文にはHMACによる認証符号を付与していて、認証が通らない場合はセッションを確立できません。また、フラグはサーバの秘密鍵 d から生成される鍵をつかってAESで暗号化されています。
脆弱性
ECDHの段階でサーバからは secp224r1 に準拠した鍵が送られてきます。また提供されるクライアントプログラムでもsecp224r1の鍵を生成してサーバに送っているのですが、プログラムやprotobuf*1をみるとどうやらsecp256r1の鍵を送ることもできるようでした。しかしプログラム上ではsecp224r1が送られてくることだけを想定しているらしく、次のようなコードがありました。
peer, err := proto2ecdsaKey(clientHello.Key)
if err != nil {
return err
}
if !peer.Curve.IsOnCurve(peer.X, peer.Y) {
return fmt.Errorf("point (%X, %X) not on curve", peer.X, peer.Y)
}
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