CTF密码学实战:Python脚本解密RSA与AES的完整指南
2026/7/23 7:12:42 网站建设 项目流程

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 AESfrom Crypto.PublicKey import RSA。注意模块名是Crypto,这保持了与旧pycrypto的兼容性,让你迁移代码几乎无痛。

2.2 辅助工具库:让你的脚本更强大

除了核心的加密库,以下几个工具库能极大提升你脚本的效率和解决问题的能力:

  1. gmpy2/sympy这是RSA解题的“核武器”。当RSA的模数N不大时,你可以用sympyfactorint函数尝试分解。但当N很大,题目却给出了某些特殊条件(比如p和q很接近,或者p/q的某些位泄露)时,gmpy2这个高性能多精度算术库就是必备的。它提供了接近C语言速度的大整数运算能力,用于实现Coppersmith攻击等高级手段。安装:pip install gmpy2。如果安装困难,sympy的数学计算功能也能解决大部分中等难度的问题。

  2. requests/pwntools:用于与远程服务器交互。很多CTF题目是给你一个网络服务的地址和端口,你需要写脚本自动连接、接收数据、计算后再发送回去。requests适合HTTP/HTTPS协议,而pwntools是CTF专属神器,能处理TCP/UDP等原始套接字通信,内置了很多方便的函数。安装:pip install requests pwntools

  3. base64/binascii:Python标准库自带的编码解码模块。题目给的密钥、密文、向量经常是Base64或十六进制(hex)格式的,你需要熟练使用base64.b64decode()binascii.unhexlify()来转换,以及对应的编码函数将处理后的结果发送回去。

注意:在导入时,确保你导入的是正确的模块。曾经有朋友误装了crypto(首字母小写)这个无关的包,导致代码报错找不到模块。认准Crypto(首字母大写)。

3. RSA解密实战:从基础到进阶攻击

RSA的安全性基于大整数分解的困难性。但在CTF中,出题人总会“不小心”设置一些弱点让我们利用。下面我们从最基础的场景开始,逐步深入。

3.1 场景一:已知私钥或分解(最基础)

这是最简单的场景。题目可能直接给你私钥文件(private.pem),或者给出了素数pq

操作步骤:

  1. 读取密钥或参数。如果给的是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)
  2. 执行解密。使用私钥或计算出的参数进行解密。
    # 方法1:使用构建的私钥对象(如果已导入) # plaintext = private_key.decrypt(c) # 方法2:使用计算出的d直接进行模幂运算 m = pow(c, d, n) # m就是解密后的整数明文
  3. 转换明文。解密得到的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_bytesbytes_to_long(加密时用)是Crypto.Util.number模块里的两个宝贝函数,负责在Python大整数和字节串之间无缝转换,务必熟练掌握。

3.2 场景二:模数N分解(factordbyafu

当题目只给了n,e,c时,核心就是分解n得到pq

  1. 在线数据库查询:首先访问 factordb.com ,输入模数n。这个网站收录了大量已知分解的整数,如果是CTF出题人从数据库里选的数,很可能直接就能查到分解结果。这是你的第一反应

  2. 本地工具分解:如果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.txtyafu对于CTF中常见的512位、768位RSA分解效率很高。

  3. 获取分解结果并解密:从yafusympy的输出中得到pq后,回到场景一的步骤计算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,但不同的公钥指数e1e2加密,得到密文c1c2原理:如果e1e2互素(通常都是),根据扩展欧几里得算法,存在整数s1s2使得e1*s1 + e2*s2 = 1。那么,m = (c1^s1 * c2^s2) mod nPython实现

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的截断,所以直接对密文ce次方根即可得到mPython实现

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,然后再对Ce次方根。

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)。由于pn的一个因子,所以这个等式在模n下也成立(但不一定为0)。Coppersmith定理告诉我们,如果这个根x足够小(小于n的 β次方,通常 β=0.5),我们就可以在多项式时间内找到它。

实操工具:我们通常不手写Coppersmith算法,而是使用现成的库。sage数学软件是首选,其内置的small_roots()函数非常强大。在Python环境中,我们可以用python-sage库(如果环境允许),或者使用RSAwienerHackerowiener等专门针对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()时,你必须指定两样东西:模式初始化向量(如果需要)

  1. 模式:决定了AES如何对多块数据进行加密。

    • ECB:最简单的模式,每块独立加密。绝对不要用于需要保密性的场景!因为它会导致相同的明文块产生相同的密文块,图案会泄露。CTF中如果看到AES-ECB,往往提示你可以利用这个特性。
    • CBC:最常用的模式之一。每个明文块在加密前会与前一个密文块进行异或操作。需要一个随机的、不可预测的IV。IV不需要保密,但必须随机且唯一。
    • CTR:将块密码变为流密码。需要一个Nonce(类似IV)和计数器。它可以并行加密,且不需要填充。
    • GCMCCM:认证加密模式,同时提供机密性和完整性。在热词中看到的“aes ccm”就属于此类。
  2. 填充: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等)

  1. ECB模式:直接省略IV参数。

    cipher = AES.new(key, AES.MODE_ECB) decrypted_padded = cipher.decrypt(ciphertext) plaintext = unpad(decrypted_padded, AES.block_size)

    CTF技巧:ECB模式会暴露数据模式。如果题目是加密了一张图片,你可以通过观察密文的重复块,来推断明文图片的结构,甚至替换块来篡改图片内容。

  2. CTR模式:CTR模式需要一个nonce(随机数)和一个计数器。pycryptodome中,你可以直接传入一个完整的initial_value,或者分别指定nonceinitial_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. 在脚本中多加入printr.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)是更优选择,它能同时防止密文被篡改。

最后,再分享一个我常用的测试习惯:在写出一个解密脚本后,我会先用已知答案的简单数据跑一遍,比如用脚本加密一个字符串,再用同一个脚本解密回来,确保基础流程正确。然后再去处理题目给的复杂数据。这个“自检”步骤能帮你排除掉很多低级的编码或逻辑错误,把精力集中在真正的密码学问题上。密码学实战就像解谜,工具和脚本是你的放大镜和钥匙,但对原理的深刻理解,才是照亮迷宫的那盏灯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询