标签 RSA 下的文章


[CISCN 2022 东北赛区]math

题干

import gmpy2
from Crypto.Util.number import *
from flag import flag
assert flag.startswith(b"flag{")
assert flag.endswith(b"}")
message=bytes_to_long(flag)
def keygen(nbit, dbit):
if 2*dbit < nbit:
while True:
a1 = getRandomNBitInteger(dbit)
b1 = getRandomNBitInteger(nbit//2-dbit)

n1 = a1*b1+1

if isPrime(n1):
break
while True:
a2 = getRandomNBitInteger(dbit)
b2 = getRandomNBitInteger(nbit//2-dbit)

n2=a2*b2+1

n3=a1*b2+1
if isPrime(n2) and isPrime(n3):
break
while True:
a3=getRandomNBitInteger(dbit)
if gmpy2.gcd(a3,a1*b1*a2*b2)==1:
v1=(n1-1)*(n2-1)
k=(a3*inverse(a3,v1)-1)//v1
v2=k*b1+1
if isPrime(v2):
return a3,n1*n2,n3*v2
def encrypt(msg, pubkey):
return pow(msg, pubkey[0], pubkey[1])

分析

分析题干,发现取近似值后有N1约等于 a1*b1*a2*b2 ,N2约等于 a1*b2*k*b1 ,两者结构相似,且有N1/N2约等于a2/k,因此理论上可以尝试采用连分数展开来求解a2和k参数的值。

首先进行可行性分析,计算是否满足收敛的验证条件,经过验证,误差大致在 2^-5112^-512 之间,满足 2^-513 的临界条件,因此满足条件,理论上可以通过连分数展开来求解。

补充:连分数展开前提条件,即确定p/q是否一定是x的某个收敛子的判断依据。要求既约分数p/q满足

QianJianTec1768191074497.png


除了连分数展开以外,此处还可以采用LLL格基规约来求解,原理是近似的,会在后续进行展开说明

对N1/N2进行常规展开之后得到多个候选解,此时可以选用a2与k均为256bit数量级的条件进行约束,经过筛选共有两组候选解

通过上述步骤得到a2与k的值之后,下一步可以采用多种方式求解,其中我认为最易于理解的思路是构造同余关系,进行CRT约束求解。

首先根据已有的式子 phi(N1)=a1*b1*a2*b2 以及 ed-1=k*phi(N1)k*phi(N1)≡-1(mod e) 分别可以构造出 phi(N1)≡0 mod a2k*phi(N1)≡-1(mod e) 两个同余式,随后对其进行CRT求解可以得到 phi(N1)≡_phi(mod a2*e)

需要注意的是构造同余式的过程当中其中的第二个式子需要满足一定的前提条件即 gcd(a2, e) 必须为1,否则得到的将不是模a2*e的解而是模 lcm(a2, e)a2*e/gcd(a2, e) 的解。在本题当中,由于 gcd(a2, e)=1 ,可以继续进行

随后是利用已知的N1与上述CRT求出的同余关系进行N1的分解。由RSA基础公式可知 phi(N1)=N1-(n1+n2)+1 ,由于n1与n2数量级均为512bit,因而 phi(N1)-N1 的量大约为512-513bit,而 a2*e 的数量级也在512bit左右,N1为已知量,因而可以通过小范围爆破来求解n1与n2的实际值

设n1n2分别为pq,则有 p+q=N-phi(N)+1 ,验证条件此处可以设为 x^2-(p+q)*x+N=0 的判别式为完全平方数,爆破完成后即可得到pq的值,随后常规RSA解密即可

exp1

from math import gcd
from gmpy2 import isqrt
from Crypto.Util.number import *
from sympy.ntheory.modular import crt

e = 86905291018330218127760596324522274547253465551209634052618098249596388694529
N1 = 112187114035595515717020336420063560192608507634951355884730277020103272516595827630685773552014888608894587055283796519554267693654102295681730016199369580577243573496236556117934113361938190726830349853086562389955289707685145472794173966128519654167325961312446648312096211985486925702789773780669802574893
N2 = 95727255683184071257205119413595957528984743590073248708202176413951084648626277198841459757379712896901385049813671642628441940941434989886894512089336243796745883128585743868974053010151180059532129088434348142499209024860189145032192068409977856355513219728891104598071910465809354419035148873624856313067
c1 = 71281698683006229705169274763783817580572445422844810406739630520060179171191882439102256990860101502686218994669784245358102850927955191225903171777969259480990566718683951421349181856119965365618782630111357309280954558872160237158905739584091706635219142133906953305905313538806862536551652537126291478865

class ContinuedFraction():
def __init__(self, numerator, denumerator):
self.numberlist = []
self.fractionlist = []
self.GenerateNumberList(numerator, denumerator)
self.GenerateFractionList()

def GenerateNumberList(self, numerator, denumerator):
while numerator != 1:
quotient = numerator // denumerator
remainder = numerator % denumerator
self.numberlist.append(quotient)
numerator = denumerator
denumerator = remainder

def GenerateFractionList(self):
self.fractionlist.append([self.numberlist[0], 1])
for i in range(1, len(self.numberlist)):
numerator = self.numberlist[i]
denumerator = 1
for j in range(i):
temp = numerator
numerator = denumerator + numerator * self.numberlist[i - j - 1]
denumerator = temp
self.fractionlist.append([numerator, denumerator])

a = ContinuedFraction(N1, N2)
for (_, solve) in enumerate(a.fractionlist):
if solve[0].bit_length() == 256 and solve[1].bit_length() == 256:
a2, k = solve
if gcd(a2, k) == 1:
inve = inverse(k, e)
_phi = int(crt([a2, e], [0, -inve%e])[0])
M = a2*e
for i in range(-10,10):
phi = (N1//M)*M+i*M+_phi
s = N1-phi+1
delta = s*s-4*N1
if delta > 0:
t = isqrt(delta)
if t*t == delta:
p = (s+t)//2
q = (s-t)//2
d = inverse(e, (p-1)*(q-1))
m = pow(c1, d, N1)
print(long_to_bytes(m).decode())

接下来是一些其他的思路,得到a2与k的值之后,可以直接尝试直接恢复d,而不是先恢复phi(N1)再分解N1并求解d

已知有 e*d-1=k*phi(N1) ,由于 phi(N1)=a1*a2*b1*b2≡0(mod a2) ,因此 k*phi(N1)≡0(mod k*a2) ,所以有 e*d-1≡0(mod k*a2)gcd(e, a2*k)=1 ,因此可以求解e关于k*a2的逆元inva2k,此时有 d%(a2*k)=inva2k ,注意与后文d的近似值_d做好区分

另外的,与上个思路相似的,有phi(N1)与N1的大小相近,有 d=(k*phi(N1)+1)/e ,根据 phi(N1)=N1-(n1+n2)+1 ,尝试通过N1+1来近似phi(N1),此时估算的_d与实际的d值有误差x为

QianJianTec1768191034522.png


估算误差x大小,ek位数同为256bit,因而d的估计误差x的位数大致在512-513bit左右,关于x有式子 x=_d-d=_d-inva2k-k1*(a2*k)≡_d-inva2k(mod a2*k)

因此满足式子 x=(_d-inva2k)%(a2*k)+t*a2*k ,其中x的位数与a2*k的位数相近,均为512bit左右,因而搜索空间t很小,可以快速爆破t求解x。随后根据预估值_d得到d的值,随后常规RSA求解

exp2

本题中非必要的,但可以作为拓展知识进行补充,在上述的方案中,最后一步枚举t求d的过程当中如果面临搜索空间过大的问题,可以尝试用BSGS算法将时间复杂度由o(B)降到o(sqrt(B))

即在求解inva2k之后,可以通过BSGS算法来求解x,本题中,对任意底数g,有

QianJianTec1768190944164.png



将同余式左侧设为G^t,右侧设为H,成功将问题转化为一个离散对数问题,即求解t,使得 G^t≡H(mod N1)

由于涉及到离散对数求解,此处实现用python可能相对麻烦,因此可能需要切换到sagemath,其中有内置的连分数展开、求解离散对数问题、求解模逆的函数,求解的思路实现起来很方便,具体的参考pcspcs大佬的wp就够了

要注意的是这里选用的不能是常规的bsgs算法实现,而应该是特定的针对指定区间的,类似于bounded bsgs的算法,与常规的算法实现有着些许的差异,具体表现在未知数x的范围,常规的应该在ord(g)-1内,一旦超过一定的量级,就难以求解出结果,而本题中因为知道x的上界,所以不需要群阶信息,也不需要做全范围的离散对数,只用在这个由数量级推算出来的确定的小范围里搜索就可以了。具体细节不难实现,这里就不多赘述了。

对于已知a2和k求解flag的另一种思路是利用爆破高位+coppersmith来求解,此时需要判断未知量是否满足在小根阈值之内,经过验证,当前恰好卡在边界条件上,很可能不稳定甚至求不出解,可以通过爆破少量高位来增强该攻击方案的鲁棒性。

爆破少量高位的具体流程是先枚举一个高位前缀(规模很小, 2^82^12 级别就够用),然后对每个前缀,把 q=a2*b2+1 改写成已知近似值+小偏移的形式,用格方法去抓这个小量。

最后,第一步求解a2和k并非必须借助连分数展开,也有其他原理相似的办法来求解。连分数本质是在进行有理数逼近。既然要找256bit的a2和k使得 N1/N2≈a2/k ,那么理论上就可以把它写成一个整数线性组合尽量小的问题,然后用LLL直接把这组小关系挖出来。将差值视为小量,利用格基规约进行求解。

本题中的小量即为 N1*k-N2*a2 ,相对于主量级这是一个极小的数,不过这里由于不同维度的数量级不一样,需要先进行缩放,把不同维度的量级拉到同一数量级上。

相对于连分数逼近而言,利用LLL算法来求解的容错性与泛用性更高,例如对于不确定是否符合理论阈值的时候,LLL往往还能给出可用的候选,但是运算量这方面而言比连分数展开的方法要重不少。

总结

复盘了一下CISCN 2022 东北赛区 math题目,并把这题里能走通的几条路线放在同一条脉络下统一整理了一遍。主线部分强调的是先把关键参数落地,再用最稳的方式收尾:前半段通过比值逼近筛出 a2k 的候选,后半段用CRT把 phi(N1) 的同余信息拼起来,再借助 phi(N1)N1 的接近性在很小的范围内定位出正确的 p+q,用判别式验平方完成分解并解密。拓展部分则是把收尾拆成可替换的模块:可以不显式恢复 phi(N1),而是直接从 d mod (a2*k) 与近似值 _d 把误差压缩到很小的搜索区间里求出 d;当枚举空间变大时再用bounded BSGS把线性枚举换成开方级复杂度;同时也说明了第一步并不一定非要用连分数,LLL同样可以在结构相似带来的小量关系里挖出候选,而爆破少量高位配合格方法属于可行但不如主线稳的备选。整体来看,这题最有价值的地方在于可以把“结构信息→可求参数→多种收尾工具”的链条串得很清楚,后续遇到类似的结构化RSA题,基本都能沿着同样的节奏把问题拆开、把不确定性压到可控范围内解决。


第三届“数信杯”数据安全大赛数据安全个人赛初赛

数据安全

第四题

题目描述:工程师小王为了保证数据的安全存储,开发了对数据处理的程序,但这样的处理方式安全吗?分析程序功能,解密文件获取原始数据,提交第6行第2列数据。

IDA打开程序



Base64解密即可,第六行数据

MjU3MTA4NzYwNDAgNzE2ODk2MTk4ODI5MDM3NDkzIGFkNzg2MkBiNy5jb20K



答案:716896198829037493

第五题

题目描述:工程师小王认识到前面开发的程序并不能保证对数据的安全存储,现在对处理程序进行了改进,这次能行吗?分析程序功能,解密文件获取原始数据,提交第8行第2列数据。



程序re1e97e14f是64位ELF可执行文件(Linux x86-64,stripped)。通过静态分析确定程序结构:

入口点_start位于0x401190,通过__libc_start_main调用main函数。main函数位于0x401579,负责文件读写和调用核心加密函数。核心加密函数sub_401276位于0x401276,实现三阶段加密处理。

程序执行流程:读取./info_81bedab.ori → 三阶段加密 → 输出./info_81bedab.ori.en

Stage A - 位运算变换(sub_401276内,地址0x4012FE~0x401360):

Stage B - RWX自解密代码执行(sub_401276内,地址0x401360~0x401450):

关键函数调用序列:

0x401368: call malloc(0x1000) 申请4KB内存

0x401380: call mprotect(page, 0x1000, 7) 设置RWX权限

0x4013A0: 从.data段地址0x404060复制0x84(132)字节代码块到RWX页

0x4013B8: 获取密钥 state = (uint16_t)(buf + 0x339)

0x4013D0: 循环调用RWX代码处理每个字节

0x84字节代码块结构(位于.data段0x404060):

解密后的变换代码(key=0x6E72解密后):

等效变换函数:

Stage C - 循环XOR(sub_401276内,地址0x401450~0x4014A0):

解密算法:

Stage C逆操作:cipher XOR "e6911c24" → B[]

从B[0x339]=0xEB, B[0x33A]=0xAB约束搜索Stage B密钥,得到key=0x6E72

Stage B逆操作:通过逆映射表还原 → A[]

Stage A逆操作:A[i] >> 1 → plaintext[]

验证:A[0x339]=0x72, A[0x33A]=0x6E,与key一致

解密成功

答案:878052199921046799

第六题



题目给出一个SQLite数据库文件,提示在Freeblock空闲块中隐藏了自定义链表数据。首先连接数据库查询sys_config表获取三个关键配置:


入口指针格式为"物理页号:偏移",页号0x13B=315,偏移0x4F0=1264,该偏移已跳过Freeblock的4字节头部

链表节点结构为大端序,NextPage占4字节,NextOff占2字节,Len占2字节

加密方式为使用当前物理页号的大端4字节表示循环异或

SQLite文件头偏移16-17处读取页大小为4096字节。链表遍历算法:从入口页315偏移1264开始,读取8字节头部解析出NextPage、

NextOff、Len,然后读取Len字节的加密数据,用struct.pack('>I', current_page)生成4字节密钥循环XOR解密。当NextPage=0且

NextOff=0时链表结束。

解密拼接后得到完整SQL脚本:



执行该SQL查询得到container_id=CN2888991777license_plate=粤B-52816,按题目要求提取container_id和license_plate后五位数字

用下划线连接。

答案:CN2888991777_52816

第七题

题目实现了一个存在漏洞的AES-CBC加密,核心代码如下:

漏洞在于hint泄露了key与message前16字节的异或值。

对于38字节的flag,自定义padding函数会在前面添加10个\x0a字节,得到48字节。PKCS7填充后变成64字节(4块)。

消息结构为:

Block 0: \x0a*10 + flag[0:6]

Block 1: flag[6:22]

Block 2: flag[22:38]

Block 3: \x10*16

由于flag[:5]=b'flag{'已知,只需爆破flag[5]一个字符即可恢复完整key。验证方法是检查解密后最后一块是否为\x10*16。

利用CBC模式特性,后续块使用前一块密文作为IV解密,可恢复完整flag。

答案:flag{IADMIN-TOP-18880101-7634567_2025}

第八题

题目提供损坏的RSA私钥key_pri.pem和加密用户数据encrypted_users.csv。解析私钥DER编码发现大量参数被零填充,属于"变异RSA"私钥恢

复问题。

私钥ASN.1结构解析:

从dp字段提取p的部分高位(496位已知,低48位为0):

p_high =

0x9d025160d56d4971363f3cf2ce7e3e96215c50bf50d0cc606f5645de7ec113d16d931383e649bc2c7328499d219e2982b726ae41c5370

aa8000000000000

使用Coppersmith小根攻击恢复p的完整值。设p = p_high + x + y*2^496,其中x为低位未知部分(约48位),y为高位修正(约16位)。构

造格基,利用LLL算法求解满足p | n的小根。

cuso库求解代码:

恢复完整p =

0xf5e49d025160d56d4971363f3cf2ce7e3e96215c50bf50d0cc606f5645de7ec113d16d931383e649bc2c7328499d219e2982b726ae41c

5370aab851b8a28df37

验证:n mod p = 0,p为512位素数。计算q = n/p,d = e^(-1) mod (p-1)(q-1),构建RSA私钥,使用PKCS1_OAEP解密查找目标记录。

答案:294_010880753296303612_5108757973927325

第九题



答案:S20251001

第十题

题目提供了三个文件:secret_image.png(隐写图片)、multimodal_model.pth(PyTorch模型权重)和stego_hint.txt(提示文件)。

根据提示文件,隐写规则如下:

1图片的R通道中隐藏了模型输入特征

2取图片左上角前20个像素的R值,计算 R值 mod 10 得到20维特征向量

3将20维特征输入MLP模型,输出数值四舍五入取整即为flag的ASCII码

4ASCII码转换为字符得到完整flag

关键点在于"左上角前20个像素"的取法:按扫描顺序取第一列从上到下的20个像素(x=0, y=0..19)。

模型结构为3层全连接神经网络(MLP):20->64->32->27,使用ReLU激活函数。输出27维向量对应flag的27个字符。

答案:flag{A12I_shu1xin2bei_2025}

数据分析

数据应急

题目背景: 一家科技公司服务器被黑客入侵,重要文件被窃取并删除。需要根据系统自动备份的残存内容进行溯源分析。

第一题



三个文件全部导出



答案:HT-2025-001234

第二题

解压压缩包,得到一个内存镜像

非预期

直接搜索flag{



预期解



发现有C:\Program Files\TrueCrypt\TrueCrypt.exe

Elcomsoft Forensic Disk Decryptor挂载

加载内存镜像先搜索一下key

保存1.evk



成功挂载



答案:flag{33c3789a36bb61beb6d4268cd7d13038}

第三题

过滤响应200的包



最后一个包发现是exe程序



导出来看看

解包

找到一个exfiltrator_balanced.pyc

反编译

该程序是一个基于 ICMP 协议的隐蔽数据外带工具,通过将文件切分为 224 字节的数据块并伪装成 Windows Ping 流量发送。其核心安全机

制包含三个部分:
程序运行时会生成一个随机的 16 字节 CryptoSeed 和一个基于时间的 ScrambleSeed(用于乱序)。利用 PBKDF2-HMAC-SHA256 算

法,结合 CryptoSeed 和两组被混淆和硬编码的盐值(b'Bloodharbor!2024'b'Silent_Update!!!'),迭代 10,000 次分别派生出两

层加密所需的 32 字节密钥。
每个数据块首先添加 4 字节大端序长度头,然后使用 ChaCha20-Poly1305 进行内层加密(Layer 1),输出包含 Nonce、密文和 Tag。接着

对 Layer 1 的完整结果使用 AES-256-GCM 进行外层加密(Layer 2),同样输出 Nonce、密文和 Tag,确保了数据的机密性与完整性。
程序首先发送包含种子的元数据块(以 EX1L 开头)。为了对抗流量分析,它利用 ScrambleSeed 初始化随机数生成器,对所有数据块的发

送顺序进行全随机打乱(Shuffle)。发送时,每个 ICMP 包的 Payload 前部填充 16 字节的伪造 Windows Ping 数据,并使用复杂的线性同

余算法生成伪随机的 ICMP ID 和序列号,使得流量在外观和统计特征上极难被识别。



打开流量包,发现流量包含有大量 ICMP 请求,其中部包含 EX1L 标志的 Payload,确认为数据外带。提取 EX1L 元数据块,获取 Crypto

Seed 和 Scramble Seed。加密机制为双层加密:首先是 ChaCha20-Poly1305 (Layer 1,带 4 字节长度头),其次是 AES-GCM (Layer 2)。密

钥由 PBKDF2 基于 Crypto Seed 和硬编码盐值生成。数据块顺序经过混淆,需利用 Scramble Seed 初始化随机数生成器并还原顺序。编写脚

本提取 ICMP Payload,还原顺序并依次解密 Layer 2 和 Layer 1,拼接后得到一个 ZIP 文件。

解压得到100张图片



写一个脚本OCR识别18316978925

[+] 找到目标: person_086.png



答案:江苏省南京市鼓楼区西湖路200号梧桐里15栋4单元101室

数据分析

背景描述

随着维真科技(VerityTech)业务规模的不断扩张,公司积累了大量内部 PDF 文档,包括重要的研发资料、业务报告、内部制度说明等。在长期

的运作过程中,由于文档管理规范不完善、缺乏统一的安全策略,公司文档的安全风险逐渐显现。

近期,公司文档服务器遭遇未知勒索程序攻击。部分 PDF 文件被加密无法打开,系统中还出现可疑链接的文档,疑似为攻击入口。同时,安

全团队在受感染终端中提取到了一份可疑的可执行程序(exe 文件),推测为勒索程序主体。

为了定位安全漏洞、恢复文档数据、查明攻击来源,公司启动了"文档安全事件分析"专项工作。本题给出攻击样本、加密文档及疑似钓鱼

PDF 文档,请你协助安全团队进行取证分析、密钥恢复、数据解密以及钓鱼链接识别。

相关信息

公司合法域名为:

其余域名均视为可疑链接,需重点审查。

第一题

通过file命令识别en.exe为Windows PE可执行文件,使用strings发现Python相关字符串,判断为PyInstaller打包程序。使用pyinstxtractor.py

解包后,在en.exe_extracted/key/目录下发现RSA密钥对文件pr.pem和public_key.pem。读取pr.pem文件内容,定位-----BEGIN PRIVATE

KEY-----标记位置,提取其后的32位字符即为答案。

答案:MIIEvgIBADANBgkqhkiG9w0BAQEFAASC

第二题

反编译

分析解包后的pyc文件,发现加密流程采用混合加密机制:使用RSA-OAEP(SHA256)加密AES密钥并保存到store.key,使用AES-256-ECB模

式对PDF文件进行加密。解密流程为:首先使用提取的RSA私钥pr.pem通过PKCS1_OAEP(SHA256)解密store.key获取AES密钥,然后使用

AES-ECB模式解密PDF文件并去除PKCS7填充,最后计算解密后文件的MD5值。

答案:52fd2cb4fc43eceb3c79233038bb5f42

第三题

使用PyMuPDF遍历999份解密后的PDF,提取PDF Annotation超链接中的URI。合法域名为veritytech.com及其子域名*.veritytech.com,排

除.local和.internal等内部域名后,筛选出所有指向外部非法域名的钓鱼URL。同时检查URL参数中的跳转地址(redirect/next/url等参数)。

过滤掉包含非ASCII字符的无效URL,将所有钓鱼URL按字典序升序排序后用逗号拼接,计算MD5值。

答案:0a16f1ca5e8db1270074d0d39f6edeaf

数据审计

背景描述

某公司运维团队在日常安全巡检过程中发现系统存在异常访问和潜在入侵迹象。经初步技术研判,问题可能与公司用于关键服务器相关。该

服务器被确认存在一定安全风险,但目前尚无法确定攻击者是否已成功获取其中的证书信息。

为进一步排查与分析潜在的数据泄漏情况,运维团队已导出该服务器的所有网络流量文件,请根据流量包文件分析并回答以下问题。

第一题

题目提供了params.csv文件,包含500组RSA参数(id, e, p, q)。需要根据id=285的参数合成RSA私钥,然后遍历所有pcap文件,提取TLS证

书中的公钥模数n,与id=285计算得到的n(n = p * q)进行匹配,找到对应的流量包。具体步骤:首先读取params.csv获取id=285的e、p、

q参数,计算n = p * q;然后遍历pcap文件,解析其中的X.509证书,提取RSA公钥的模数n;最后比对两个n值,相等则说明该pcap使用了

id=285对应的证书。

答案:mAqY0WRHsV.pcap

第二题

需要使用params.csv中的RSA参数生成私钥,然后用私钥解密TLS加密的流量包,获取HTTP明文数据。具体步骤:首先根据e、p、q计算私

钥参数d = e^(-1) mod (p-1)(q-1),生成PEM格式私钥文件;然后遍历每个pcap文件,提取证书中的公钥模数n,匹配对应的私钥;使用

tshark配合私钥解密TLS流量,提取HTTP请求体;解密后的数据为POST请求到/api/v1/user/profile,请求体为hex编码的JSON,包含

username和phone字段;遍历所有解密结果,查找username为"王梅"的记录,获取其phone字段。

答案:13930079365

第三题

系统在请求包中设置了Authorization头部字段,使用JWT进行身份认证。JWT的payload中包含username和phone信息,而请求体中也包含

相同字段。越权访问的判断标准是:JWT中的用户信息与请求体中的用户信息不一致。具体步骤:解密所有流量包获取HTTP明文;提取

Authorization头部中的JWT token;JWT由三部分组成(header.payload.signature),对payload部分进行Base64URL解码获取JSON数

据;同时解析请求体中的hex编码JSON数据;比较JWT payload中的username和phone与请求体中的username和phone;若不一致则判定为

越权访问,统计越权账号总数。

答案:5