TL;DR

  • RSAのCycle Attackというもので用いる暗号化回数が与えられる
  • これを用いると実質的にλ(λ(n))\lambda(\lambda(n))が分かる
  • その逆演算は一意に定まらないが、λ(n)\lambda(n)の倍数を求める事は出来るのでこれを利用して復号出来る

Prerequisite

  • カーマイケル関数

Writeup

次のようなスクリプトが渡される

#!/usr/bin/python3

# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
It is well known that any RSA encryption can be undone by just encrypting the
ciphertext over and over again. If the RSA modulus has been chosen badly then
the number of encryptions necessary to undo an encryption is small.
If n = 0x112b00148621 then only 209 encryptions are necessary as the following
example demonstrates:

>>> e = 65537
>>> n = 0x112b00148621
>>> pt = 0xdeadbeef
>>> # Encryption
>>> ct = pow(pt, e, n)
>>> # Decryption via cycling:
>>> pt = ct
>>> for _ in range(209):
>>>   pt = pow(pt, e, n)
>>> # Assert decryption worked:
>>> assert ct == pow(pt, e, n)

However, if the modulus is well chosen then a cycle attack can take much longer.
This property can be used for a timed release of a message. We have confirmed
that it takes a whopping 2^1025-3 encryptions to decrypt the flag. Pack out
your quantum computer and perform 2^1025-3 encryptions to solve this
challenge. Good luck doing this in 48h.
"""

e = 65537
n = 0x99efa9177387907eb3f74dc09a4d7a93abf6ceb7ee102c689ecd0998975cede29f3ca951feb5adfb9282879cc666e22dcafc07d7f89d762b9ad5532042c79060cdb022703d790421a7f6a76a50cceb635ad1b5d78510adf8c6ff9645a1b179e965358e10fe3dd5f82744773360270b6fa62d972d196a810e152f1285e0b8b26f5d54991d0539a13e655d752bd71963f822affc7a03e946cea2c4ef65bf94706f20b79d672e64e8faac45172c4130bfeca9bef71ed8c0c9e2aa0a1d6d47239960f90ef25b337255bac9c452cb019a44115b0437726a9adef10a028f1e1263c97c14a1d7cd58a8994832e764ffbfcc05ec8ed3269bb0569278eea0550548b552b1
ct = 0x339be515121dab503106cd190897382149e032a76a1ca0eec74f2c8c74560b00dffc0ad65ee4df4f47b2c9810d93e8579517692268c821c6724946438a9744a2a95510d529f0e0195a2660abd057d3f6a59df3a1c9a116f76d53900e2a715dfe5525228e832c02fd07b8dac0d488cca269e0dbb74047cf7a5e64a06a443f7d580ee28c5d41d5ede3604825eba31985e96575df2bcc2fefd0c77f2033c04008be9746a0935338434c16d5a68d1338eabdcf0170ac19a27ec832bf0a353934570abd48b1fe31bc9a4bb99428d1fbab726b284aec27522efb9527ddce1106ba6a480c65f9332c5b2a3c727a2cca6d6951b09c7c28ed0474fdc6a945076524877680
# Decryption via cycling:
pt = ct
for _ in range(2**1025 - 3):
  pt = pow(pt, e, n)
# Assert decryption worked:
assert ct == pow(pt, e, n)

# Print flag:
print(pt.to_bytes((pt.bit_length() + 7)//8, 'big').decode())

RSAにはCycle Attackというものが存在し、不適切なnnを選ぶとnnの大きさに対して十分小さい回数の暗号化の繰り返しで平文を復元出来るらしい。スクリプトのコメントには例としてn = 0x112b00148621の場合が添えられており、この場合は暗号文を209回暗号化をすることで平文に戻る。

問題のnnでもこれに該当する回数が存在するが、そもそもnnが2048bitと大きいことから、この回数も非常に大きくなる。そしてその回数はなんと2102532^{1025} - 3回であり、CTF期間中どころか人類が滅ぶぐらいの時間が経過しそうである。

というわけで、nnに対して、追加のヒントとしてこの回数が与えられているという状況で暗号文を復号出来るかという問題になる。


この繰り返しの回数をkkとおくと、次が成り立っている。

mc(ek)mod  n m \equiv c^{(e^k)} \mod n

ここで、cmemod  nc\equiv m^e \mod nであるから、m(me)(ek)m(ek+1)mod  nm \equiv (m^{e})^{(e^k)} \equiv m^{(e^{k+1})} \mod nが成り立つ。mmek+1e^{k+1}乗すると元に戻る事を考えるとek+11mod  λ(n)e^{k+1} \equiv 1 \mod \lambda(n)であることが分かる1。ここでλ\lambdaはカーマイケル関数である。更にこの式もeek+1k+1乗すると1になる事を言っているので、k+10mod  λ(λ(n))k+1 \equiv 0 \mod \lambda(\lambda(n))であると考えられる2k+1k+1が無駄なく最も良い形で与えられているとしたら、k+1=λ(λ(n))k+1 = \lambda(\lambda(n))となり、以下ではこの仮定の下、議論を進める(実際、スクリプト記載のsmall exampleではλ(λ(n))=209+1\lambda(\lambda(n)) = 209+1となっている)。

この問題ではk+1=210252k+1 = 2^{1025} - 2となっており、これの素因数分解は比較的綺麗な形になる。具体的には指数が2以上の素因数が存在せずk+1=i=1lpik+1 = \prod_{i=1}^l p_iというll個の素因数に分解出来る。なお、SageMathのfactor()は時間がかかったので珍しくfactorDBを用いたら出てきた。

カーマイケル関数の定義を用いるとx=i=1lpieix = \prod_{i=1}^{l'} p_i^{e_i}とした時に、λ(x)=lcm(λ(p1e1),λ(p2e2),,λ(plel))\lambda(x) = \mathrm{lcm}(\lambda(p_1^{e_1}), \lambda(p_2^{e_2}), \dots, \lambda(p_{l'}^{e_{l'}}))となる。更に奇素数ppに対してはλ(pe)=pe1(p1)\lambda(p^{e'}) = p^{e'-1}(p-1)が成り立つ。

したがって、xxの素因数pip_iに対してpi1p_i-1λ(x)\lambda(x)の約数であることがわかる。また、今回はλ(x)\lambda(x)の素因数に指数が2以上のものが含まれないのでei2e_i \leq 2であることもわかる。よって、λ(x)\lambda(x)の約数を全部列挙し、それに1を足した結果が素数であれば、xxの素因数である可能性がある。このような値をpp'としてp2p'^2全ての積を計算すれば、それはxxの倍数となっている(ただし、CTF中はp2p'^2の2乗を見逃していて、pp'の総積だけを計算したが、運良く解けた)。

さて、問題のインスタンスに戻るとxxに相当するのはλ(n)\lambda(n)である。よって、この方法でλ(n)\lambda(n)の倍数を計算出来るのでCλ(n)C\lambda(n)とする。ちなみに、k+1=λ(λ(n))k+1 = \lambda(\lambda(n))の約数は2172^{17}個ぐらいあったが、1を足して素数になるのは600通りぐらいだったので総積を計算出来た。

これを用いてed1mod  Cλ(n)ed \equiv 1 \mod C\lambda(n)を計算する。このようなddを用いて復号すると次のようになる。

cdmedm1+CCλ(n)mmod  n c^d \equiv m^{ed} \equiv m^{1 + C'C\lambda(n)} \equiv m \mod n

ここで、CC'ed1=CCλ(n)ed - 1 = C'C\lambda(n)を満たす整数である。そして合同の最後の変形はmλ(n)1mod  nm^{\lambda(n)} \equiv 1 \mod nを用いた。

よって、mmが復号出来たことになり、フラグが手に入る。

Code

約数の列挙とそこからλ(n)\lambda(n)の倍数を復元するところまで(復号は別コード)

from itertools import product
from Crypto.Util.number import isPrime
from tqdm import tqdm


e = 65537
n = 0x99efa9177387907eb3f74dc09a4d7a93abf6ceb7ee102c689ecd0998975cede29f3ca951feb5adfb9282879cc666e22dcafc07d7f89d762b9ad5532042c79060cdb022703d790421a7f6a76a50cceb635ad1b5d78510adf8c6ff9645a1b179e965358e10fe3dd5f82744773360270b6fa62d972d196a810e152f1285e0b8b26f5d54991d0539a13e655d752bd71963f822affc7a03e946cea2c4ef65bf94706f20b79d672e64e8faac45172c4130bfeca9bef71ed8c0c9e2aa0a1d6d47239960f90ef25b337255bac9c452cb019a44115b0437726a9adef10a028f1e1263c97c14a1d7cd58a8994832e764ffbfcc05ec8ed3269bb0569278eea0550548b552b1
ct = 0x339be515121dab503106cd190897382149e032a76a1ca0eec74f2c8c74560b00dffc0ad65ee4df4f47b2c9810d93e8579517692268c821c6724946438a9744a2a95510d529f0e0195a2660abd057d3f6a59df3a1c9a116f76d53900e2a715dfe5525228e832c02fd07b8dac0d488cca269e0dbb74047cf7a5e64a06a443f7d580ee28c5d41d5ede3604825eba31985e96575df2bcc2fefd0c77f2033c04008be9746a0935338434c16d5a68d1338eabdcf0170ac19a27ec832bf0a353934570abd48b1fe31bc9a4bb99428d1fbab726b284aec27522efb9527ddce1106ba6a480c65f9332c5b2a3c727a2cca6d6951b09c7c28ed0474fdc6a945076524877680
k = 2**1025 - 3 + 1

# http://factordb.com/index.php?query=359538626972463181545861038157804946723595395788461314546860162315465351611001926265416954644815072042240227759742786715317579537628833244985694861278948248755535786849730970552604439202492188238906165904170011537676301364684925762947826221081654474326701021369172596479894491876959432609670712659248448274430
factors = [2, 3, 5, 17, 257, 641, 65537, 274177, 2424833, 6700417, 67280421310721, 1238926361552897, 59649589127497217, 5704689200685129054721, 7455602825647884208337395736200454918783366342657, 93461639715357977769163558199606896584051237541638188580280321, 741640062627530801524787141901937474059940781097519023905821316144415759504705008092818711693940737]

divs = []
michael = 1
for isdivs in tqdm(product([0,1], repeat=16)):
    prod = 2
    for isdiv, x in zip(isdivs, factors[1:]):
        if isdiv == 1:
            prod *= x
    if isPrime(prod + 1):
        divs.append(prod + 1)
        michael *= (prod + 1)

print(len(divs))
print(michael)

Flag

CTF{Recycling_Is_Great}


1

正確には特定の平文mmについてのみ成り立つことしか分かっていないが、任意のmmについても成り立つだろうという仮定を用いた

2

ここに関してもeeに対してのみ成り立つことしか分かっていない(つまりeeZλ(n)\mathbb Z_{\lambda(n)}^*における位数がk+1k+1であるという事実しかわからない)が、任意の値についても成り立つだろうという仮定を用いた