某国赛CTF密码学题目Writeup:RSA

目录

题目

一个level1目录,内部是很多公钥文件,一个level2压缩包带密码,密码应该在level1解密拿到

level1

有一个README,看一下内容

我们需要先恢复message内容

有两个python脚本,看名称一个是生成message,另一个是加密

message

先看generate-plaintexts.py,分析一下原始明文的内容

这里我们可以得知share的格式是模数d : 余数k : 原始比特长度

plain 复制代码
d = 一个素数
k = S mod d
original_bits = 原消息长度

但是仔细看脚本里的Asmuth-Bloom并没有真正做标准的Asmuth-Bloom重构条件,本质上只是把秘密整数 S 拆成了很多个

也就是说后面只要拿到足够多组(di, ki),就能直接用中国剩余定理把S还原出来

往后看生成的消息和share,先把message1.txt的内容被复制到 10 个plaintext文件的开头

message2到message10每个message都被分成10份share,分别放进对应的plaintext-xxx.txt

加密分析

继续看encrypt.py分析加密方式

当rsa模数位数大于等于2048时,先随机生成AES key,再RSA-OAEP加密这个AES key,然后AES-GCM 加密正文

当 RSA 模数位数小于2048时,直接RSA加密AES key,再AES-GCM加密正文

现在我们的思路就是,先找出多个能被破解的RSA公钥,解开对应的ciphertext,拿到多条share,再对每个message用CRT重建

公钥分析

先看一下所有公钥的特征

plain 复制代码
key-0.pem   4096 bits, e=65537
key-1.pem   2048 bits, e=65537
key-2.pem   2047 bits, e=65537
key-3.pem   2047 bits, e=65537
key-4.pem   2566 bits, e=65537
key-5.pem   198 bits,  e=3
key-6.pem   2047 bits, e=一个超大数
key-7.pem   2047 bits, e=65537
key-8.pem   2048 bits, e=65537
key-9.pem   4095 bits, e=65537
key-10.pem  4095 bits, e=65537
key-11.pem  4096 bits, e=65537
key-12.pem  2047 bits, e=一个超大数
key-13.pem  2048 bits, e=65537
key-14.pem  4095 bits, e=65537
key-15.pem  2565 bits, e=65537
key-16.pem  2048 bits, e=65537
key-17.pem  2047 bits, e=一个超大数
key-18.pem  2048 bits, e=65537
key-19.pem  4096 bits, e=65537

1、key-5.pem很小,可以直接分解

2、有几对大小相同,可能存在共因子或者相近素数

3、key-6、key-12、key-17 的e很大

两两求gcd(key 1、2、4、15)

编写脚本批量

python 复制代码
#!/usr/bin/env python3
from math import gcd
from pathlib import Path
import argparse
from Crypto.PublicKey import RSA


def load_keys(key_dir: Path):
    keys = []
    for pem in sorted(key_dir.glob("key-*.pem"), key=lambda p: int(p.stem.split("-")[1])):
        idx = int(pem.stem.split("-")[1])
        key = RSA.import_key(pem.read_bytes())
        keys.append({
            "idx": idx,
            "path": pem,
            "n": key.n,
            "e": key.e,
            "bits": key.n.bit_length(),
        })
    return keys


def main():

    key_dir = Path("level1").resolve()
    keys = load_keys(key_dir)

    if not keys:
        raise SystemExit(f"No key-*.pem found in {key_dir}")

    hits = []
    for i in range(len(keys)):
        for j in range(i + 1, len(keys)):
            k1 = keys[i]
            k2 = keys[j]
            g = gcd(k1["n"], k2["n"])
            if g == 1:
                continue

            q1 = k1["n"] // g
            q2 = k2["n"] // g
            hits.append((k1, k2, g, q1, q2))

            print(f"[!] shared factor found: key-{k1['idx']}.pem <-> key-{k2['idx']}.pem")
            print(f"    gcd bits : {g.bit_length()}")
            print(f"    q1  bits : {q1.bit_length()}")
            print(f"    q2  bits : {q2.bit_length()}")
            print(f"    check #1 : {g * q1 == k1['n']}")
            print(f"    check #2 : {g * q2 == k2['n']}")
            print()

    if not hits:
        print("[-] no non-trivial gcd found")
        return

    print("[+] summary")
    for k1, k2, g, q1, q2 in hits:
        print(
            f"    key-{k1['idx']} / key-{k2['idx']} -> "
            f"shared {g.bit_length()} bits, others {q1.bit_length()} / {q2.bit_length()} bits"
        )


