Skip to content

Latest commit

 

History

History
104 lines (67 loc) · 6.13 KB

File metadata and controls

104 lines (67 loc) · 6.13 KB

Z 同学的 RSA

坊间传言 Jeff Dean 在面试 Google 的时候,曾根据公钥心算出 Google 私钥。当然大多数人都把这事当作笑话对待,但只有 Z 同学知道,这是真的。

根据国家计生委未公开的大数据统计,平均每 66666666 位少年少女中就有一位这样的天才,心算能力极为恐怖,可以在线性复杂度内分解任意长度的大整数,因为时间瓶颈主要在于把结果用笔写出来。中国科学技术大学少年班学院成立的主要目的,其实就是为了网罗这样的神童。每当这样的一位天才出现,国家就会将其秘密保护起来,送到国家高性能计算中心作为 ALU,而其同学却只是被告知出国。实际上,曾排 TOP500 榜单第一名的超算“神威-太湖之光”的主要算力其实是由两位这样的天才少年提供,剩余的 10649600 个核心则负责将通用的计算程序规约到大整数分解,以及处理输入输出等等外围工作。

后来,少年班学院的“冰球”,Z 同学的一个好朋友,在离开科大去北大做相关秘密研究之际,告诉了 Z 同学这个消息。Z 同学马上意识到 RSA 实际上并不安全,惊慌失措,立刻删掉了自己所有服务器上面的 RSA 公钥,大喊:“不能再公开 RSA 中的 n 了!我们必须立即以一种新的方式使用 RSA!”。在连续几个通宵的苦思冥想后,Z 同学写了一段 python 代码,用他改进过的 RSA 算法加密了一段消息。新的算法并没有透露 n,只给定了两个大整数:(p*q)^(p+q) 和 (p*q)^(p-q),其中 ^ 是按位异或运算。

“终于能睡个安稳觉了”,Z 同学如是想。在上床之前,Z 同学告诉你,除非这个新的算法有漏洞,否则不要打扰他的休眠。而喜欢恶作剧的你,能找到正当理由叫醒正在熟睡的 Z 同学吗?

打开题目


我出这道题的灵感来自于某个国际比赛的一道 RSA 题目,那道题目的解法也是逐位爆破,具体的题目找不到了,欢迎知道的同学告诉我是哪题。

RSA 基本知识

请参见 RSA 基本介绍 - CTF Wiki

分析

题目代码很简单,除去空行,连 10 行都不到

import sympy

p = sympy.randprime(2 ** 1023, 2 ** 1024)
q = sympy.randprime(2 ** 1023, 2 ** 1024)

a = (p * q) ^ (p + q)
b = (p * q) ^ (p - q)

flag = open('flag.txt', 'rb').read()
m = int.from_bytes(flag, 'big')

print(a, b, pow(m, 65537, p * q))

代码中随机生成两个大素数 pq,然后计算出 a = (p * q) ^ (p + q)b = (p * q) ^ (p - q),其中 ^ 是按位异或运算。然后,读取 flag 文件的内容并且转换成一个大整数 m,再输出 ab 和用参数 n = p * q, e = 65537 的 RSA 加密后的 flag。

要解密 flag,我们只能求出 pq,然后算出 RSA 的私钥。可是,因为异或运算的存在,我们从 ab 很难用数学推导的方法来解出 pq

解答

解法 1

根据题目描述

根据国家计生委未公开的大数据统计,平均每 66666666 位少年少女中就有一位这样的天才,心算能力极为恐怖,可以在线性复杂度内分解任意长度的大整数,因为时间瓶颈主要在于把结果用笔写出来。中国科学技术大学少年班学院成立的主要目的,其实就是为了网罗这样的神童。

我们去少年班学院找出一位这样的天才,然后让他/她心算求解即可得到 flag。

解法 2

我们定义 f1(x, y) = (x * y) ^ (x + y)f2(x, y) = (x * y) ^ (x - y),我们发现这两个函数都有一个共同的性质,就是函数值的最低 n 个二进制位只和 xy 的最低 n 个二进制位有关。也就是说,我们可以用 ab 的最低 n 位来判断 pq 的最低 n 位是否可能正确。如果它们的最低 n 位满足 f1f2 函数,那么它们就是 pq 低位的候选答案;如果不满足,它们就根本不可能是真正 pq 的低位。

所以我们可以从一个二进制位(n=1)开始,每次增加一位。每增加一位时,我们把原来满足条件的 pq 低位的每种可能情况分别在前面加上 0 或 1,这样每种情况就变成了 4 种新的情况,然后对所有新的情况用 f1f2 函数提供的约束条件进行过滤,只保留满足条件的情况。当跑到 1024 位的时候,就只会剩下真正满足条件的 pq 了。

然后,我们根据 RSA 的原理,在 mod (p-1)*(q-1) 的意义下对 e 求逆元,得到私钥 d,计算 pow(c, d, p * q) 即可得到 flag 的大整数表示。

这个问题的巧妙之处在于,每增加一位,候选答案的数量会变成 4 倍,但同时根据数学期望,正好会有 1/4 的候选答案被过滤后保留下来,所以程序的运行时间不会指数爆炸。

求解脚本如下:

#!/usr/bin/env python3

import gmpy

a, b, c = [int(s) for s in open('output.txt').read().split()]

f1 = lambda p, q: (p * q) ^ (p + q)
f2 = lambda p, q: (p * q) ^ (p - q)

candidates = {(0, 0)}

for m in range(1025):
    print(m, len(candidates))
    candidates_ = set()
    mask = (2 << m) - 1
    for x, y in candidates:
        if f1(x, y) == a and f2(x, y) == b:
            p, q = x, y
            d = gmpy.invert(65537, (p - 1) * (q - 1))
            m = pow(c, d, p * q)
            print(bytes.fromhex(hex(m)[2:]))
            exit()
        for bx in range(2):
            for by in range(2):
                xx = x + (bx << m)
                yy = y + (by << m)
                if f1(xx, yy) & mask != a & mask:
                    continue
                if f2(xx, yy) & mask != b & mask:
                    continue
                candidates_.add((xx, yy))
    candidates = candidates_

最后吐槽一下 github 的 markdown 不能加 latex 数学公式。

彩蛋

题目中的 pq 真的是用随机数生成出来的吗?Z 同学会不会在里面隐藏了什么信息?不要问我,我不知道