1. 项目概述:一次从文件格式到非对称加密的实战演练
最近在BUUCTF平台上刷题,遇到一道来自ACTF新生赛的Crypto题目,它巧妙地将ZIP伪加密和RSA解密这两个看似不相关的知识点串联在了一起,形成了一个非常经典的“混合型”挑战。这道题不仅考察了对ZIP文件结构的理解,更是对RSA加解密原理的一次绝佳实战检验。很多刚入门密码学(Crypto)的朋友可能会觉得RSA的数学原理有些抽象,或者对ZIP伪加密只闻其名,而这道题提供了一个完美的、从文件操作到数学计算的完整闭环。今天,我就带大家从头到尾,手把手复现一遍这道题的完整解题流程,把其中涉及到的每一个技术细节、工具使用和思维过程都掰开揉碎了讲清楚。无论你是CTF新手,还是想巩固一下RSA基础,相信这篇详尽的复盘都能给你带来收获。
简单来说,这道题的典型流程是:你首先拿到一个ZIP压缩包,尝试解压时发现需要密码,初步判断是伪加密。修复伪加密后,得到一些文件,其中很可能包含RSA加解密所需的参数(如密文c、公钥指数e、模数n,有时甚至直接给出p和q)。然后,你需要运用RSA的解密公式,计算出明文,最终得到Flag。这个过程涵盖了文件分析、工具使用、数学计算和脚本编写等多个环节,是Crypto入门的一道分水岭。
2. 核心思路拆解:为什么是伪加密+RSA?
在深入动手之前,我们先花点时间理解一下出题人的思路。这道题将“ZIP伪加密”和“RSA解密”结合,绝非随意拼凑,而是有很强的教学和考察意图。
2.1 第一层:ZIP伪加密——信息隐藏与格式分析
ZIP伪加密是CTF Misc(杂项)和Crypto题目中一个非常古老的技巧,但历久弥新。它的核心在于,ZIP文件格式中有一个标志位(通常是在文件头或目录区的全局加密标志位)被手动修改,使得解压软件(如Windows资源管理器、Bandizip等)误以为这个文件被加密了,从而弹出密码输入框。但实际上,文件数据本身并没有经过任何加密算法处理,是“明文”存储的。
为什么出题人喜欢用它?
- 制造障碍,过滤新手 :它设置了一个非常直观的“门禁”,让没有相关知识的选手卡在第一步,无法获取到后续真正的挑战内容(即RSA参数)。
-
考察文件格式知识
:它要求选手了解ZIP文件的基本结构,知道如何用十六进制编辑器(如010 Editor, WinHex)或专用工具(如
zipdetails,binwalk)去查看和修改特定字节。 - 引导使用工具 :它自然而然地引入了CTF中常用的分析工具链。
在这道题里,伪加密的作用就是那个“盒子”,把真正的谜题(RSA参数)锁在里面。你需要先学会如何打开这个盒子。
2.2 第二层:RSA解密——数学原理与应用
打开“盒子”后,你得到的通常是一个文本文件,里面写着类似
n=xxxxxx, e=65537, c=yyyyyy
的内容,或者直接给出了p和q。这就是一道标准的RSA题目。
RSA题目的核心考察点:
- 参数理解 :你是否明白n(模数)、e(公钥指数)、c(密文)、d(私钥指数)、φ(n)(欧拉函数)之间的关系。
-
私钥计算
:核心是计算私钥d,公式为
d ≡ e^(-1) (mod φ(n))。其中φ(n) = (p-1)*(q-1)。这需要你理解模逆元的概念。 -
解密计算
:得到d后,解密公式为
m ≡ c^d (mod n),其中m就是整数形式的明文。 - 编码转换 :最后一步,需要将整数m转换成字节(bytes),再解码为字符串(通常是ASCII或UTF-8),才能看到Flag。
这道题将两者结合,模拟了一个简单的“数据泄露”场景:某人将加密信息(RSA密文)和密钥参数打包,并幼稚地用了伪加密来保护压缩包。作为安全研究员/CTF选手,你需要层层剥开,最终还原信息。
3. 工具与环境准备
工欲善其事,必先利其器。复现这道题,你不需要复杂的渗透环境,只需要以下几样工具,大部分都是免费且轻量的。
1. 十六进制编辑器(必选) 这是处理ZIP伪加密的核心。我强烈推荐 010 Editor ,它功能强大,有免费的评估版,而且内置了ZIP文件模板,能帮你高亮显示关键结构,非常适合新手。当然,WinHex、HxD也是不错的选择。
2. Python3 环境(必选)
RSA的解密计算几乎必然要用到Python,因为我们需要进行大数运算和模逆计算。请确保你的Python环境已安装。我们主要会用到
gmpy2
或
pycryptodome
库来处理大数。在终端或CMD中执行以下命令安装:
pip install gmpy2 pycryptodome
如果
gmpy2
安装困难(特别是在Windows上),可以尝试安装
gmpy2
的预编译轮子,或者使用
libnum
库作为替代(
pip install libnum
),它同样提供了方便的RSA计算函数。
3. ZIP伪加密修复工具(可选但推荐) 虽然手动修改十六进制是根本方法,但有一些小工具可以一键修复伪加密,能节省时间。例如:
- ZipCenOp.jar : 一个经典的Java工具,可以检测和修复伪加密。
-
在Kali Linux中
:可以使用
zipdetails查看结构,然后用binwalk -e有时能直接暴力提取(但并非总是有效)。
对于这道题的复现,我们优先使用 010 Editor手动修改 的方式,因为这是最本质、最能学到东西的方法。使用一键工具虽然快,但容易让你错过对文件格式的理解。
4. 文本编辑器/IDE 用来写Python解密脚本,比如VS Code、PyCharm,甚至系统自带的记事本都可以。
4. 第一步:破解ZIP伪加密,获取题目文件
假设我们拿到的压缩包叫
challenge.zip
。当你用WinRAR或7-Zip尝试解压时,它会提示你输入密码。
4.1 使用010 Editor分析ZIP结构
用010 Editor打开
challenge.zip
。打开后,点击菜单栏的
Templates -> Run Template
,然后选择
ZIP.bt
。这个模板会自动解析ZIP文件结构,并用不同的颜色高亮显示各个部分,让你一目了然。
一个ZIP文件主要由三部分组成:
- 本地文件头 (Local File Header) :每个被压缩文件前都有一个,包含文件名、压缩方法、CRC等。 关键字段是“通用位标记”(General purpose bit flag) ,它的第0位如果为1,表示该文件被加密。 对于伪加密,这里通常是0(未加密) 。
- 文件数据 (File Data) :压缩后的实际数据。
- 中央目录记录 (Central Directory Record) :位于文件末尾,是文件的“索引”,包含类似本地文件头的信息。 伪加密的关键就在这里! 中央目录记录里也有一个“通用位标记”。出题人通常会把 中央目录记录里的这个标记的第0位设为1 ,而 本地文件头里的对应位保持为0 。
- 目录结束标识 (End of Central Directory Record) :标记中央目录的结束。
如何识别和修复?
-
在010 Editor的模板视图下,找到
Central Directory部分,展开其中一个文件的记录。 -
找到
General purpose bit flag字段。如果它的值是0x0001,0x0009(0x0001 | 0x0008) 等,且其二进制表示的第0位是1,则说明在中央目录层面标记了加密。 -
同时,检查上方
Local File Header里同一个文件的General purpose bit flag。如果这里是0x0000或第0位是0,那就构成了“伪加密”:数据没加密(本地头说没加密),但索引说加密了(中央目录说加密了),导致解压软件索要密码。 -
修复方法
:将
中央目录记录
中的
General purpose bit flag的值修改为和 本地文件头 一致(通常是0x0000)。直接在十六进制视图找到对应字节修改即可。例如,把01 00改成00 00。
注意 :有些题目可能会把本地文件头的标记也设为加密,但数据仍是明文,这也是一种伪加密。修复原则是 让两个标记位保持一致,且确保其二进制第0位为0 。最稳妥的方法是,将本地文件头和中央目录记录的“通用位标记”都改为
0x0000。
4.2 修复后解压
保存修改后的ZIP文件。再次尝试解压,此时应该不再需要密码,可以成功解压出一个或多个文件。常见的输出是一个
flag.enc
(密文文件)和一个
pubkey.pem
或
key.txt
(包含RSA公钥或参数的文件)。
实操心得 :
- 修改前最好备份原文件。
-
如果010 Editor模板解析失败,可以手动搜索。ZIP中央目录的开始有固定标记
0x02014b50,目录结束标记是0x06054b50。找到中央目录后,每个文件记录开头后第8个字节开始的两个字节就是“通用位标记”。 -
使用
zipdetails -v challenge.zip命令也能清晰看到每个部分的标记位状态,是很好的辅助验证手段。
5. 第二步:分析RSA参数,理解题目类型
解压后,我们得到了RSA相关的文件。现在需要分析题目给了我们什么。
常见的RSA题目参数给出形式:
- 直接给出p, q, e, c :这是最简单的一种。你直接有了私钥计算所需的一切。
- 给出n, e, c :需要你对大整数n进行分解,得到p和q。如果n很小(比如小于512位),可以用网站(如factordb.com)或工具(如yafu)分解。这道新生赛题很可能属于这种或第一种。
-
给出公钥文件(.pem)和密文文件
:你需要用Python的Crypto库读取公钥,提取出n和e。命令
openssl rsa -pubin -text -modulus -in pubkey.pem也可以查看。 - 只给出n和e,但e很大或很小 :这可能考察Wiener攻击、低加密指数攻击等。
- 给出多组n和c,使用相同的明文或相同的模数 :可能考察共模攻击、广播攻击等。
对于ACTF新生赛这道题,根据常见模式,我们假设解压后得到一个
output.txt
,内容如下:
n = 123456789... (一个很大的整数)
e = 65537
c = 987654321... (另一个很大的整数)
或者更直接:
p = 1123...
q = 3345...
e = 65537
c = 9876...
我们的任务就是利用这些参数,解出明文m。
6. 第三步:编写Python脚本进行RSA解密
这是整个挑战的核心计算部分。我们将分步骤用Python实现。
6.1 情况一:已知p, q, e, c
这是最直接的情况。解密流程如下:
-
计算
n = p * q -
计算欧拉函数
φ(n) = (p-1) * (q-1) -
计算私钥指数
d,即e关于φ(n)的模逆元。满足e * d ≡ 1 (mod φ(n))。 -
计算明文
m = pow(c, d, n)。这里pow(c, d, n)是Python的内置函数,表示c^d mod n,效率极高。 -
将整数
m转换为字节串,再解码为字符串。
完整Python脚本示例:
import gmpy2
from Crypto.Util.number import long_to_bytes
# 题目给出的参数
p = gmpy2.mpz(1123...) # 替换为实际的p
q = gmpy2.mpz(3345...) # 替换为实际的q
e = 65537
c = gmpy2.mpz(9876...) # 替换为实际的c
# 1. 计算n
n = p * q
# 2. 计算φ(n)
phi = (p - 1) * (q - 1)
# 3. 计算私钥d
# 使用gmpy2.invert计算模逆元
d = gmpy2.invert(e, phi)
# 4. 解密,计算明文m
m = pow(c, d, n) # 使用pow的三参数形式进行模幂运算
# 5. 将整数m转换为字节,然后解码为字符串
try:
flag = long_to_bytes(m).decode('utf-8')
print("Flag is:", flag)
except UnicodeDecodeError:
# 有时解密出的字节不是UTF-8文本,可能是其他格式或需要进一步处理
print("Decrypted bytes (hex):", long_to_bytes(m).hex())
print("Decrypted integer m:", m)
关键点解释:
-
gmpy2.mpz(): 将整数转换为gmpy2的大整数类型,支持非常大的整数运算,比Python原生int在模幂运算上更快。 -
gmpy2.invert(a, b): 计算a关于模b的逆元,即(a * x) % b == 1中的x。 -
pow(c, d, n): Python内置的模幂运算,计算c^d mod n,即使c, d, n都非常大,也能高效计算。 -
long_to_bytes(): 来自Crypto.Util.number,将大整数转换为字节串。这是将数字转换为可读文本的关键一步。 -
decode('utf-8'): 尝试将字节串解码为UTF-8字符串。Flag通常是一个可读字符串。
6.2 情况二:已知n, e, c,需要分解n
如果题目只给了n, e, c,那么第一步就是分解n。对于新生赛题目,n通常不会太大(256-512位),可以尝试在线分解。
步骤:
- 分解n :访问 factordb.com,将n的值(十进制)粘贴进去查询。如果已有记录,它会直接返回p和q。如果n较小,你也可以用本地工具如yafu来分解。
- 得到p和q后 ,后续步骤与情况一完全相同。
脚本示例(包含分解步骤的提示):
import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long
import requests
# 题目给出的参数
n = gmpy2.mpz(123456789...) # 替换为实际的n
e = 65537
c = gmpy2.mpz(987654321...) # 替换为实际的c
# --- 第一步:分解n (这里假设我们已经从factordb得到了p和q) ---
# 假设通过查询factordb,我们得到:
p = gmpy2.mpz(实际p值)
q = gmpy2.mpz(实际q值)
# 验证一下 p * q 是否等于 n
assert p * q == n, "p*q != n, 分解可能有误!"
# --- 后续步骤与情况一相同 ---
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
try:
flag = long_to_bytes(m).decode()
print("Flag is:", flag)
except:
print("Bytes:", long_to_bytes(m))
关于分解n的注意事项:
- 如果factordb也分解不了,说明n可能比较大(比如1024位以上),这通常超出了新生赛范围,可能需要考虑其他攻击方式(如共模攻击、低加密指数攻击等),但本题大概率是能分解的。
- 在CTF中,有时n是故意构造的,比如p和q非常接近,可以使用费马分解法;或者n有多个小因子,可以使用Pollard's rho算法。但对于复现这道题,我们默认它能被factordb分解。
6.3 情况三:从公钥文件(.pem)中提取n和e
如果给的是
pubkey.pem
文件,我们需要先解析它。
方法一:使用Python的Crypto库
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes
import gmpy2
# 读取公钥文件
with open('pubkey.pem', 'r') as f:
key = RSA.import_key(f.read())
n = key.n
e = key.e
print(f"n = {n}")
print(f"e = {e}")
# 读取密文c (假设c是十六进制或十进制字符串写在文件里)
with open('ciphertext.bin', 'rb') as f: # 如果是二进制文件
c = bytes_to_long(f.read())
# 或者如果c是文本文件里的十进制数
# with open('c.txt', 'r') as f:
# c = int(f.read().strip())
# 接下来的分解和解密步骤同上...
方法二:使用OpenSSL命令(在终端中)
openssl rsa -pubin -text -modulus -in pubkey.pem
这个命令会输出模数n(以十六进制显示,前面有
Modulus=
)、指数e等信息。你需要将十六进制的Modulus转换为十进制整数供Python使用。
7. 第四步:处理解密结果,获取Flag
运行解密脚本后,你可能会直接得到Flag字符串,也可能得到一串字节。常见的情况和后续处理:
-
直接输出可读字符串
:比如
flag{this_is_a_sample_flag}。这是最理想的情况。 -
输出字节,但看起来像乱码
:尝试不同的解码方式。除了
utf-8,还可以试试ascii,latin-1。有时Flag可能包含非ASCII字符(虽然少见)。 -
输出的字节以
b'flag{开头 :这说明long_to_bytes的结果已经是字节串了,直接打印就能看到。decode()失败可能是因为Flag里含有不可解码的字符(比如花括号被错误转换?)。实际上,long_to_bytes(m)的结果如果以b'flag{'开头,直接print(long_to_bytes(m))就能看到Flag。 -
输出一个很大的整数,没有明显文本特征
:这可能意味着解密得到的
m还不是最终的Flag。有可能这个m是另一层加密的密钥,或者需要进一步处理(如将其转换为16进制再解码)。但在标准RSA解密题中,解密出的整数m直接转字节就是Flag。
一个健壮的输出处理代码段:
m_bytes = long_to_bytes(m)
# 尝试多种方式显示
print("原始字节:", m_bytes)
print("十六进制:", m_bytes.hex())
# 尝试解码为常见编码
for encoding in ['utf-8', 'ascii', 'latin-1', 'utf-16', 'utf-16le']:
try:
text = m_bytes.decode(encoding)
print(f"尝试解码为 {encoding}: {text}")
if 'flag' in text or 'FLAG' in text or '{' in text: # 常见Flag特征
print(f"\n[+] 可能的Flag (使用 {encoding}): {text}")
except UnicodeDecodeError:
pass
8. 常见问题与排查技巧实录
在复现过程中,你几乎一定会遇到一些问题。下面是我总结的一些常见坑点和解决方法。
8.1 ZIP伪加密修复后仍无法解压
- 问题 :修改了中央目录的加密标记位,但解压软件依然提示加密或报错。
-
排查
:
- 检查多个文件 :如果ZIP包里有多个文件,确保你修改了 所有 文件中央目录记录的加密标记位。010 Editor的模板视图可以帮你快速浏览所有记录。
-
检查本地文件头
:有些题目会把本地文件头的加密标记也设为1。你需要将
本地文件头
和
中央目录记录
中对应文件的加密标记位
都改为0
。在010 Editor中,找到每个文件的
Local File Header,修改其General purpose bit flag为0x0000。 -
使用修复工具交叉验证
:用
ZipCenOp.jar检测一下:java -jar ZipCenOp.jar r challenge.zip。它会报告修复了哪些位。这可以帮你确认问题所在。 - 文件损坏 :极少数情况下,文件可能本身损坏。重新下载题目文件试试。
8.2 RSA解密脚本运行报错或结果不对
-
问题 :
gmpy2.invert(e, phi)报错或计算出的d看起来不对。 -
排查 :
-
检查参数类型
:确保
p,q,e,c都使用gmpy2.mpz()转换成了大整数类型,特别是当数字非常大时。 -
验证
e和φ(n)是否互质 :RSA要求gcd(e, φ(n)) = 1。如果不互质,则e关于φ(n)的模逆元不存在。可以用gmpy2.gcd(e, phi)检查,结果必须是1。如果不是,说明题目可能不是标准RSA,或者你的p, q, e有误。 -
验证
p * q == n:如果你是从n分解得到的p和q,务必用assert p * q == n验证一下。分解网站有时会给出多个因子,你需要确认哪两个相乘等于n。 -
检查密文c是否小于n
:RSA要求密文c必须满足
0 <= c < n。如果c >= n,解密会失败。但通常题目给出的c都是正确的。 - 核对参数来源 :仔细检查你从文件中读取的数字是否正确,有没有多复制或少复制字符。特别是从十六进制转换十进制时,容易出错。
-
检查参数类型
:确保
-
问题 :解密出的
m转字节后不是可读文本。 -
排查 :
-
尝试直接打印字节的十六进制
:
print(long_to_bytes(m).hex())。看看开头是不是666c6167(“flag”的十六进制)或者7b(“{”的十六进制)。如果是,说明方向对了,可能是解码问题。 -
可能m是数字形式的Flag
:有些Flag就是纯数字,比如
flag{123456}。直接打印整数m看看。 - 可能还需要一层解密 :极少数情况下,RSA解密出的结果是一个密钥,用于解密另一个文件。但新生赛题通常一步到位。
-
检查加解密公式是否用反
:确保你是用
私钥指数d
去解密密文c。公式是
m = c^d mod n。千万不要用公钥指数e去解密。
-
尝试直接打印字节的十六进制
:
8.3 安装gmpy2库失败
-
问题
:
pip install gmpy2在Windows上安装失败,提示缺少VC++编译环境。 -
解决方案
:
-
访问
https://www.lfd.uci.edu/~gohlke/pythonlibs/#gmpy2,下载对应你Python版本和系统架构的.whl文件(例如gmpy2‑2.1.0b5‑cp39‑cp39‑win_amd64.whl对应 Python 3.9 64位)。 -
在下载目录下,运行
pip install 文件名.whl。 -
或者,使用
libnum库作为替代。安装pip install libnum,在脚本中from libnum import invmod, n2s。invmod(e, phi)对应计算模逆元,n2s(m)对应将整数转为字符串,非常方便。
-
访问
8.4 在线分解网站factordb无法分解n
- 问题 :n比较大(如768位以上),factordb没有记录。
-
解决方案
:
- 确认n的值 :再检查一遍n有没有复制错误。
- 尝试其他分解方法 :对于CTF题目,n通常有弱点。
-
使用yafu
:这是一个强大的整数分解工具。将n保存到一个文件(如
num.txt),内容就是123456789...,然后运行yafu-x64.exe "factor(@)" -batchfile num.txt。 - 检查n是否为素数 :如果n本身是素数,那这不是标准的RSA,可能是其他变种。
- 检查n是否有小因子 :可以用Python写个循环试除小素数。
- 考虑是否无需分解 :题目可能考察其他RSA攻击,比如已知n, e, d,或者e很小/很大。回顾一下题目描述和给出的文件,看有没有其他提示。
9. 完整复现流程总结与思维导图
为了让你对整个流程有一个全局的、直观的认识,我把从拿到题目到获取Flag的完整步骤和关键决策点梳理如下:
-
起点:获取题目文件
challenge.zip。 -
第一关:ZIP伪加密
- 现象 :解压索要密码。
-
工具
:010 Editor / WinHex /
zipdetails。 - 操作 :用十六进制编辑器打开,定位并修改中央目录记录(或连同本地文件头)中的“通用位标记”(General purpose bit flag),将其加密位(第0位)置0。
-
验证
:修改后能正常解压,得到内含文件(如
output.txt,pubkey.pem,cipher.bin)。
-
第二关:分析RSA参数
-
从文件读取参数
:可能是
(n, e, c),也可能是(p, q, e, c),或公钥文件。 -
关键判断
:是否给出了p和q?
- 是 :直接进入解密计算。
- 否 :只有n,需要分解。
-
从文件读取参数
:可能是
-
第三关:分解n(如需)
- 首选 :访问 factordb.com,输入n(十进制)查询。
- 备用 :使用本地工具yafu,或检查n是否为素数、有无小因子等。
- 输出 :获得素数因子p和q。
-
第四关:RSA解密计算
-
公式
:
-
φ(n) = (p-1)*(q-1) -
d = e^(-1) mod φ(n)(使用gmpy2.invert(e, phi)) -
m = c^d mod n(使用pow(c, d, n))
-
- 工具 :Python + gmpy2/pycryptodome。
-
公式
:
-
第五关:结果解析
-
转换
:
m(整数) ->long_to_bytes(m)-> 尝试.decode('utf-8')。 -
输出
:得到Flag字符串,格式通常为
flag{...}或ACTF{...}。
-
转换
:
- 终点:提交Flag 。
整个过程的思维核心是 “层层剥离” :伪加密是文件层面的简单混淆,修复它就能看到真正的密码学挑战(RSA)。而RSA解密则是标准的数学计算过程,只要按部就班代入公式,就能得到结果。这道题的精妙之处在于,它将一个简单的文件操作和一个基础的密码学算法结合,让选手在实战中同时掌握两种技能。
最后,再分享一个我个人的小技巧:在CTF比赛中,遇到ZIP伪加密,可以养成先用
zipdetails
或
binwalk -e
看一眼的习惯,有时能节省打开十六进制编辑器的时间。而对于RSA题目,一定要把得到的参数(n,e,c)先完整地复制到一个文本编辑器里核对一遍,避免因复制粘贴错误导致白忙活一场。密码学挑战,细节决定成败。


被折叠的 条评论
为什么被折叠?