if __name__ == "__main__":
    main()

共有两组存在共因子,接下来通过gcd恢复私钥

python 复制代码
#!/usr/bin/env python3
from math import gcd
from pathlib import Path
from Crypto.PublicKey import RSA


def load_public_keys(level1_dir: Path):
    keys = {}
    for pem in sorted(level1_dir.glob("key-*.pem"), key=lambda p: int(p.stem.split("-")[1])):
        idx = int(pem.stem.split("-")[1])
        keys[idx] = RSA.import_key(pem.read_bytes())
    return keys


def main():
    root = Path(__file__).resolve().parent
    level1_dir = root / "level1"
    out_dir = root / "recovered_keys_from_gcd"
    out_dir.mkdir(exist_ok=True)

    keys = load_public_keys(level1_dir)
    indices = sorted(keys)
    recovered = {}

    print(f"[+] scanning {len(indices)} public keys for shared factors")
    print()

    for i in range(len(indices)):
        for j in range(i + 1, len(indices)):
            a = indices[i]
            b = indices[j]
            n1 = keys[a].n
            n2 = keys[b].n
            g = gcd(n1, n2)
            if g == 1:
                continue

            print(f"[!] shared factor found: key-{a}.pem <-> key-{b}.pem")
            print(f"    gcd bits = {g.bit_length()}")

            for idx, other in ((a, b), (b, a)):
                p = int(g)
                q = int(keys[idx].n // g)
                phi = (p - 1) * (q - 1)
                d = pow(keys[idx].e, -1, phi)
                priv = RSA.construct((keys[idx].n, keys[idx].e, d, p, q))
                recovered[idx] = {
                    "p": p,
                    "q": q,
                    "d": d,
                    "other": other,
                    "priv": priv,
                }
                out_path = out_dir / f"key-{idx}-private.pem"
                out_path.write_bytes(priv.export_key())
                print(f"    [OK] recovered key-{idx}.pem using gcd with key-{other}.pem")
                print(f"         wrote {out_path.name}")
            print()

    print("[+] summary")
    if not recovered:
        print("    no gcd-vulnerable keys found")
        return

    for idx in sorted(recovered):
        info = recovered[idx]
        print(
            f"    key-{idx}.pem: p_bits={info['p'].bit_length()} "
            f"q_bits={info['q'].bit_length()} d_bits={info['d'].bit_length()}"
        )


if __name__ == "__main__":
    main()

成功恢复4个私钥,解密得到两份plaintext

小模数直接因式分解(key 5)

直接写脚本分解

python 复制代码
#!/usr/bin/env python3
from pathlib import Path
from Crypto.PublicKey import RSA
from sympy import factorint


def load_public_keys(level1_dir: Path):
    keys = {}
    for pem in sorted(level1_dir.glob("key-*.pem"), key=lambda p: int(p.stem.split("-")[1])):
        idx = int(pem.stem.split("-")[1])
        keys[idx] = RSA.import_key(pem.read_bytes())
    return keys


def main():
    root = Path(__file__).resolve().parent
    level1_dir = root / "level1"
    out_dir = root / "recovered_keys_small_modulus"
    out_dir.mkdir(exist_ok=True)

    keys = load_public_keys(level1_dir)

    print("[+] scanning for small RSA moduli")
    print()

    recovered = {}
    for idx in sorted(keys):
        key = keys[idx]
        bits = key.n.bit_length()

        # 这里只打非常小的模数,避免误伤大数
        if bits > 256:
            continue

        print(f"[+] trying key-{idx}.pem ({bits} bits)")
        factors_dict = factorint(key.n)

        factors = []
        for prime, exp in factors_dict.items():
            factors.extend([int(prime)] * exp)

        prod = 1
        for f in factors:
            prod *= f

        if prod != key.n:
            print("    [-] factorization check failed")
            print()
            continue

        phi = 1
        for f in factors:
            phi *= (f - 1)

        try:
            d = pow(key.e, -1, phi)
        except ValueError:
            print("    [-] e is not invertible modulo phi")
            print()
            continue

        recovered[idx] = {
            "factors": factors,
            "d": d,
        }

        out_path = out_dir / f"key-{idx}-private-info.txt"
        lines = [
            f"key = key-{idx}.pem",
            f"n_bits = {bits}",
            f"e = {key.e}",
            f"d = {d}",
            f"factor_count = {len(factors)}",
            "factors =",
        ]
        lines.extend(str(f) for f in factors)
        out_path.write_text("\n".join(lines) + "\n")

        print(f"    [OK] factors found: {len(factors)} prime factors")
        print(f"    [OK] wrote {out_path.name}")
        print()

    print("[+] summary")
    if not recovered:
        print("    no small modulus key was factored")
        return

    for idx in sorted(recovered):
        info = recovered[idx]
        bit_list = ", ".join(str(f.bit_length()) for f in info["factors"])
        print(f"    key-{idx}.pem -> factor bits [{bit_list}]")


if __name__ == "__main__":
    main()
plain 复制代码
n_bits = 198
e = 3
d = 223190228465821694677907677521533419817411950213806180649643
factor_count = 3
factors =
1528159911436197621287
15950473204537742369
13734854446830088769

但是这一步得到的结果一个都解不出来,应该是误导

Fermat分解(key 3、7、13、18)

我们之前公钥分析的时候看到了有几组2047/2048位模数,很像正常双素数RSA的大小,尝试Fermat分解

python 复制代码
#!/usr/bin/env python3
from math import isqrt
from pathlib import Path
from Crypto.PublicKey import RSA


def load_public_keys(level1_dir: Path):
    keys = {}
    for pem in sorted(level1_dir.glob("key-*.pem"), key=lambda p: int(p.stem.split("-")[1])):
        idx = int(pem.stem.split("-")[1])
        keys[idx] = RSA.import_key(pem.read_bytes())
    return keys


def fermat_factor(n, max_steps=200000):
    a = isqrt(n)
    if a * a < n:
        a += 1
    for step in range(max_steps + 1):
        b2 = a * a - n
        b = isqrt(b2)
        if b * b == b2:
            p = int(a - b)
            q = int(a + b)
            if p * q == n:
                return p, q, step
        a += 1
    return None


def main():
    root = Path(__file__).resolve().parent
    level1_dir = root / "level1"
    out_dir = root / "recovered_keys_fermat"
    out_dir.mkdir(exist_ok=True)

    keys = load_public_keys(level1_dir)

    print("[+] scanning keys with Fermat factorization")
    print()

    recovered = {}
    for idx in sorted(keys):
        key = keys[idx]
        bits = key.n.bit_length()

        # Fermat 主要给中等/大模数用,小模数前一步已处理
        if bits < 512:
            continue

        print(f"[+] trying key-{idx}.pem ({bits} bits)")
        result = fermat_factor(key.n)
        if not result:
            print("    [-] not factorable within step limit")
            print()
            continue

        p, q, step = result
        phi = (p - 1) * (q - 1)
        try:
            d = pow(key.e, -1, phi)
        except ValueError:
            print("    [-] e is not invertible modulo phi")
            print()
            continue

        priv = RSA.construct((key.n, key.e, d, p, q))
        pem_path = out_dir / f"key-{idx}-private.pem"
        info_path = out_dir / f"key-{idx}-info.txt"
        pem_path.write_bytes(priv.export_key())
        info_path.write_text(
            "\n".join([
                f"key = key-{idx}.pem",
                f"n_bits = {bits}",
                f"method = Fermat",
                f"step = {step}",
                f"p_bits = {p.bit_length()}",
                f"q_bits = {q.bit_length()}",
                f"d_bits = {d.bit_length()}",
                f"p = {p}",
                f"q = {q}",
                f"d = {d}",
            ]) + "\n"
        )
        recovered[idx] = {"step": step, "p": p, "q": q, "d": d}
        print(f"    [OK] factorable by Fermat at step {step}")
        print(f"    [OK] wrote {pem_path.name}")
        print()

    print("[+] summary")
    if not recovered:
        print("    no key was factored by Fermat")
        return
    for idx in sorted(recovered):
        info = recovered[idx]
        print(
            f"    key-{idx}.pem -> step={info['step']} "
            f"p_bits={info['p'].bit_length()} q_bits={info['q'].bit_length()}"
        )


if __name__ == "__main__":
    main()

成功恢复4个私钥,解密得到3份plaintext

中国剩余定理恢复message

到这一步我们已经拿到了5份plaintext

内容和我们之前分析的一致,对应的列数分别如下

先把share按message对齐

对每条message做CRT恢复

level1密码

得到密码:9Zr4M1ThwVCHe4nHnmOcilJ8

level2

用密码解压后得到

level3仍然是个带密码的压缩包,先来看这个脚本

python 复制代码
import gmpy2
import hashlib
from Crypto.Util.number import *

def generate_polynomial_coefficients(degree, bound=100):
    coefficients = []
    for i in range(degree + 1):
        coefficients.append(getRandomRange(-bound, bound))
    while coefficients[-1] == 0:
        coefficients[-1] = getRandomRange(-bound, bound)
    return coefficients

def evaluate_polynomial(coeffs, x, n):
    result = 0
    power = 1
    for i, coeff in enumerate(coeffs):
        result = (result + coeff * power) % n
        power = (power * x) % n
    return result

p = getPrime(512)
q = getPrime(512)
d = getPrime(180)  

lam = (p-1) * (q-1) // gmpy2.gcd(p-1, q-1)

e = gmpy2.invert(d, lam)

n = p * q
m1 = bytes_to_long(b"Secret message: " + b"A" * 16)

poly2_coeffs = generate_polynomial_coefficients(2)
poly3_coeffs = generate_polynomial_coefficients(3)

m2 = evaluate_polynomial(poly2_coeffs, m1, n)
m3 = evaluate_polynomial(poly3_coeffs, m1, n)

c1 = pow(m1, e, n)
c2 = pow(m2, e, n)
c3 = pow(m3, e, n)

flag_hash = hashlib.sha256(str(p + q).encode()).hexdigest()
flag = f"next pass is {flag_hash}"

poly2_coeffs_display = poly2_coeffs.copy()
poly2_coeffs_display[0] = "?"  
poly3_coeffs_display = poly3_coeffs.copy()
poly3_coeffs_display[0] = "?"  

print("n =", n)
print("e =", e)
print("c1 =", c1)
print("c2 =", c2)
print("c3 =", c3)
print("poly2_coeffs =", poly2_coeffs_display)  
print("poly3_coeffs =", poly3_coeffs_display)  

# n = 99573363048275234764231402769464116416087010014992319221201093905687439933632430466067992037046120712199565250482197004301343341960655357944577330885470918466007730570718648025143561656395751518428630742587023267450633824636936953524868735263666089452348466018195099471535823969365007120680546592999022195781
# e = 12076830539295193533033212232487568888200963123024189287629493480058638222146972496110814372883829765692623107191129306190788976704250502316265439996891764101447017190377014980293589797403095249538391534986638973035285900867548420192211241163778919028921502305790979880346050428839102874086046622833211913299
# c1 = 88537483899519116785221065592618063396859368769048931371104532271282451393564912999388648867349770059882231896252136530442609316120059139869000411598215669228402275014417736389191093818032356471508269901358077592526362193180661405990147957408129845474938259771860341576649904811782733150222504695142224907008
# c2 = 94664119872856479106437852390497826718494891787231788048637569911820802241271385500237521849783257742020074596801243551199558780516078698265978603364906102405513805258523893161481880920117078266554039648951735640682344760175446065823258414274759467738667667682764189886842676476993302026419523097311062869020
# c3 = 17452842791128500883484238194649202112170709841838938382334668085116735124742824096780522905501631340623564870573855325492444043679562915249332054460794149403631363005265204604824764244033832504758820981926933304283896441280291037649477532990161765794023799333588888734136298348520144818810380374971149851767
# poly2_coeffs = ['?', -36, -94]
# poly3_coeffs = ['?', -11, -20, 11]

task.py分析

1、生成512-bit的p,q

2、生成一个180-bit的私钥指数d

3、构造公钥:lam = lcm(p-1, q-1);e = invert(d, lam)

4、计算c1、c2、c3

最终密码是sha256(str(p + q).encode()).hexdigest(),也就是说固定为sha256(str(p + q)),我们只需要分解n得到p、q即可

这道题首先想到从c1、c2、c3和多项式关系入手,但是多项式常数项被隐藏了,e也不是小指数,不一定能够快速分解,继续往下看

d只有180位,而n有1024位,那么n^(1/4) ≈ 2^256,d远远小于这个量级,也就是小私钥RSA,这里我们很容易联想到Wiener攻击

但这里并不是标准的Wiener,正常e为invert(d, phi(n))

标准Wiener推导很多时候默认的是ed - 1 = k * φ(n)

这里变成了ed - 1 = k * λ(n)

也就是说ed - 1是λ(n)的倍数,不一定是φ(n)的倍数

所以普通版Wiener套公式可能不直接命中,但小私钥的问题还是存在

概括一下思路:

1、先把它认成小私钥RSA,也就是Wiener家族问题

2、对e/n做连分数展开,拿到一串收敛分数候选

3、因为这里用的是λ(n)而不是φ(n),所以不能死套标准Wiener判定,而是做一个小修正:

  • 从收敛分数里拿候选d

  • 允许它和真实d差一个很小的因子修正

  • 然后利用ed - 1是λ(n)的倍数这一点去反向分解n

为什么拿到ed-1的倍数就能分解n呢,因为如果拿到了某个数M是λ(n)的倍数,那么对于任意和n互素的a:a^M≡1modn

这就和RSA/群阶有关,然后可以像Miller-Rabin/平方根分解那样,把M写成:M=2^s*t

不断平方,寻找非平凡平方根,从而求出:gcd(x-1,n)

最后把n拆开,所以只要从小私钥候选里拿到一个正确的ed-1,就能把n因式分解

关键信息有:n、e、d很小、flag为sha256(str(p+q))

修正版Wiener分解p,q

编写脚本

python 复制代码
#!/usr/bin/env python3
from math import gcd
from pathlib import Path
import json
import random

# 来自 level2/task.py 注释区的公开参数
N = 99573363048275234764231402769464116416087010014992319221201093905687439933632430466067992037046120712199565250482197004301343341960655357944577330885470918466007730570718648025143561656395751518428630742587023267450633824636936953524868735263666089452348466018195099471535823969365007120680546592999022195781
E = 12076830539295193533033212232487568888200963123024189287629493480058638222146972496110814372883829765692623107191129306190788976704250502316265439996891764101447017190377014980293589797403095249538391534986638973035285900867548420192211241163778919028921502305790979880346050428839102874086046622833211913299


def rational_to_contfrac(x: int, y: int):
    out = []
    while y:
        a = x // y
        out.append(a)
        x, y = y, x - a * y
    return out


def convergents_from_contfrac(frac):
    n0, d0 = 0, 1
    n1, d1 = 1, 0
    for a in frac:
        n2 = a * n1 + n0
        d2 = a * d1 + d0
        yield n2, d2
        n0, d0, n1, d1 = n1, d1, n2, d2


def factor_from_lambda_multiple(n: int, m: int, trials: int = 80):
    if m <= 0:
        return None

    t = m
    s = 0
    while t % 2 == 0:
        t //= 2
        s += 1

    if s == 0:
        return None

    bases = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
    bases += [random.randrange(2, n - 1) for _ in range(trials)]

    for a in bases:
        g = gcd(a, n)
        if 1 < g < n:
            return g, n // g

        x = pow(a, t, n)
        if x == 1 or x == n - 1:
            continue

        for _ in range(s):
            y = pow(x, 2, n)
            if y == 1:
                p = gcd(x - 1, n)
                if 1 < p < n:
                    return p, n // p
                break
            if y == n - 1:
                break
            x = y
    return None


def main():
    root = Path(__file__).resolve().parent
    out_json = root / "step5_level2_factor_result.json"
    out_txt = root / "step5_level2_factor_result.txt"

    frac = rational_to_contfrac(E, N)

    print("[+] running modified Wiener attack on level2")
    print(f"[+] convergents: {len(frac)}")
    print()

    # 这里的关键:候选分母可能是 g * d,所以尝试除以一个小因子 g
    small_divisors = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32]

    checked = 0
    for idx, (k, q_candidate) in enumerate(convergents_from_contfrac(frac), start=1):
        if q_candidate.bit_length() > 220:
            break

        for g in small_divisors:
            if q_candidate % g != 0:
                continue

            d = q_candidate // g
            if d <= 0:
                continue

            m = E * d - 1
            checked += 1

            fac = factor_from_lambda_multiple(N, m)
            if not fac:
                continue

            p, q = fac
            if p * q != N:
                continue
            if p > q:
                p, q = q, p

            result = {
                "method": "modified_wiener_lambda",
                "convergent_index": idx,
                "small_divisor_g": g,
                "d": str(d),
                "d_bits": d.bit_length(),
                "p": str(p),
                "q": str(q),
                "p_bits": p.bit_length(),
                "q_bits": q.bit_length(),
                "n": str(N),
                "e": str(E),
            }
            out_json.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
            out_txt.write_text(
                "\n".join([
                    "[level2 factor result]",
                    f"method = {result['method']}",
                    f"convergent_index = {idx}",
                    f"small_divisor_g = {g}",
                    f"d_bits = {d.bit_length()}",
                    f"p_bits = {p.bit_length()}",
                    f"q_bits = {q.bit_length()}",
                    f"p = {p}",
                    f"q = {q}",
                ]) + "\n"
            )

            print(f"[OK] found factors at convergent #{idx}, g={g}")
            print(f"     d bits = {d.bit_length()}")
            print(f"     p bits = {p.bit_length()}")
            print(f"     q bits = {q.bit_length()}")
            print(f"[+] wrote {out_json}")
            print(f"[+] wrote {out_txt}")
            return

    print(f"[-] not found after checking {checked} candidates")


