CTF实战:从ZIP伪加密到RSA解密的完整通关指南

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等)误以为这个文件被加密了,从而弹出密码输入框。但实际上,文件数据本身并没有经过任何加密算法处理,是“明文”存储的。

为什么出题人喜欢用它?

  1. 制造障碍,过滤新手 :它设置了一个非常直观的“门禁”,让没有相关知识的选手卡在第一步,无法获取到后续真正的挑战内容(即RSA参数)。
  2. 考察文件格式知识 :它要求选手了解ZIP文件的基本结构,知道如何用十六进制编辑器(如010 Editor, WinHex)或专用工具(如 zipdetails , binwalk )去查看和修改特定字节。
  3. 引导使用工具 :它自然而然地引入了CTF中常用的分析工具链。

在这道题里,伪加密的作用就是那个“盒子”,把真正的谜题(RSA参数)锁在里面。你需要先学会如何打开这个盒子。

2.2 第二层:RSA解密——数学原理与应用

打开“盒子”后,你得到的通常是一个文本文件,里面写着类似 n=xxxxxx, e=65537, c=yyyyyy 的内容,或者直接给出了p和q。这就是一道标准的RSA题目。

RSA题目的核心考察点:

  1. 参数理解 :你是否明白n(模数)、e(公钥指数)、c(密文)、d(私钥指数)、φ(n)(欧拉函数)之间的关系。
  2. 私钥计算 :核心是计算私钥d,公式为 d ≡ e^(-1) (mod φ(n)) 。其中 φ(n) = (p-1)*(q-1) 。这需要你理解模逆元的概念。
  3. 解密计算 :得到d后,解密公式为 m ≡ c^d (mod n) ,其中m就是整数形式的明文。
  4. 编码转换 :最后一步,需要将整数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文件主要由三部分组成:

  1. 本地文件头 (Local File Header) :每个被压缩文件前都有一个,包含文件名、压缩方法、CRC等。 关键字段是“通用位标记”(General purpose bit flag) ,它的第0位如果为1,表示该文件被加密。 对于伪加密,这里通常是0(未加密)
  2. 文件数据 (File Data) :压缩后的实际数据。
  3. 中央目录记录 (Central Directory Record) :位于文件末尾,是文件的“索引”,包含类似本地文件头的信息。 伪加密的关键就在这里! 中央目录记录里也有一个“通用位标记”。出题人通常会把 中央目录记录里的这个标记的第0位设为1 ,而 本地文件头里的对应位保持为0
  4. 目录结束标识 (End of Central Directory Record) :标记中央目录的结束。

如何识别和修复?

  1. 在010 Editor的模板视图下,找到 Central Directory 部分,展开其中一个文件的记录。
  2. 找到 General purpose bit flag 字段。如果它的值是 0x0001 , 0x0009 (0x0001 | 0x0008) 等,且其二进制表示的第0位是1,则说明在中央目录层面标记了加密。
  3. 同时,检查上方 Local File Header 里同一个文件的 General purpose bit flag 。如果这里是 0x0000 或第0位是0,那就构成了“伪加密”:数据没加密(本地头说没加密),但索引说加密了(中央目录说加密了),导致解压软件索要密码。
  4. 修复方法 :将 中央目录记录 中的 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题目参数给出形式:

  1. 直接给出p, q, e, c :这是最简单的一种。你直接有了私钥计算所需的一切。
  2. 给出n, e, c :需要你对大整数n进行分解,得到p和q。如果n很小(比如小于512位),可以用网站(如factordb.com)或工具(如yafu)分解。这道新生赛题很可能属于这种或第一种。
  3. 给出公钥文件(.pem)和密文文件 :你需要用Python的Crypto库读取公钥,提取出n和e。命令 openssl rsa -pubin -text -modulus -in pubkey.pem 也可以查看。
  4. 只给出n和e,但e很大或很小 :这可能考察Wiener攻击、低加密指数攻击等。
  5. 给出多组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

