1. 项目概述:从CTF实战到Python脚本的密码学解密之旅
如果你玩过CTF(Capture The Flag)网络安全竞赛,或者对数据安全感兴趣,那你一定绕不开RSA和AES这两个名字。它们就像是密码学世界的“倚天剑”和“屠龙刀”,一个负责非对称加密,在密钥交换和数字签名领域称王;另一个则统治着对称加密,是数据机密性的基石。在CTF的Crypto(密码学)赛题中,RSA和AES更是常客,从简单的模数分解到复杂的侧信道攻击,花样百出。但很多新手朋友一看到加密算法、数学公式和Python代码就头大,感觉无从下手。
这篇内容,就是为你准备的。我们不谈高深的理论推导,直接从CTF实战中最常见的场景出发,手把手教你如何用Python的Crypto库(更准确地说,是pycryptodome)来搞定RSA和AES的解密。我会把我在实际解题和项目开发中踩过的坑、总结的技巧,毫无保留地分享出来。无论你是想入门CTF密码学,还是需要在工作中快速实现一个加解密工具,这篇文章都能给你提供一套清晰、可复现的“操作手册”。我们的目标很明确:让你看完就能写脚本,写了就能跑通,跑通就能解出题或者完成任务。
2. 环境准备与核心库选型解析
工欲善其事,必先利其器。在开始写解密脚本之前,一个稳定、功能齐全的Python密码学环境是第一步。这里面的门道,可能比你想象的多一点。
2.1 为什么是pycryptodome而不是pycrypto?
很多老教程会推荐安装pycrypto,但我要告诉你,千万别再装这个了。pycrypto项目早在2014年就停止了维护,存在已知的安全漏洞,并且对新版Python的支持也很差。它的继任者就是pycryptodome,这是一个几乎完全兼容pycryptoAPI 的替代品,但功能更强大、维护更积极、安全性更高。在CTF和实际开发中,pycryptodome是事实上的标准。
安装命令很简单:
pip install pycryptodome如果你遇到网络问题,可以使用国内的镜像源加速,例如:
pip install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple安装成功后,在Python中通常这样导入:from Crypto.Cipher import AES和from Crypto.PublicKey import RSA。注意模块名是Crypto,这保持了与旧pycrypto的兼容性,让你迁移代码几乎无痛。
2.2 辅助工具库:让你的脚本更强大
除了核心的加密库,以下几个工具库能极大提升你脚本的效率和解决问题的能力:
gmpy2/sympy:这是RSA解题的“核武器”。当RSA的模数N不大时,你可以用sympy的factorint函数尝试分解。但当N很大,题目却给出了某些特殊条件(比如p和q很接近,或者p/q的某些位泄露)时,gmpy2这个高性能多精度算术库就是必备的。它提供了接近C语言速度的大整数运算能力,用于实现Coppersmith攻击等高级手段。安装:pip install gmpy2。如果安装困难,sympy的数学计算功能也能解决大部分中等难度的问题。requests/pwntools:用于与远程服务器交互。很多CTF题目是给你一个网络服务的地址和端口,你需要写脚本自动连接、接收数据、计算后再发送回去。requests适合HTTP/HTTPS协议,而pwntools是CTF专属神器,能处理TCP/UDP等原始套接字通信,内置了很多方便的函数。安装:pip install requests pwntools。base64/binascii:Python标准库自带的编码解码模块。题目给的密钥、密文、向量经常是Base64或十六进制(hex)格式的,你需要熟练使用base64.b64decode()、binascii.unhexlify()来转换,以及对应的编码函数将处理后的结果发送回去。
注意:在导入时,确保你导入的是正确的模块。曾经有朋友误装了
crypto(首字母小写)这个无关的包,导致代码报错找不到模块。认准Crypto(首字母大写)。
3. RSA解密实战:从基础到进阶攻击
RSA的安全性基于大整数分解的困难性。但在CTF中,出题人总会“不小心”设置一些弱点让我们利用。下面我们从最基础的场景开始,逐步深入。
3.1 场景一:已知私钥或分解(最基础)
这是最简单的场景。题目可能直接给你私钥文件(private.pem),或者给出了素数p和q。
操作步骤:
- 读取密钥或参数。如果给的是PEM格式的私钥:
如果给的是from Crypto.PublicKey import RSA with open('private.pem', 'r') as f: private_key = RSA.import_key(f.read())p,q,e,c(密文):import math p = 101 q = 113 e = 65537 c = 123456 # 示例密文 n = p * q phi = (p-1)*(q-1) # 计算私钥指数d,即e关于phi的模逆元 d = pow(e, -1, phi) # Python 3.8+ 可以直接用内置函数 # 对于更早的Python版本,可以使用扩展欧几里得算法 # from Crypto.Util.number import inverse # d = inverse(e, phi) - 执行解密。使用私钥或计算出的参数进行解密。
# 方法1:使用构建的私钥对象(如果已导入) # plaintext = private_key.decrypt(c) # 方法2:使用计算出的d直接进行模幂运算 m = pow(c, d, n) # m就是解密后的整数明文 - 转换明文。解密得到的
m通常是一个大整数,需要转换成可读的字符串。from Crypto.Util.number import long_to_bytes flag = long_to_bytes(m).decode('utf-8', errors='ignore') # 尝试UTF-8解码 print(flag)
实操心得:long_to_bytes和bytes_to_long(加密时用)是Crypto.Util.number模块里的两个宝贝函数,负责在Python大整数和字节串之间无缝转换,务必熟练掌握。
3.2 场景二:模数N分解(factordb与yafu)
当题目只给了n,e,c时,核心就是分解n得到p和q。
在线数据库查询:首先访问 factordb.com ,输入模数
n。这个网站收录了大量已知分解的整数,如果是CTF出题人从数据库里选的数,很可能直接就能查到分解结果。这是你的第一反应。本地工具分解:如果factordb查不到,就需要尝试本地分解。对于小于256位的
n(约77个十进制数字),可以尝试用sympy。import sympy n = 1234567890123456789012345678901234567890 factors = sympy.factorint(n) print(factors) # 输出如 {p1: exp1, p2: exp2...}对于更大的
n(比如512位),sympy可能会非常慢甚至卡死。这时就需要更专业的工具,比如yafu(Yet Another Factorization Utility)。它是一个命令行工具,自动化集成了多种高效的分解算法(如Pollard-rho, ECM, SIQS等)。使用方法通常是将n保存到一个文件(如num.txt),内容就是factor(你的n),然后在命令行运行yafu-x64.exe "factor(@)" -batchfile num.txt。yafu对于CTF中常见的512位、768位RSA分解效率很高。获取分解结果并解密:从
yafu或sympy的输出中得到p和q后,回到场景一的步骤计算d并解密。
踩坑记录:有时
yafu分解出的因子可能不止两个(如果n是多个素数的乘积,即Multi-prime RSA),你需要将所有素数因子乘起来验证是否等于n。私钥指数d的计算公式变为phi = (p1-1)*(p2-1)*...*(pk-1),d = inverse(e, phi)。解密公式m = pow(c, d, n)依然不变。
3.3 场景三:利用特殊漏洞(共模、低指数、广播攻击等)
当常规分解走不通时,就要考虑RSA本身的应用或参数设置是否存在漏洞。
3.3.1 共模攻击条件:相同的明文m,用相同的模数n,但不同的公钥指数e1和e2加密,得到密文c1和c2。原理:如果e1和e2互素(通常都是),根据扩展欧几里得算法,存在整数s1和s2使得e1*s1 + e2*s2 = 1。那么,m = (c1^s1 * c2^s2) mod n。Python实现:
from Crypto.Util.number import inverse, long_to_bytes import math n = ... # 公共模数 e1, c1 = ... e2, c2 = ... # 扩展欧几里得算法求系数 gcd, s1, s2 = extended_gcd(e1, e2) # 需要自己实现或使用gmpy2.gcdext # 确保s1或s2为负数时,计算对应的密文的模逆元 if s1 < 0: c1 = inverse(c1, n) s1 = -s1 if s2 < 0: c2 = inverse(c2, n) s2 = -s2 m = (pow(c1, s1, n) * pow(c2, s2, n)) % n flag = long_to_bytes(m)3.3.2 低加密指数攻击(比如e=3)条件:公钥指数e很小(如3),明文m满足m^e < n。原理:此时加密过程c = m^e没有经过模n的截断,所以直接对密文c开e次方根即可得到m。Python实现:
import gmpy2 from Crypto.Util.number import long_to_bytes c = ... # 密文 e = 3 # 使用gmpy2的iroot进行整数开方 m, is_exact = gmpy2.iroot(c, e) if is_exact: flag = long_to_bytes(int(m))注意事项:如果m^e只是略大于n,可能可以通过枚举小范围的k,尝试对c + k*n开方,即m = iroot(c + k*n, e)。
3.3.3 广播攻击条件:相同的明文m,用相同的公钥指数e(通常较小,如3),但不同的模数n1, n2, ..., nk加密,得到密文c1, c2, ..., ck。原理:根据中国剩余定理(CRT),可以构造一个方程m^e ≡ C (mod N),其中N = n1*n2*...*nk。当m^e < N时,就可以像低加密指数攻击一样直接开方。Python实现:通常使用sympy.ntheory.modular.crt函数来求解中国剩余定理,得到C,然后再对C开e次方根。
3.4 场景四:Coppersmith攻击与高位泄露
这是CTF中较难但非常经典的题型。题目可能只泄露了素数p的高位或低位,或者泄露了私钥d的一部分。核心思想是:利用已知的部分信息,构造一个关于未知部分的整数方程,然后利用Coppersmith方法在模数n下求解小根。
典型描述:“在RSA加密中,把素数 p 的高位给泄漏了。”攻击方案思路: 假设n = p * q,我们知道p的高位是pH,即p = pH + x,其中x是未知的低位部分且相对较小。 那么我们可以构造多项式f(x) = pH + x在模p下的根x0满足f(x0) ≡ 0 (mod p)。由于p是n的一个因子,所以这个等式在模n下也成立(但不一定为0)。Coppersmith定理告诉我们,如果这个根x足够小(小于n的 β次方,通常 β=0.5),我们就可以在多项式时间内找到它。
实操工具:我们通常不手写Coppersmith算法,而是使用现成的库。sage数学软件是首选,其内置的small_roots()函数非常强大。在Python环境中,我们可以用python-sage库(如果环境允许),或者使用RSAwienerHacker、owiener等专门针对RSA攻击的Python脚本中的相关实现。更常见的是,在CTF比赛中直接编写Sage脚本。
一个简化的Sage脚本示例:
# 假设在SageMath环境中运行 n = ... # 模数 pH = ... # p的高位,例如已知前200位 # 假设p是512位,已知高位200位,那么未知低位x的位数约为312位 # 我们需要构造多项式 f(x) = pH + x # p的高位需要左移到正确的位置。假设pH是已知的高位数值。 # 更常见的写法是:p_known = pH << unknown_bits kbits = 312 # 未知的低位位数 p_known = pH # 这里pH应该是已经左移了未知位数后的值,或者直接是高位数值 PR.<x> = PolynomialRing(Zmod(n)) f = x + p_known roots = f.small_roots(X=2^kbits, beta=0.4) # X是根的上界,beta通常取0.4~0.5 if roots: x0 = roots[0] p = p_known + x0 if n % p == 0: print("Found p:", p) q = n // p # 后续计算phi, d, 解密...关键点:p_known的构造需要小心。如果题目说“泄露了p的高位”,通常意味着给出了p的十进制或十六进制表示的前面一部分。你需要将其转换为整数,然后左移足够的位数(未知低位所占的比特数),使其对齐到p的完整比特长度。
4. AES解密实战:模式、填充与密钥处理
AES是一种对称加密算法,密钥长度可以是128、192或256位。在CTF中,AES的挑战往往不在于破解算法本身(目前是安全的),而在于错误的使用方式,比如模式选择不当、初始向量(IV)管理不善、填充规则被绕过等。
4.1 核心概念澄清:模式与填充
在调用Crypto.Cipher.AES.new()时,你必须指定两样东西:模式和初始化向量(如果需要)。
模式:决定了AES如何对多块数据进行加密。
- ECB:最简单的模式,每块独立加密。绝对不要用于需要保密性的场景!因为它会导致相同的明文块产生相同的密文块,图案会泄露。CTF中如果看到AES-ECB,往往提示你可以利用这个特性。
- CBC:最常用的模式之一。每个明文块在加密前会与前一个密文块进行异或操作。需要一个随机的、不可预测的IV。IV不需要保密,但必须随机且唯一。
- CTR:将块密码变为流密码。需要一个Nonce(类似IV)和计数器。它可以并行加密,且不需要填充。
- GCM、CCM:认证加密模式,同时提供机密性和完整性。在热词中看到的“aes ccm”就属于此类。
填充:AES块大小是16字节。如果明文不是16的整数倍,就需要填充。
pycryptodome默认使用PKCS#7填充。例如,一个15字节的数据,会填充1个值为\x01的字节;一个16字节的数据,会额外填充一个完整的16字节块,每个字节值为\x10。解密后,库会自动去除填充。
4.2 场景一:已知密钥和IV的CBC解密
这是最直接的场景。题目给你密钥(key)、初始向量(IV)和密文(ciphertext)。
操作步骤:
from Crypto.Cipher import AES from Crypto.Util.Padding import unpad # 用于去除填充 import base64 # 假设给定的数据是Base64编码的 key_b64 = 'AAAAAAAAAAAAAAAAAAAAAA==' # 示例,16字节的base64 iv_b64 = 'BBBBBBBBBBBBBBBBBBBBBB==' ciphertext_b64 = '...' # 解码 key = base64.b64decode(key_b64) iv = base64.b64decode(iv_b64) ciphertext = base64.b64decode(ciphertext_b64) # 创建AES解密器,模式为CBC cipher = AES.new(key, AES.MODE_CBC, iv) # 解密并去除填充 try: decrypted_padded = cipher.decrypt(ciphertext) plaintext = unpad(decrypted_padded, AES.block_size) # AES.block_size == 16 print(plaintext.decode('utf-8')) except ValueError as e: print(f"解密或解填充失败: {e}") # 可能是密钥/IV错误,或者填充不正确注意事项:
AES.new()的参数顺序是(key, mode, iv)。对于不需要IV的模式(如ECB),则省略iv参数。decrypt()方法返回的是解密后的数据,但可能还带着PKCS#7填充字节。必须使用unpad()来移除它们。- 如果
unpad()抛出ValueError,说明填充格式不对,很可能密钥或IV是错误的。
4.3 场景二:无IV或IV可推导(ECB、CTR等)
ECB模式:直接省略IV参数。
cipher = AES.new(key, AES.MODE_ECB) decrypted_padded = cipher.decrypt(ciphertext) plaintext = unpad(decrypted_padded, AES.block_size)CTF技巧:ECB模式会暴露数据模式。如果题目是加密了一张图片,你可以通过观察密文的重复块,来推断明文图片的结构,甚至替换块来篡改图片内容。
CTR模式:CTR模式需要一个
nonce(随机数)和一个计数器。pycryptodome中,你可以直接传入一个完整的initial_value,或者分别指定nonce和initial_value(此时initial_value是计数器起始值)。更常见的用法是,题目会给你一个相当于IV的东西,你可以将其作为nonce,并假设计数器从0开始。# 假设 given_iv 作为 nonce cipher = AES.new(key, AES.MODE_CTR, nonce=given_iv, initial_value=0) # CTR模式是流加密,不需要填充 plaintext = cipher.decrypt(ciphertext)重要:CTR模式下,绝对不要重复使用相同的(nonce, key)对来加密不同的消息,否则会导致流密钥重用,安全性完全丧失。CTF中有时会利用这一点。
4.4 场景三:Padding Oracle攻击(CBC模式)
这是CBC模式一个非常经典的攻击。条件:攻击者能够向一个服务提交密文,并能够得知解密后填充是否有效(例如,服务返回“解密错误”或“填充错误”)。
原理简述:攻击者可以篡改密文块的字节,利用服务返回的填充有效/无效信息,逐个字节地推导出中间状态值,从而计算出明文。这个攻击完全不需要知道密钥!
Python脚本框架: 你需要实现一个函数,能够与目标服务器交互,提交密文并判断返回是否表示“填充错误”。
import requests def is_padding_ok(ciphertext_hex): """向目标服务器发送密文,返回True表示填充正确,False表示填充错误""" url = "http://target.com/decrypt" data = {"ciphertext": ciphertext_hex} resp = requests.post(url, data=data) # 根据服务器返回信息判断,可能是状态码不同,也可能是返回内容包含特定字符串 return "padding error" not in resp.text # 然后使用Padding Oracle攻击算法(如PoC||GTFO中的经典算法)来逐字节解密。 # 这里不展开完整算法代码,但思路是:从最后一个块开始,篡改前一个密文块(或IV), # 爆破最后一个填充字节的值,利用服务器的反馈进行判断。实操心得:实现Padding Oracle攻击脚本是对你编程能力和对CBC模式理解的一次很好考验。网上有很多现成的攻击脚本(如padbuster的Python版),但理解原理后自己写一遍收获更大。关键点在于控制好字节异或的操作和服务器反馈的判断逻辑。
5. 脚本编写实战与调试技巧
把各个模块组合成一个能稳定运行的脚本,并处理好各种边界情况,才是真正的实战能力。
5.1 一个完整的CTF RSA解题脚本示例
假设题目通过网络连接给出n,e,c,我们需要分解n(假设不大),解密后把明文发回去。
#!/usr/bin/env python3 from pwn import * # 使用pwntools进行网络交互 from Crypto.Util.number import long_to_bytes, bytes_to_long import sympy # 1. 连接题目服务器 r = remote('ctf.example.com', 12345) # 2. 接收数据。格式需要根据题目调整,这里假设服务器一行行发送 n, e, c n_line = r.recvline().decode().strip() e_line = r.recvline().decode().strip() c_line = r.recvline().decode().strip() # 提取数字,可能包含前缀如"n = " n = int(n_line.split('=')[1].strip()) e = int(e_line.split('=')[1].strip()) c = int(c_line.split('=')[1].strip()) print(f"[*] Got n: {n}") print(f"[*] Got e: {e}") print(f"[*] Got c: {c}") # 3. 尝试分解n print("[*] Trying to factor n...") factors = sympy.factorint(n) if len(factors) == 2 and all(exp == 1 for exp in factors.values()): p, q = list(factors.keys()) print(f"[+] Factorization success! p={p}, q={q}") else: print("[-] Factorization failed or n has more than 2 prime factors.") exit(1) # 4. 计算私钥d并解密 phi = (p-1)*(q-1) d = pow(e, -1, phi) # Python 3.8+ m = pow(c, d, n) flag = long_to_bytes(m).decode('utf-8', errors='ignore') print(f"[+] Decrypted message: {flag}") # 5. 发送flag回服务器(如果需要) r.sendlineafter(b'Give me the flag:', flag.encode()) print(r.recvall().decode())5.2 调试与错误排查清单
写脚本不可能一次成功,以下是常见的错误和排查思路:
| 错误现象 | 可能原因 | 排查方法 |
|---|---|---|
ModuleNotFoundError: No module named 'Crypto' | pycryptodome未安装或安装不正确 | pip list检查是否安装。确认导入时是Crypto不是crypto。 |
ValueError: Data must be padded to 16 byte boundary in CBC mode | 密文长度不是16的倍数 | 检查密文解码是否正确。可能是Base64或Hex解码出错,或者传输中丢失了字符。 |
ValueError: Padding is incorrect. | 密钥、IV错误,或密文被篡改 | 确认密钥和IV的编码、长度(AES-128是16字节)。尝试用题目给的示例验证加解密流程。 |
| RSA解密得到乱码 | 解密出的整数m转换字节时不对 | 1. 尝试long_to_bytes(m).hex()看是否是flag的hex格式。2. 尝试 long_to_bytes(m)[::-1](可能字节序反了)。3. 检查解密过程是否正确,特别是 phi的计算和模逆运算。 |
| 分解n时间过长或内存溢出 | n太大(如1024位以上),或工具算法不适用 | 1. 先上 factordb 查询。 2. 检查题目是否有其他提示(如p、q接近,p-1光滑等),换用特定算法( yafu的-one或-factor选项)。3. 考虑是否不是分解思路,而是其他攻击(共模、低指数等)。 |
| 网络脚本收不到数据或卡住 | 题目交互协议理解错误 | 1. 用nc命令手动连接服务器,摸清交互流程。2. 在脚本中多加入 print或r.interactive()进行调试。3. 注意 recvuntil()和recvline()的使用,可能有多余的空行或提示符。 |
5.3 性能优化小贴士
- 大数运算用
gmpy2:涉及大量模幂运算(如pow(c, d, n))时,gmpy2比Python原生整数运算快几个数量级。 - 避免重复计算:在循环中需要重复使用的值(如
phi,d)先计算好。 - 使用
bytes而非str:加解密接口处理的是字节串。在内部逻辑中尽量使用bytes类型,减少编解码次数。 - 合理处理异常:使用
try...except包裹可能出错的部分(如网络超时、解码错误),让脚本更健壮,并给出有用的错误信息。
6. 从解题到理解:密码学的安全本质
通过以上实战,我们不仅学会了写脚本,更应该理解这些攻击为何能成功,从而在以后自己设计系统时避免犯同样的错误。
- RSA:安全核心在于
n难以分解。因此,必须使用足够长(目前建议至少2048位)且随机生成的素数p和q。任何对p、q的约束(如接近、低位相同、由特定算法生成)或对参数e、d的特殊选择(如过小),都可能引入致命漏洞。 - AES:算法本身是安全的,但模式选择和密钥管理是关键。
- 禁止使用ECB模式存储或传输敏感数据。
- CBC模式的IV必须随机且不可预测,最好使用密码学安全的随机数生成器(CSPRNG)生成。
- 密钥必须保密且足够随机。
- 认证加密模式(如GCM)是更优选择,它能同时防止密文被篡改。
最后,再分享一个我常用的测试习惯:在写出一个解密脚本后,我会先用已知答案的简单数据跑一遍,比如用脚本加密一个字符串,再用同一个脚本解密回来,确保基础流程正确。然后再去处理题目给的复杂数据。这个“自检”步骤能帮你排除掉很多低级的编码或逻辑错误,把精力集中在真正的密码学问题上。密码学实战就像解谜,工具和脚本是你的放大镜和钥匙,但对原理的深刻理解,才是照亮迷宫的那盏灯。