if __name__ == "__main__":
    main()
plain 复制代码
method = modified_wiener_lambda
convergent_index = 108
small_divisor_g = 4
d_bits = 180
p_bits = 512
q_bits = 512
p = 9107629749578703007521753435128188255997152813554367948373413785978810344628038189563213036161837882971963700600044412434325055960560499599730973768232229
q = 10932961240863053140021227333995503042196506533433133299916502568289020826078008576745192725272664686384293526720057562101790406102729886343233171385110689

成功分解p、q

从p,q计算下一层密码

python 复制代码
#!/usr/bin/env python3
from pathlib import Path
import json
import hashlib


def main():
    root = Path(__file__).resolve().parent
    in_json = root / "step5_level2_factor_result.json"
    out_txt = root / "step5_level2_next_pass.txt"

    if not in_json.exists():
        raise SystemExit(f"Missing input file: {in_json}")

    result = json.loads(in_json.read_text())
    p = int(result["p"])
    q = int(result["q"])
    s = p + q
    digest = hashlib.sha256(str(s).encode()).hexdigest()
    password = f"next pass is {digest}"

    out_txt.write_text(
        "\n".join([
            "[level2 next pass]",
            f"p_plus_q = {s}",
            f"sha256(str(p+q)) = {digest}",
            password,
        ]) + "\n"
    )

    print(f"[+] p + q = {s}")
    print(f"[+] sha256(str(p+q)) = {digest}")
    print(password)
    print(f"[+] wrote {out_txt}")