这是最直接的情况。解密流程如下:

  1. 计算 n = p * q
  2. 计算欧拉函数 φ(n) = (p-1) * (q-1)
  3. 计算私钥指数 d ,即 e 关于 φ(n) 的模逆元。满足 e * d ≡ 1 (mod φ(n))
  4. 计算明文 m = pow(c, d, n) 。这里 pow(c, d, n) 是Python的内置函数,表示 c^d mod n ,效率极高。
  5. 将整数 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位),可以尝试在线分解。

步骤:

  1. 分解n :访问 factordb.com,将n的值(十进制)粘贴进去查询。如果已有记录,它会直接返回p和q。如果n较小,你也可以用本地工具如yafu来分解。
  2. 得到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字符串,也可能得到一串字节。常见的情况和后续处理:

  1. 直接输出可读字符串 :比如 flag{this_is_a_sample_flag} 。这是最理想的情况。
  2. 输出字节,但看起来像乱码 :尝试不同的解码方式。除了 utf-8 ,还可以试试 ascii , latin-1 。有时Flag可能包含非ASCII字符(虽然少见)。
  3. 输出的字节以 b'flag{ 开头 :这说明 long_to_bytes 的结果已经是字节串了,直接打印就能看到。 decode() 失败可能是因为Flag里含有不可解码的字符(比如花括号被错误转换?)。实际上, long_to_bytes(m) 的结果如果以 b'flag{' 开头,直接 print(long_to_bytes(m)) 就能看到Flag。
  4. 输出一个很大的整数,没有明显文本特征 :这可能意味着解密得到的 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伪加密修复后仍无法解压

  • 问题 :修改了中央目录的加密标记位,但解压软件依然提示加密或报错。
  • 排查
    1. 检查多个文件 :如果ZIP包里有多个文件,确保你修改了 所有 文件中央目录记录的加密标记位。010 Editor的模板视图可以帮你快速浏览所有记录。
    2. 检查本地文件头 :有些题目会把本地文件头的加密标记也设为1。你需要将 本地文件头 中央目录记录 中对应文件的加密标记位 都改为0 。在010 Editor中,找到每个文件的 Local File Header ,修改其 General purpose bit flag 0x0000
    3. 使用修复工具交叉验证 :用 ZipCenOp.jar 检测一下: java -jar ZipCenOp.jar r challenge.zip 。它会报告修复了哪些位。这可以帮你确认问题所在。
    4. 文件损坏 :极少数情况下,文件可能本身损坏。重新下载题目文件试试。

8.2 RSA解密脚本运行报错或结果不对

  • 问题 gmpy2.invert(e, phi) 报错或计算出的d看起来不对。

  • 排查

    1. 检查参数类型 :确保 p , q , e , c 都使用 gmpy2.mpz() 转换成了大整数类型,特别是当数字非常大时。
    2. 验证 e φ(n) 是否互质 :RSA要求 gcd(e, φ(n)) = 1 。如果不互质,则 e 关于 φ(n) 的模逆元不存在。可以用 gmpy2.gcd(e, phi) 检查,结果必须是1。如果不是,说明题目可能不是标准RSA,或者你的p, q, e有误。
    3. 验证 p * q == n :如果你是从n分解得到的p和q,务必用 assert p * q == n 验证一下。分解网站有时会给出多个因子,你需要确认哪两个相乘等于n。
    4. 检查密文c是否小于n :RSA要求密文c必须满足 0 <= c < n 。如果c >= n,解密会失败。但通常题目给出的c都是正确的。
    5. 核对参数来源 :仔细检查你从文件中读取的数字是否正确,有没有多复制或少复制字符。特别是从十六进制转换十进制时,容易出错。
  • 问题 :解密出的 m 转字节后不是可读文本。

  • 排查

    1. 尝试直接打印字节的十六进制 print(long_to_bytes(m).hex()) 。看看开头是不是 666c6167 (“flag”的十六进制)或者 7b (“{”的十六进制)。如果是,说明方向对了,可能是解码问题。
    2. 可能m是数字形式的Flag :有些Flag就是纯数字,比如 flag{123456} 。直接打印整数m看看。
    3. 可能还需要一层解密 :极少数情况下,RSA解密出的结果是一个密钥,用于解密另一个文件。但新生赛题通常一步到位。
    4. 检查加解密公式是否用反 :确保你是用 私钥指数d 去解密密文c。公式是 m = c^d mod n 。千万不要用公钥指数e去解密。

8.3 安装gmpy2库失败

  • 问题 pip install gmpy2 在Windows上安装失败,提示缺少VC++编译环境。
  • 解决方案
    1. 访问 https://www.lfd.uci.edu/~gohlke/pythonlibs/#gmpy2 ,下载对应你Python版本和系统架构的 .whl 文件(例如 gmpy2‑2.1.0b5‑cp39‑cp39‑win_amd64.whl 对应 Python 3.9 64位)。
    2. 在下载目录下,运行 pip install 文件名.whl
    3. 或者,使用 libnum 库作为替代。安装 pip install libnum ,在脚本中 from libnum import invmod, n2s invmod(e, phi) 对应计算模逆元, n2s(m) 对应将整数转为字符串,非常方便。

8.4 在线分解网站factordb无法分解n

  • 问题 :n比较大(如768位以上),factordb没有记录。
  • 解决方案
    1. 确认n的值 :再检查一遍n有没有复制错误。
    2. 尝试其他分解方法 :对于CTF题目,n通常有弱点。
    • 使用yafu :这是一个强大的整数分解工具。将n保存到一个文件(如 num.txt ),内容就是 123456789... ,然后运行 yafu-x64.exe "factor(@)" -batchfile num.txt
    • 检查n是否为素数 :如果n本身是素数,那这不是标准的RSA,可能是其他变种。
    • 检查n是否有小因子 :可以用Python写个循环试除小素数。
    1. 考虑是否无需分解 :题目可能考察其他RSA攻击,比如已知n, e, d,或者e很小/很大。回顾一下题目描述和给出的文件,看有没有其他提示。

9. 完整复现流程总结与思维导图

为了让你对整个流程有一个全局的、直观的认识,我把从拿到题目到获取Flag的完整步骤和关键决策点梳理如下:

  1. 起点:获取题目文件 challenge.zip
  2. 第一关:ZIP伪加密
    • 现象 :解压索要密码。
    • 工具 :010 Editor / WinHex / zipdetails
    • 操作 :用十六进制编辑器打开,定位并修改中央目录记录(或连同本地文件头)中的“通用位标记”(General purpose bit flag),将其加密位(第0位)置0。
    • 验证 :修改后能正常解压,得到内含文件(如 output.txt , pubkey.pem , cipher.bin )。
  3. 第二关:分析RSA参数
    • 从文件读取参数 :可能是 (n, e, c) ,也可能是 (p, q, e, c) ,或公钥文件。
    • 关键判断 :是否给出了p和q?
      • :直接进入解密计算。
      • :只有n,需要分解。
  4. 第三关:分解n(如需)
    • 首选 :访问 factordb.com,输入n(十进制)查询。
    • 备用 :使用本地工具yafu,或检查n是否为素数、有无小因子等。
    • 输出 :获得素数因子p和q。
  5. 第四关: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。
  6. 第五关:结果解析
    • 转换 m (整数) -> long_to_bytes(m) -> 尝试 .decode('utf-8')
    • 输出 :得到Flag字符串,格式通常为 flag{...} ACTF{...}
  7. 终点:提交Flag

整个过程的思维核心是 “层层剥离” :伪加密是文件层面的简单混淆,修复它就能看到真正的密码学挑战(RSA)。而RSA解密则是标准的数学计算过程,只要按部就班代入公式,就能得到结果。这道题的精妙之处在于,它将一个简单的文件操作和一个基础的密码学算法结合,让选手在实战中同时掌握两种技能。

最后,再分享一个我个人的小技巧:在CTF比赛中,遇到ZIP伪加密,可以养成先用 zipdetails binwalk -e 看一眼的习惯,有时能节省打开十六进制编辑器的时间。而对于RSA题目,一定要把得到的参数(n,e,c)先完整地复制到一个文本编辑器里核对一遍,避免因复制粘贴错误导致白忙活一场。密码学挑战,细节决定成败。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值