2023 NepnepCTF RSA_random

  1. 题目源码
  2. 分析
    1. MT19937 PRNG预测
    2. 连分数
  3. 本地复现

题目源码

from gmpy2 import next_prime, invert as inverse_mod
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
from random import getrandbits
from math import lcm
from sys import exit

global_bits = 1024

BANNER = rb"""
.--------.--------.--------.--------.--------.--------.--------.--------.--------.--------.--------.
| N.--.  | E.--.  | P.--.  | C.--.  | T.--.  | F.--.  | H.--.  | A.--.  | P.--.  | P.--.  | Y.--.  |
|  :/\:  |  (\/)  |  :():  |  :/\:  |  :/\:  |  :/\:  |  (\/)  |  :():  |  :/\:  |  :/\:  |  (\/)  |
|  :\/:  |  :\/:  |  ()()  |  (__)  |  :\/:  |  (__)  |  :\/:  |  ()()  |  :\/:  |  :\/:  |  :\/:  |
|  '--'n |  '--'e |  '--'p |  '--'c | '--'t  |  '--'f |  '--'h |  '--'a |  '--'p |  '--'p |  '--'y |
`--------`--------`--------`--------'--------`--------`--------`--------`--------`--------`--------`
"""


def generate_prime(bits: int):
    p = (getrandbits(bits - 32) << 32)
    return next_prime(p)


def generate_private_key(bits: int):
    p, q = generate_prime(bits), generate_prime(bits)
    n, phi = p * q, lcm(p-1, q-1)

    d = inverse_mod(0x10001, phi)
    privateKey = RSA.construct((int(n), int(0x10001), int(d), int(p), int(q)))
    return privateKey, p > q


if __name__ == "__main__":
    print(BANNER.decode())
    print("Welcome to the world of random RSA.")
    print("Please make your choice.")
    for _ in range(8):
        choice = input()

        if choice == '1':
            p, q = generate_prime(global_bits), generate_prime(global_bits)
            N = p*q
            d = generate_prime(global_bits-32)
            e = inverse_mod(d, (p * p - 1) * (q * q - 1))
            print(f"{int(N)}")
            print(f"{int(e)}")

        elif choice == '2':
            privateKey, signal = generate_private_key(global_bits)
            Cipher = PKCS1_v1_5.new(privateKey)
            c = (Cipher.encrypt(flag.encode()))
            print(c)
            exit()

        else:
            exit()

分析

MT19937 PRNG预测

观察choice,发现choice=1choice=2好像并没有直接的关系,但是都用到了getrandbits函数,联想到MT19937 PRNG预测

mersenne-twister-predictor

import random
from mt19937predictor import MT19937Predictor

predictor = MT19937Predictor()
for _ in range(624):
    x = random.getrandbits(32)
    predictor.setrandbits(x, 32)

assert random.getrandbits(32) == predictor.getrandbits(32)

题目一共有8次交互机会,当choice=1,pq都是getrandbits(1024-32),d是getrandbits(1024-32-32),所以前7轮可以获得(992*2+960)*7=20608bits,满足MT19937所需要的624*32=19968bits

然后根据每一轮的N、e用连分数恢复p、q、d,就可以预测了

连分数

比赛的时候没找到题目对应的论文,只找到了X-NUCA weird这个比较相似的,但是不会构造成题目e=inverse_mod(d, (p*p-1)*(q*q-1))的形式,论文提到这种形式可以找e / (N^2-9/4*N+1)的连分数

注意:题目没有输出每一轮p和q的顺序,所以需要爆破互换pq的,复现为了方便验证,手动加上一个signal值

本地复现

from sage.all import *
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
from random import getrandbits
from mt19937predictor import MT19937Predictor

flag = "NepCTF{c4e4356067fb3bedc53dde7af59beb1c}"
global_bits = 1024

def generate_prime(bits: int):
    p = (getrandbits(bits - 32) << 32)
    return int(next_prime(p))

def generate_private_key(bits: int):
    p, q = generate_prime(bits), generate_prime(bits)
    n, phi = p * q, lcm(p-1, q-1)
    d = int(inverse_mod(0x10001, phi))
    privateKey = RSA.construct((n, 0x10001, d, p, q))
    return privateKey, p > q

NN = []
ee = []
signal = []

for i in range(7):
    p, q = generate_prime(global_bits), generate_prime(global_bits)
    if p > q:
        signal.append(1)
    else:
        signal.append(0)
    N = p * q
    d = generate_prime(global_bits - 32)
    e = int(inverse_mod(d, (p * p - 1) * (q * q - 1)))
    NN.append(N)
    ee.append(e)

privateKey = generate_private_key(global_bits)[0]
Cipher = PKCS1_v1_5.new(privateKey)
c = Cipher.encrypt(flag.encode())
# print(c)

# 连分数解 pqd
def attack(N, e):
    convergents = continued_fraction(e / (N ** 2 - Integer(9) / Integer(4) * N + 1)).convergents()
    for c in convergents:
        k = c.numerator()
        d = c.denominator()
        if pow(pow(2, e, N), d, N) == 2:
            phi = (e * d - 1) // k
            p_a_q = int(sqrt(N ** 2 + 1 - phi + 2 * N))
            p_s_q = int(sqrt(N ** 2 + 1 - phi - 2 * N))
            p = (p_a_q - p_s_q) // 2
            assert N % p == 0
            q = N // p
            d = int(inverse_mod(e, (p * p - 1) * (q * q - 1)))
            return p, q, d

predictor = MT19937Predictor()

for i in range(7):
    print(i)
    p, q, d = attack(NN[i], ee[i])
    # 根据 signal 调整 pq 顺序
    if signal[i] == 0:
        if p > q:
            p, q = q, p
    else:
        if p < q:
            p, q = q, p

    predictor.setrandbits(p >> 32, 992)
    predictor.setrandbits(q >> 32, 992)
    predictor.setrandbits(d >> 32, 960)

p = int(next_prime(predictor.getrandbits(992) << 32))
q = int(next_prime(predictor.getrandbits(992) << 32))
n, phi = p * q, lcm(p-1, q-1)
d = int(inverse_mod(0x10001, phi))
privateKey = RSA.construct((n, 0x10001, d, p, q))
Cipher = PKCS1_v1_5.new(privateKey)
flag = Cipher.decrypt(c, '\x00')
print(flag)
# b'NepCTF{c4e4356067fb3bedc53dde7af59beb1c}'

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 skatexu@qq.com

文章标题:2023 NepnepCTF RSA_random

字数:868

本文作者:skateXu

发布时间:2023-10-10, 10:19:40

最后更新:2023-11-22, 16:19:52

原始链接:http://example.com/2023/10/10/2023-NepnepCTF-RSA-random/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。