if __name__ == "__main__":
    main()

level2密码

plain 复制代码
[+] p + q = 20040590990441756147542980769123691298193659346987501248289916354267831170706046766308405761434502569356257227320101974536115462063290385942964145153342918
[+] sha256(str(p+q)) = 2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac
next pass is 2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac

得到密码:2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac

level3

解压得到level3脚本

python 复制代码
from Crypto.Util.number import getPrime, bytes_to_long

p = getPrime(1536)
q = getPrime(1536)
n = p * q
e = 65537

flag = b"dart{**************}"
c = pow(bytes_to_long(flag), e, n)

CONST1 = 0xDEADBEEFCAFEBABE123456789ABCDEFFEDCBA9876543210
CONST2 = 0xCAFEBABEDEADBEEF123456789ABCDEF0123456789ABCDEF
CONST3 = 0x123456789ABCDEFFEDCBA9876543210FEDCBA987654321
MOD128 = 2 ** 128  
MASK64 = (1 << 64) - 1  
MASK128 = (1 << 128) - 1  

leak = ( (p * CONST1) ^ (q * CONST2) ^ ((p & q) << 64) ^ ((p | q) << 48) ^ ((p ^ q) * CONST3) ) + ((p + q) % MOD128) ^ ((p * q) & MASK64)

