この記事はCTF Advent Calendar 2022の23日目の記事です。昨日はXornetさんのペアリングでCTFの問題を解くでした。昔、暗号の文脈以外でWeil pairingを勉強した記憶はあります*1が忘れました。後ほどゆっくり読みたいと思います。
初めまして、チョコラスクです。今年からCTFを始めました。初めて真面目に参加したCTFはzer0pts CTF 2022です。
以前は競技プログラミングをやっていました*2。また、大学で少し数学を勉強していました。
ちゃんとしたwriteupを公開するのは初めて*3で怖いですが、これまでに解いた問題のうち、競プロ知識や数学知識を使っていい感じに解けた問題のwriteupを書いてみます。
CakeCTF 2022 Rock Door
DSAが実装されており、一度だけ (goma
を含まない) 好きな文字列の署名 を計算させることができます。ただし、計算結果は の方しか得られません。その後、hirake goma
の正当な署名を入力できればフラグが得られます。
ソースコード内の各変数がどれぐらいの大きさかを確認してみます。 は 1024 bit程度であるのに対し、 はSHA256ハッシュを取った結果なので 256 bit程度です。好きな文字列について を取得すると、 という関係式が成り立ちます。 は 512 bit程度で、これは に比べて小さく、 を適当に選んだとき が 512 bit程度になる確率は 程度です。よって、 が 256 bit程度で が 512 bit程度という条件だけで が一意に定まりそうです。
は入力する文字列から計算できるので、 がわかれば がわかり、さらに は から求まるので、 もわかります。秘密鍵 がわかれば署名の計算ができるので、解けます。
想定解ではLLLを用いて を計算していますが、floor sum という (競プロ界では有名な) アルゴリズムを用いることもできるので、それを解説します。
floor sum は、正整数 と整数 に対し、
まず、 と仮定してよいです。実際、 とおくと、
なので、 の場合に帰着できます。 についても同様です。
とおきます。 の範囲に格子点はないことに注意すると、 は図の青い領域内に含まれる格子点の個数に等しいです。直線 を 軸、 を 軸と視点を変えてみることで、
がわかります。
よって、 から の場合に帰着でき、さらに の場合に帰着できるので、 にユークリッドの互除法を適用するのと同じ要領で の場合に帰着でき、 が求まります。
さて、これを今回の問題にどう適用するかですが、
であることに着目します。すると、 の範囲で を満たす の個数は
と表せます。 の範囲で初めて となる を二分探索することで、 を満たす を見つけることができます。
# sum[i=0 to n-1] floor((a*i+b)/m) def floor_sum(n, m, a, b): if n == 0: return 0 ret = 0 if a >= m: ret += n*(n-1)//2*(a//m) a %= m if b >= m: ret += n*(b//m) b %= m if a == 0: return ret q, r = (a*n+b)//m, (a*n+b)%m ret += floor_sum(q, a, m, r) return ret from hashlib import sha256 from Crypto.Util.number import getRandomRange, inverse, long_to_bytes def h(s: bytes) -> int: return int(sha256(s).hexdigest(), 16) q = 139595134938137125662213161156181357366667733392586047467709957620975239424132898952897224429799258317678109670496340581564934129688935033567814222358970953132902736791312678038626149091324686081666262178316573026988062772862825383991902447196467669508878604109723523126621328465807542441829202048500549865003 p = 2*q + 1 g = 2 y = 37925794679172810656660325405743980058149298941234310232364510309420515092602600523697777529812917758313422672128981188920487521638575170382644658820074833119309713550634143577305912149763628209572141367725296502112007404954248895722761291522996966773323841581645549327400582154539861787835821155142888287483 m1 = b"myon" z1 = h(m1) # "myon" に対する s の値 s1 = 34112470810592176118801977602000637660265529835590820260074241304093463468236031746057456541876447210174543389860988306468965410818788719940381401389545593766521657980898975708370037147335849545814914529044948910315266154298872698766582160589313927112886437376450668673875319127868804480390170246274373661339 assert floor_sum(2**256, q, s1, 0)-floor_sum(2**256, q, s1, -2**512) == 1 left = 0 right = 2**256 while right-left > 1: mid = (left+right)//2 if floor_sum(mid, q, s1, 0)-floor_sum(mid, q, s1, -2**512) > 0: right = mid else: left = mid k1 = left r1 = h(long_to_bytes(pow(g, k1, p))) xr1 = (k1*s1-z1)%q assert xr1%r1 == 0 x = xr1//r1 assert pow(g, x, p) == y m = b"hirake goma" z = h(m) k = getRandomRange(0, q) r = h(long_to_bytes(pow(g, k, p))) assert r < q s = (z + x*r) * inverse(k, q) % q sinv = inverse(s, q) gk = pow(g, sinv*z, p) * pow(y, sinv*r, p) % p r2 = h(long_to_bytes(gk)) assert r == r2 print(f'r = {r}') print(f's = {s}')
SECCON Beginners CTF 2022 omni-RSA
RSA暗号っぽいものが実装されています。 つの素数 を用いており、 および、秘密鍵 について の下 470 bit ( とする) が与えられます。簡単にわかることとして、 はともに 256 bitで なので、 です。
42 bit程度の整数 を用いて と書けます。 なので、正整数 を用いて
と書けます。ここで なので です。 は全探索ができそうです。
想定解では を取ってCoppersmith法で を求めています*5が、ここでは を取ってみます。すると です。 を全探索することにすると、未知数は のみです。 倍して平方完成すると
となります。 は 270 bit未満で小さいです。
あとは という方程式が解ければ がわかり、 がわかれば より がわかり、 より もわかるので、復号できます。
Henselの補題を思い出すと、これは解けそうです:
を素数、 を整数係数多項式、 を正整数とし、 および が成り立っているとする。このとき、, を満たす が で唯一つ存在する。
今回はHenselの補題における微分が0でないという条件が満たされないので、そのままは成り立ちませんが、似たようなことは成り立ちます:
を奇数、 とし、 が成り立っているとする。このとき、, を満たす が で唯一つ存在する。
これは、 と のうちちょうど一方が次の解になることが簡単にわかるので示せます。よって、 での解、 での解、 での解・・・を順番に計算していけば解けます。
これをそのまま実装してもよいですが、SageMathには 進数を扱う関数が色々用意されているので、それを使うとシンプルに実装できます*6。問題は、 が適当な精度で与えられているときに、 進数体 上で を解く問題と考えられ、これはSageMathのsquare_root関数で実現できます。
公式ドキュメントに記載されているように、 の場合精度が1だけ落ちますが、これは、 が奇数のとき の解は で (存在すれば) 2個であることから理解できます。
細かい注意として、SageMathの 進整数環のクラスには絶対精度と相対精度の2種類があります*7。
sage: a = Zp(2, prec = 10)(272) sage: a 2^4 + 2^8 + O(2^14) sage: a.square_root(all=True) [2^2 + 2^5 + 2^7 + 2^8 + 2^9 + O(2^11), 2^2 + 2^3 + 2^4 + 2^6 + 2^10 + O(2^11)] sage: a = Zp(2, prec = 10, type = 'capped-abs')(272) sage: a 2^4 + 2^8 + O(2^10) sage: a.square_root(all=True) [2^2 + 2^5 + O(2^7), 2^2 + 2^3 + 2^4 + 2^6 + O(2^7)]
今回は常に で解きたいので、絶対精度の方を指定するとよいです。
from Crypto.Util.number import * from output import * R = Zp(2, prec = 470, type = 'capped-abs') for k in range(1, e): a = k*k*rq*rq + 4*k*e*s - 4*k try: xs = R(a).square_root(all=True) except Exception: continue x = min([v.lift() for v in xs]) if int(x).bit_length() > 270: continue q = (x//k-rq)//2+1 r = q+rq p = n//(q*r) d = inverse(e, (p-1)*(q-1)*(r-1)) print(long_to_bytes(int(pow(cipher, d, n))))
明日はGodAimPikaさんの「CTF初心者がNSA Codebreakerに参加した感想」です。
*1:Silvermanの本を読んだりした気がします。
*2:人生が忙しいわけではないのですが、赤になってモチベが下がってしまいました。
*3:CTF界のwriteup文化すごい。
*4:競プロ文脈のように四則演算の計算量を とみなせる場合、計算量は ですが、そうでない場合何と書けばいいかパッとはわからなかったのでごまかしました。
*5:そもそも の方程式だけでなく の約数 の方程式も解けるというのを知らず、勉強になりました。
*6:CTFのCrypto問で 進数が使えることがあるというのは、zer0pts CTF 2021 pure divisionのwriteupで見ました。
*7:コンテスト中は、何も指定しないと相対精度になることに気づいておらず、なぜか を取らないとうまく求まらないなあと思っていました。