強いCTFerかつVTuberのkurenaif先生が2000subscriber記念に問題を提供されていたので取り組みました。面白かったです。
kurenaif先生のチャンネルはこちら
問題はこちら
383bitの平文を40bit程度のを用いたRSAで400回暗号化しています。40bitの素因数分解は簡単なので、暗号文を復号すればいいと思いきや、なので復号に成功したとしても全体がわかるわけではありません。このような場合の対応として、各暗号文を復号して400個のを得て中国剰余定理に従って上のを求めるテクがあります。
しかしここで問題を難しくしているのが、公開鍵です。です。はが素数であることから偶数×偶数=偶数であり、はまず間違いなく満たされません。
このとき、暗号文のe乗根は一意に定まらず、複数個存在します。が5を素因数に含んでいないケースだけを考えても、各暗号文ごとに少なくとも個程度のe乗根が存在して、そのうちを満たすものは一つだけです。各暗号文についてe乗根全体を探索するのは明らかに不可能です。
そこでe乗根の数を減らすことを考えます。まず、ではなく、について考えます。これで組み合わせの個数が減ることはありませんが、問題を解くのに不便な素因数を除外して考えることができるようになります。
問題を解くのに不便な素因数とは何でしょう。例えばが5を素因数に含むケースは今回はスキップしてしまうことでを公開鍵とした暗号文についてだけ考えればよくなります。
また、のとき、この剰余の法のもとでの乗根の数は2個に限定されます(どうやって証明するのかわからなかった)。この性質を使うことで、という暗号文についての答えの候補を2個に絞ることができます。
はおよそ20bitですから、20個ほどの組を集めれば通りの全探索での候補が列挙でき、そのなかにフラグがあることを期待できます。
実際には22個の組をつかってやりました。
import ast with open("output.txt") as f: f.readline() e = ast.literal_eval(f.readline().strip().split("= ")[1]) ns = ast.literal_eval(f.readline().strip().split("= ")[1]) cs = ast.literal_eval(f.readline().strip().split("= ")[1]) e1 = 5^4 e2 = 2^5 xs = [] mods = [] cnt = 0 for i in range(len(ns)): for p, _ in factor(ns[i]): if p % 4 == 3 and (p-1) % 5 != 0: dp = int(pow(e1, -1, p-1)) xs.append(pow(cs[i], dp, p)) mods.append(p) roots = [] for i in range(len(xs)): r1, r2 = GF(mods[i])(xs[i]).nth_root(32, all=True) roots.append((int(r1), int(r2))) T = 22 from tqdm import tqdm from itertools import product print(2^T) for pat in tqdm(product([0, 1], repeat=T)): rs = [] for i in range(T): rs.append(roots[i][pat[i]]) x = CRT_list(rs, mods[:T]) x = int(x) if x.bit_length() == 383: print(x.to_bytes((383 + 7) // 8, "big"))
kurenaif先生、登録者数2000人突破おめでとうございます。これからもますますのご活躍を期待しています。