print(f"n = {n}")
print(f"e = {e}")
print(f"c = {c}")
print(f"leak = {leak}")

# n = 3656543170780671302102369785821318948521533232259598029746397061108006818468053676291634112787611176554924353628972482471754519193717232313848847744522215592281921147297898892307445674335249953174498025904493855530892785669281622228067328855550222457290704991186404511294392428626901071668540517391132556632888864694653334853557764027749481199416901881332307660966462957016488884047047046202519520508102461663246328437930895234074776654459967857843207320530170144023056782205928948050519919825477562514594449069964098794322005156920839848615481717184615581471471105167310877784107653826948801838083937060929103306952084786982834242119877046219260840966142997264676014575104231122349770882974818427591538551719990220347345614399639643257685591321500648437402084919467346049683842042993975696447711080289559063959271045082506968532103445241637971734173037224394103944153692310048043693502870706225319787902231218954548412018259
# e = 65537
# c = 1757914668604154089701710446907445787512346500378259224658947923217272944211214757488735053484213917067698715050010452193463598710989123020815295814709518742755820383364097695929549366414223421242599840755441311771835982431439073932340356341636346882464058493459455091691653077847776771631560498930589569988646613218910231153610031749287171649152922929066828605655570431656426074237261255561129432889318700234884857353891402733791836155496084825067878059001723617690872912359471109888664801793079193144489323455596341708697911158942505611709946252101670450796550313079139560281843612045681545992626944803230832776794454353639122595107671267859292222861367326121435154862607517890329925621367992667728899878422037182817860641530146234730196633237339901726508906733897556146751503097127672718192958642776389691940671356367304182825433592577899881444815062581163386947075887218537802483045756886019426749855723715192981635971943
# leak = 153338022210585970687495444409227961261783749570114993931231317427634321118309600575903662678286698071962304436931371977179197266063447616304477462206528342008151264611040982873859583628234755013757003082382562012219175070957822154944231126228403341047477686652371523951028071221719503095646413530842908952071610518530005967880068526701564472237686095043481296201543161701644160151712649014052002012116829110394811586873559266763339069172495704922906651491247001057095314718709634937187619890550086009706737712515532076

task.py分析

这一层题目给了个leak:

python 复制代码
leak = ( (p * CONST1) ^ (q * CONST2) ^ ((p & q) << 64) ^ ((p | q) << 48) ^ ((p ^ q) * CONST3) ) + ((p + q) % MOD128) ^ ((p * q) & MASK64)

而且我们已知:n=p*q

这意味着n给了p*q的所有低位约束,leak又给了你p,q混合后的额外低位约束

这类题很适合做从低位开始,一位一位枚举p,q的bit,逐步筛掉错误分支,也就是bitlifting/low-bitreconstruction

假设已经猜出了p,q的低k位,那么可以检查p*q mod 2^k是否等于n mod 2^k

也可以检查leak的低k位是否匹配,如果都匹配就保留这个候选,如果不匹配就裁掉,由于这题约束很强,实际会很快收敛到唯一一组p,q

我们的思路就很清晰了,从低位恢复 p,q,计算d = e^{-1} mod (p-1)(q-1)、m = c^d mod n

得到最终 flag

bit-lifting分解p、q

编写bit-lifting脚本

python 复制代码
#!/usr/bin/env python3
from pathlib import Path
import json


MASK64 = (1 << 64) - 1


def main():
    root = Path(__file__).resolve().parent
    in_json = root / "step6_level3_params.json"
    out_json = root / "step6_level3_factor_result.json"
    out_txt = root / "step6_level3_factor_result.txt"

    params = json.loads(in_json.read_text())
    n = int(params["n"])
    leak = int(params["leak"])
    A = int(params["const1"])
    B = int(params["const2"])
    C = int(params["const3"])

    target = leak ^ (n & MASK64)

    def t_low(p: int, q: int, k: int):
        mask = (1 << k) - 1
        x = ((p * A) ^ (q * B) ^ ((p & q) << 64) ^ ((p | q) << 48) ^ ((p ^ q) * C))
        s = (p + q) & ((1 << 128) - 1)
        return (x + s) & mask

    cands = {(1, 1)}
    print("[+] starting low-bit lifting")

    for k in range(1, 1536):
        mask = (1 << (k + 1)) - 1
        target_n = n & mask
        target_t = target & mask
        nxt = set()

        for p, q in cands:
            for bp in (0, 1):
                for bq in (0, 1):
                    pp = p | (bp << k)
                    qq = q | (bq << k)
                    if (pp * qq) & mask != target_n:
                        continue
                    if t_low(pp, qq, k + 1) != target_t:
                        continue
                    nxt.add((pp, qq))

        cands = nxt
        if not cands:
            raise SystemExit(f"No candidates left at bit {k + 1}")
        if len(cands) > 10000:
            raise SystemExit(f"Too many candidates at bit {k + 1}: {len(cands)}")
        if (k + 1) % 128 == 0 or k < 20:
            print(f"    bits {k + 1}: {len(cands)} candidate(s)")

    if len(cands) != 1:
        raise SystemExit(f"Expected exactly 1 final candidate, got {len(cands)}")

    p, q = next(iter(cands))
    if p > q:
        p, q = q, p

    if p * q != n:
        raise SystemExit("Recovered factors do not multiply back to n")

    result = {
        "method": "bit_lifting_from_n_and_leak",
        "p": str(p),
        "q": str(q),
        "p_bits": p.bit_length(),
        "q_bits": q.bit_length(),
    }
    out_json.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
    out_txt.write_text(
        "\n".join([
            "[level3 factor result]",
            f"method = {result['method']}",
            f"p_bits = {p.bit_length()}",
            f"q_bits = {q.bit_length()}",
            f"p = {p}",
            f"q = {q}",
        ]) + "\n"
    )

    print(f"[OK] recovered p and q")
    print(f"[+] wrote {out_json}")
    print(f"[+] wrote {out_txt}")


if __name__ == "__main__":
    main()
plain 复制代码
method = bit_lifting_from_n_and_leak
p_bits = 1536
q_bits = 1536
p = 1707409731376314795999673399644896831773113631916193464259935767498639233398656041693151647684636824933980722820855222725690297941643517419906221825861730073489224554305559179283922396783611098752937409830400941648419260598819028494552512482509512236956144966960506482426135682006340832164470103583395459735070745893411476234089404558564875229768662461905874150673437533528191205114624917667791799816966017830043080586780620851038224028348188524334138215456616749
q = 2141573345627585371553787251648053741410445905256360451856340437573603641142767425814601119975790360922588103737873793493614076837288293103777795188569218362732735616305950754442774259470221411601014586798147206055920156749954034989669601788173864983031343224818275175942690789067112461738865540083192521838275489406927966810543155749231403489582462847315255827362973792259727757243033997062234932720733059810639405260209480382634232650958389211775287627610180991

成功分解

RSA解密

python 复制代码
#!/usr/bin/env python3
from pathlib import Path
import json
from Crypto.Util.number import long_to_bytes


def main():
    root = Path(__file__).resolve().parent
    params_json = root / "step6_level3_params.json"
    factors_json = root / "step6_level3_factor_result.json"
    out_txt = root / "step6_level3_flag.txt"

    params = json.loads(params_json.read_text())
    factors = json.loads(factors_json.read_text())

    n = int(params["n"])
    e = int(params["e"])
    c = int(params["c"])
    p = int(factors["p"])
    q = int(factors["q"])

    phi = (p - 1) * (q - 1)
    d = pow(e, -1, phi)
    m = pow(c, d, n)
    flag = long_to_bytes(m).decode("utf-8")

    out_txt.write_text(
        "\n".join([
            "[level3 flag]",
            f"phi_bits = {phi.bit_length()}",
            f"d_bits = {d.bit_length()}",
            f"flag = {flag}",
        ]) + "\n"
    )

    print(flag)
    print(f"[+] wrote {out_txt}")


if __name__ == "__main__":
    main()

FLAG

得到flag:dart{379c9308-e9a8-45a1-bd55-45bbd822e86d}

相关推荐
yv_3013 小时前
云曦26开学考复现
ctf
老星*1 天前
Vaultwarden:开源轻量的1Password替代,自托管密码管理方案
开源·github·密码学
山川绿水2 天前
bugku——MISC——键盘
安全·网络安全·系统安全·密码学
Y5neKO3 天前
某国赛CTF逆向题目Writeup:re1
python·逆向·ctf
Sunsets_Red4 天前
乘法逆元的 exgcd 求法
c++·学习·数学·算法·c#·密码学·信息学竞赛
code_li4 天前
为什么手机没网,身份验证器也能生成验证码
智能手机·密码学
Chen--Xing4 天前
2026SUCTF -- Crypto -- SULattice -- 解题记录
ctf·writeup·crypto·suctf·xctf
Y5neKO5 天前
某国赛CTF逆向题目Writeup:re2
逆向·ctf