1. 项目概述:从一场比赛看CTF的实战魅力
最近在带一些刚接触网络安全的新人,聊起CTF(Capture The Flag,夺旗赛),很多人第一反应是“门槛高”、“看不懂”。正好,前段时间蓝桥杯的CTF选拔赛刚结束,我带着几个新人复盘了一下,发现其中几道题特别有代表性,简直就是为新手量身定做的“入门说明书”。尤其是那些涉及图片隐写和RSA加密的题目,看似神秘,其实只要掌握一点Python脚本的编写能力,就能轻松破解。这让我觉得,与其让大家对着抽象的术语发怵,不如直接上手,用一场真实的比赛作为引子,手把手地带大家走一遍从“看到题目一脸懵”到“写出脚本拿到flag”的完整过程。
CTF比赛里的Misc(杂项)和Crypto(密码学)类别,是很多新手最先接触的领域。Misc中的图片隐写,考验的是你的信息搜集和工具使用能力;而Crypto中的RSA,则是理解现代密码学基础的绝佳窗口。这两者都有一个共同点:非常适合用Python来自动化处理。Python丰富的库和简洁的语法,能让你把精力集中在解题逻辑上,而不是复杂的语法细节。通过这次对蓝桥杯CTF赛题的拆解,我希望不仅能教会你解这几道题,更能让你建立起“遇到问题 -> 分析特征 -> 寻找工具/编写脚本 -> 解决问题”的CTF基础思维模式。无论你是零基础的在校学生,还是想转行安全领域的开发者,这篇内容都将为你打开一扇实操的大门。
2. 核心思路解析:隐写与RSA的解题通法
在深入代码之前,我们必须先理清面对这类题目的通用思考路径。很多新手卡壳,不是因为不会写代码,而是不知道从何入手。CTF解题,尤其是Misc和Crypto,往往遵循一个相对固定的流程:识别题目类型、搜集或提取隐藏信息、应用对应算法或工具进行解码/解密。
对于图片隐写题,核心思路是“怀疑一切”。一张普通的图片,可能通过修改文件结构、在像素数据中嵌入信息、利用颜色通道的最低有效位(LSB)隐藏数据、甚至将压缩包、文本文件直接附加在图片文件末尾等方式来隐藏flag。我们的任务就是像侦探一样,用各种工具进行“体检”。通常的检查清单包括:用
file
命令查看真实文件类型;用
binwalk
或
foremost
工具分离可能内嵌的文件;用
strings
命令搜索可读字符串;用
exiftool
查看图片元数据;用
Stegsolve
、
zsteg
等专用工具分析LSB隐写。而Python脚本在这里的角色,往往是自动化执行这些检查中的某一项,或者处理工具输出后的进一步解码。
对于RSA密码题,核心思路是“理解参数与攻击场景”。RSA的安全性建立在“大数分解难题”上,但CTF中的RSA题目,为了考察知识点,通常会故意设置一些“不安全”的参数。解题的关键在于拿到题目给出的公钥(通常包含n和e)或加密后的密文c,然后寻找n的分解方式(例如n较小、n由两个很接近的素数生成、p或q不当重用等),从而计算出私钥d,最终解密得到明文m(也就是flag)。Python的
gmpy2
或
Crypto
库提供了大数运算和RSA计算的核心函数,我们的脚本就是根据不同的攻击场景(如模数分解、共模攻击、低加密指数攻击等),组织这些计算逻辑。
无论是哪种题型,编写Python脚本都不是第一步。第一步永远是 手动分析 ,用眼睛和基础工具去观察、去尝试。当发现某个步骤重复、繁琐,或者需要特定的数学计算时,才是Python脚本登场的时候。脚本是思维的延伸和效率的工具,而不是替代品。
3. 实战准备:构建你的Python解题环境
工欲善其事,必先利其器。在开始破解之前,我们需要一个顺手的Python环境。对于CTF解题而言,环境配置追求的是“全”和“快”,不需要像大型项目那样严谨。这里我推荐两种方案,你可以任选其一。
方案一:本地Python环境 + 虚拟环境
这是最灵活的方式。首先确保你安装了Python 3.6以上版本。我强烈建议使用
conda
或
venv
创建一个独立的虚拟环境,避免包冲突。
# 使用venv创建虚拟环境
python -m venv ctf-env
# 激活环境 (Linux/macOS)
source ctf-env/bin/activate
# 激活环境 (Windows)
ctf-env\Scripts\activate
激活后,安装核心依赖库。这些库覆盖了从二进制处理、密码学计算到图像处理的大部分需求:
pip install pycryptodome gmpy2 pillow opencv-python-headless numpy pandas
-
pycryptodome: 强大的密码学工具库,提供了标准的RSA、AES等算法的实现。 -
gmpy2: 高性能的多精度算术库,处理CTF中动辄几百位的大整数分解和模幂运算必不可少,速度比Python原生整数运算快很多。 -
pillow(PIL): Python图像处理的事实标准库,用于读取、操作图片像素数据。 -
opencv-python-headless: 另一个图像处理库,在某些特定场景(如频域分析)下可能用到,安装headless版本无需GUI界面,更轻量。 -
numpy: 科学计算基础,配合图像处理进行数组操作非常方便。 -
pandas: 不是必须,但在处理一些结构化的数据或日志时能节省大量时间。
方案二:使用预配置的CTF Docker镜像或在线环境
如果你不想折腾本地环境,或者需要在多台机器上快速开始,使用Docker是极佳选择。社区有很多维护良好的CTF专用镜像,如
pwn.red/jupyter
,里面预装了包括Python、
pwntools
、
binwalk
、
zsteg
等上百种工具。只需一条命令:
docker run -p 8888:8888 -it pwn.red/jupyter
然后在浏览器中打开对应的Jupyter Notebook链接,就能获得一个开箱即用的完整Web IDE环境。这对于新手来说,能极大降低环境配置带来的挫败感,让你直接聚焦于解题本身。
注意: 无论选择哪种方式,请务必在开始前测试核心库的导入。在Python交互环境中尝试
import Crypto, gmpy2, PIL,确保没有报错。环境问题往往是阻碍第一步的最大“隐形成本”。
4. 图片隐写实战:从“体检”到“手术”
我们假设拿到一张名为
suspect.jpg
的图片,题目提示flag就藏在其中。下面,我们按照从常规到深入的顺序,用Python脚本配合其他工具,进行一步步排查。
4.1 第一步:基础信息收集与文件分析
在动任何脚本之前,先用系统命令进行快速筛查,这能给你后续的脚本编写提供方向。
# 1. 查看文件真实类型,有时.jpg可能是.png伪装的
file suspect.jpg
# 2. 搜索文件中所有可打印字符串,flag可能以明文形式存在
strings suspect.jpg | grep -i “flag{”
# 3. 查看图片的Exif信息,摄影师注释、GPS坐标都可能藏信息
exiftool suspect.jpg
如果
file
命令显示除了JPEG image data外还有“Zip archive data”之类的信息,那很可能图片里捆绑了一个压缩包。如果
strings
找到了可疑的Base64字符串或类似
flag{
的片段,那方向就明确了。
4.2 第二步:使用Python进行自动化初步筛查
当手动检查发现线索后,我们可以用Python脚本将这个过程自动化,特别是当需要处理大量文件时。编写一个脚本
basic_check.py
:
import os
import subprocess
from pathlib import Path
def check_image(file_path):
print(f”[*] 正在检查文件: {file_path}”)
# 检查文件类型
result = subprocess.run([‘file’, file_path], capture_output=True, text=True)
print(f”文件类型: {result.stdout.strip()}”)
# 使用strings搜索flag常见格式
result = subprocess.run([‘strings’, file_path], capture_output=True, text=True)
lines = result.stdout.split(‘\n’)
for line in lines:
if ‘flag{‘ in line.lower() or ‘ctf{‘ in line.lower() or ‘key’ in line:
print(f”发现可疑字符串: {line}”)
# 这里可以添加调用exiftool的代码,如果已安装
# try:
# result = subprocess.run([‘exiftool’, file_path], capture_output=True, text=True)
# if ‘Comment’ in result.stdout:
# print(“发现注释信息:”, result.stdout)
# except FileNotFoundError:
# print(“未找到exiftool,跳过元数据检查。”)
if __name__ == ‘__main__’:
img_path = ‘suspect.jpg’
if Path(img_path).exists():
check_image(img_path)
else:
print(f”文件 {img_path} 不存在!”)
这个脚本封装了基础检查,你可以根据需要扩展,比如自动解码发现的Base64字符串。
4.3 第三步:深入像素层——LSB隐写分析与提取
如果前述方法都无效,那么flag很可能通过LSB(最低有效位)隐写术藏在图片的像素数据中。原理很简单:一个像素的RGB值每个通道是0-255,修改其最低的1个比特位(0或1),对人眼来说几乎无法察觉,但却可以用于编码信息。
我们可以用Python的PIL库来提取LSB信息。假设信息是按顺序存储在R通道的LSB中,下面是一个通用提取脚本
lsb_extract.py
:
from PIL import Image
import numpy as np
def lsb_extract(image_path, channel=0, bit_plane=0):
“””
提取指定颜色通道和位平面的LSB信息。
channel: 0=R, 1=G, 2=B
bit_plane: 0=最低位 (LSB), 1=次低位, 以此类推
“””
img = Image.open(image_path)
# 将图像转换为RGB数组
pixels = np.array(img)
# 提取指定通道的数据
channel_data = pixels[:, :, channel]
# 获取指定位平面的值 (0或1)
bit_values = (channel_data >> bit_plane) & 1
# 将二维数组展平为一维
bits = bit_values.flatten()
# 将比特位分组为字节(8位一组)
bytes_list = []
byte = 0
for i, bit in enumerate(bits):
byte = (byte << 1) | bit
if (i + 1) % 8 == 0:
bytes_list.append(byte)
byte = 0
# 将字节转换为字符,尝试解码为ASCII
extracted = bytes(bytes_list).decode(‘ascii’, errors=‘ignore’)
# 通常隐写的信息会有可读头或尾,我们打印前200个字符看看
print(f”提取结果 (通道{channel}, 位平面{bit_plane}) 预览:”)
print(extracted[:200])
# 可以尝试搜索常见标志
if ‘flag{‘ in extracted:
print(“\n[+] 发现flag!”)
start = extracted.find(‘flag{‘)
# 简单假设flag以}结束
end = extracted.find(‘}’, start) + 1
if end > start:
print(extracted[start:end])
return extracted
if __name__ == ‘__main__’:
# 尝试不同的通道和位平面组合
for ch in range(3): # R, G, B
for bp in range(2): # LSB和次低位
print(f”\n=== 尝试通道 {ch}, 位平面 {bp} ===”)
lsb_extract(‘suspect.jpg’, channel=ch, bit_plane=bp)
这个脚本会系统地尝试从RGB三个通道的最低两位提取信息。运行后,仔细观察输出中是否有可读的英文单词或
flag{
格式的字符串。
实操心得: LSB提取出的数据常常是杂乱无章的,因为信息可能不是从第一个像素开始存储的,或者采用了不同的编码(如每个字节的LSB)。这时,可以尝试修改脚本,比如从第N个像素开始读取,或者尝试不同的比特平面组合。另一个技巧是,将提取出的比特流直接保存为二进制文件,然后用
file命令检查其类型,可能会发现它其实是一个ZIP或PNG文件的开头。
4.4 第四步:文件分离与进阶隐写
如果
binwalk
分析显示图片内嵌了其他文件,我们可以用Python调用
binwalk
进行自动分离,或者手动编写代码解析文件结构。例如,有一种常见的隐写是将一个ZIP文件附加在JPG文件末尾。JPG文件以
FF D9
结束,之后的数据就是附加物。我们可以用Python脚本将其切割出来:
def extract_appended_data(jpg_path, output_path):
with open(jpg_path, ‘rb’) as f:
data = f.read()
# 查找JPG结束标记
jpg_end = data.find(b’\xff\xd9’)
if jpg_end == -1:
print(“未找到标准的JPG结束标记。”)
return False
# JPG结束标记占2字节,所以附加数据从其后开始
appended_data = data[jpg_end + 2:]
if not appended_data:
print(“未发现附加数据。”)
return False
with open(output_path, ‘wb’) as f:
f.write(appended_data)
print(f”附加数据已提取到 {output_path},文件大小:{len(appended_data)} 字节”)
# 尝试用file命令识别提取出的文件
import subprocess
subprocess.run([‘file’, output_path])
return True
extract_appended_data(‘suspect.jpg’, ‘extracted.zip’)
提取出的
extracted.zip
可能需要密码,密码有时又藏在图片的元数据或LSB中,这就形成了环环相扣的谜题。
5. RSA密码破解实战:理解数学与编写攻击脚本
RSA题目通常会给你一个
pub.key
(公钥文件)和一个
flag.enc
(加密后的文件)。你需要从公钥中提取参数,找到漏洞,计算私钥,最后解密。
5.1 第一步:提取RSA公钥参数
首先,我们需要从公钥文件中获取模数
n
和公钥指数
e
。使用Python的
Crypto
库可以很方便地做到这一点。
from Crypto.PublicKey import RSA
# 方法1:从PEM格式的公钥文件读取
with open(‘pub.key’, ‘r’) as f:
key_data = f.read()
pub_key = RSA.import_key(key_data)
n = pub_key.n
e = pub_key.e
print(f”模数 n = {n}”)
print(f”公钥指数 e = {e}”)
print(f”n的十进制位数: {len(str(n))}”) # 判断n的大小
如果公钥文件是OpenSSL生成的,可能会是
-----BEGIN PUBLIC KEY-----
格式的PEM文件。有时题目会直接给你
n
和
e
的值,那就更简单了。
5.2 第二步:判断攻击场景并分解n
这是RSA题目的核心。我们需要根据
n
的特点选择攻击方法。
场景1:n较小,可直接分解
如果
n
的十进制位数在256位以下(大约80个十进制数字以内),可以尝试用本地工具或在线网站(如factordb.com)直接分解。在Python中,我们可以用
sympy
库或
gmpy2
的
is_prime
和
next_prime
函数辅助进行简单爆破(仅适用于极小的n)。
import gmpy2
from sympy import factorint
n = 1234567890123456789012345678901234567890 # 示例,实际替换为题目n
if len(str(n)) < 80:
# 使用sympy分解(适用于中等大小的整数)
factors = factorint(n)
print(f”分解结果: {factors}”)
if len(factors) == 2 and all(p > 1 for p in factors):
p, q = list(factors.keys())
print(f”p = {p}, q = {q}”)
注意: 对于超过100位的
n,本地分解需要很长时间甚至不可行,必须寻找其他漏洞。
场景2:n由两个非常接近的素数生成——费马分解法
如果
p
和
q
很接近,那么
n
可以近似看作一个平方数。费马分解法对此非常有效。
import gmpy2
def fermat_factorization(n):
a = gmpy2.isqrt(n) + 1
b2 = a*a - n
while not gmpy2.is_square(b2):
a += 1
b2 = a*a - n
b = gmpy2.isqrt(b2)
p = a + b
q = a - b
return int(p), int(q)
n = 非常大的整数
p, q = fermat_factorization(n)
print(f”费马分解成功: p={p}, q={q}”)
print(f”验证 n == p*q: {n == p * q}”)
场景3:公钥指数e非常小(如e=3)——低加密指数攻击
如果
e
很小,并且明文
m
也很小(满足
m^e < n
),那么加密过程
c = m^e mod n
实际上就等于
m^e
(因为没有模运算溢出)。此时直接对密文
c
开
e
次方即可得到明文
m
。
import gmpy2
from Crypto.Util.number import long_to_bytes
c = 密文整数
e = 3
# 尝试对c开e次方根
m_int, is_exact = gmpy2.iroot(c, e)
if is_exact:
m = long_to_bytes(int(m_int))
print(f”低加密指数攻击成功,明文: {m}”)
场景4:相同的n,不同的e加密了同一消息——共模攻击
如果两次加密使用了相同的模数
n
但不同的公钥指数
e1
和
e2
,并且
e1
和
e2
互素,那么可以通过扩展欧几里得算法找到
x
和
y
使得
e1*x + e2*y = 1
,然后计算
m = (c1^x * c2^y) mod n
来恢复明文。
import gmpy2
from Crypto.Util.number import long_to_bytes
n = 模数
e1, c1 = 公钥指数1和密文1
e2, c2 = 公钥指数2和密文2
# 使用扩展欧几里得算法求系数
gcd, x, y = gmpy2.gcdext(e1, e2)
# 确保gcd(e1, e2) == 1
if gcd != 1:
print(“e1和e2不互素,共模攻击不适用”)
else:
# 计算明文
if x < 0:
c1 = gmpy2.invert(c1, n)
x = -x
if y < 0:
c2 = gmpy2.invert(c2, n)
y = -y
m = (pow(c1, x, n) * pow(c2, y, n)) % n
print(f”共模攻击成功,明文: {long_to_bytes(m)}”)
5.3 第三步:计算私钥并解密
一旦成功分解
n
得到
p
和
q
,计算私钥
d
就是标准流程:
-
计算
phi(n) = (p-1)*(q-1) -
计算私钥指数
d = e^(-1) mod phi(n),即e关于phi(n)的模逆元。
from Crypto.Util.number import inverse, long_to_bytes
# 假设我们已经有了 p, q, e, c (密文整数)
phi = (p - 1) * (q - 1)
# 计算私钥指数 d
d = inverse(e, phi) # 或者用 gmpy2.invert(e, phi)
print(f”私钥指数 d = {d}”)
# 解密: m = c^d mod n
m_int = pow(c, d, n)
# 将整数明文转换为字节
flag = long_to_bytes(m_int)
print(f”解密得到的flag: {flag}”)
最后得到的
flag
很可能就是最终的答案。
6. 蓝桥杯CTF赛题实例串联解析
让我们将上面的知识串联起来,模拟一个蓝桥杯CTF中可能出现的复合题型。题目描述:“一张看似普通的风景图,背后却隐藏着秘密。flag被加密了,钥匙就在图片中。”
第一步:分析图片
我们拿到
challenge.jpg
。用
file
和
binwalk
检查,发现图片末尾附加了一个ZIP文件。用前面提到的Python脚本
extract_appended_data
将其分离,得到
hidden.zip
。解压
hidden.zip
需要密码。
第二步:寻找密码
尝试用
strings
和
exiftool
查看图片,在Exif的Comment字段发现一串Base64编码的字符串:
U2FyYWggMTIz
。解码后得到
Sarah 123
。尝试用这个作为ZIP密码,成功解压。里面有两个文件:
pub_key.pem
和
encrypted.bin
。
第三步:破解RSA
用Python脚本读取
pub_key.pem
,得到
n
和
e
(假设
e=65537
)。发现
n
不大,只有120位十进制数字。我们将其提交到
factordb.com
或使用
sympy.factorint
进行分解,成功得到
p
和
q
。
from Crypto.PublicKey import RSA
from Crypto.Util.number import inverse, long_to_bytes
import gmpy2
with open(‘pub_key.pem’, ‘r’) as f:
pub_key = RSA.import_key(f.read())
n, e = pub_key.n, pub_key.e
# 假设从factordb得到分解结果
p = 1234567890123456789012345678901234567890123456789012345678901234567
q = 9876543210987654321098765432109876543210987654321098765432109876543
assert p * q == n
phi = (p-1)*(q-1)
d = inverse(e, phi)
with open(‘encrypted.bin’, ‘rb’) as f:
c = int.from_bytes(f.read(), ‘big’) # 密文是二进制大整数
m_int = pow(c, d, n)
flag = long_to_bytes(m_int)
print(flag) # 输出:flag{This_Is_The_Final_Flag}
至此,我们通过“图片隐写提取密码 -> 解压得到RSA材料 -> 分解n计算私钥 -> 解密”这一完整链条拿到了flag。
7. 常见问题与排查技巧实录
在实际操作中,你肯定会遇到各种报错和意外情况。这里记录了几个最典型的问题和我的解决思路。
问题1:
ModuleNotFoundError: No module named ‘Crypto’
这是最常见的问题。因为Python有一个历史遗留的、同名的
crypto
包(全小写)。正确的包名是
pycryptodome
。
-
解决方案
:确保使用
pip install pycryptodome安装。如果已经安装但仍有问题,尝试导入时使用from Cryptodome.PublicKey import RSA(注意Cryptodome的‘D’大写)。在某些系统上,可能需要卸载假冒的crypto包:pip uninstall crypto。
问题2:分解大整数n时,脚本卡死或无结果 本地分解大整数(如2048位)在普通计算机上是不现实的。
-
排查思路
:
-
检查n的长度
:
print(len(str(n)))。如果超过250位十进制数,基本放弃本地分解。 -
寻找特殊关系
:检查
n是否是光滑数(能被小素数整除),可以用gmpy2.gcd(n, small_prime)快速测试一批小素数。或者检查n是否是完全平方数(即p=q),但这在RSA中几乎不可能。 -
利用已知漏洞
:回忆常见的攻击场景。
n是否可能由两个非常接近的素数生成(用费马分解法尝试)?题目是否给出了n以外的其他提示,如p+q或p-q的值?有时题目会故意给出n和e,但n本身是容易分解的,考察的就是你对n大小的敏感度。 -
求助于资源
:将
n的十进制字符串复制到factordb.com查询。如果该网站也没有记录,那这道题很可能不是考分解,而是考其他攻击方式(如共模攻击、低加密指数广播攻击等),需要重新审题。
-
检查n的长度
:
问题3:LSB隐写提取出的数据全是乱码,找不到flag 这说明提取方式可能不对。信息可能不是从第一个像素开始存储的,或者存储顺序(RGB通道顺序、位平面顺序)有变化,甚至可能先经过了加密或编码。
-
排查技巧
:
- 偏移尝试 :修改提取脚本,不从像素数组的索引0开始,而是从索引100、1000等位置开始读取比特流。
-
全通道全位平面扫描
:编写一个循环脚本,自动尝试所有RGB通道组合(如只取R,只取G,只取B,R+G, R+G+B等)以及不同的位平面(0到7),并将提取出的比特流保存为文件,再用
file命令检查文件类型。有时隐藏的是一个完整的PNG或ZIP文件头。 -
检查文件头
:将提取出的原始字节(
bytes_list)的前几个字节打印出来(print(bytes_list[:10])),对照常见的文件魔数(Magic Number),例如PK(ZIP)、PNG、JFIF(JPEG)等。 -
考虑编码
:尝试将提取出的字节流用不同的编码解码(如
utf-8,latin-1),或者先进行Base64解码、ROT13解密等简单变换再看。
问题4:解密RSA后得到的明文是一串毫无意义的字节 恭喜你,私钥计算和解密过程很可能已经成功了!但明文可能不是直接的ASCII文本。
-
后续操作
:
-
转换为十六进制
:
print(flag.hex()),看是否是一个可读的十六进制字符串,或者对应着某个文件的头。 -
尝试常见编码
:除了
decode(‘utf-8’),还可以尝试decode(‘latin-1’),或者直接print(repr(flag))查看原始字节表示。 -
保存为文件
:将
flag字节内容直接写入文件,例如with open(‘output’, ‘wb’) as f: f.write(flag),然后用file output命令判断其类型。很可能解密出来的是一个图片或另一个压缩包,需要你进行下一步分析。
-
转换为十六进制
:
问题5:脚本运行一切正常,但得到的flag提交不正确
这是最令人头疼的情况。首先,请百分之百确认你提交的flag格式与题目要求一致(是
flag{...}
还是
FLAG{...}
或者别的格式?是否包含空格?)。
-
深度检查
:
-
核对每一个中间步骤
:重新计算
p和q,验证p*q == n是否严格成立。检查phi的计算是否正确。检查d的计算(d*e % phi == 1)。 -
密文是否正确
:确保你读取的密文
c是完整的、没有经过错误编码的。有时密文是十六进制字符串,需要先int(c, 16)转换;有时是Base64编码,需要先解码。 -
题目是否有陷阱
:有些题目会在最后一步进行额外的编码或哈希。例如,解密出的明文可能是
md5(real_flag),你需要对这个明文再进行一次MD5解密(查彩虹表)。仔细阅读题目描述,每一个单词都可能有提示。
-
核对每一个中间步骤
:重新计算
编写CTF解题脚本,三分靠写,七分靠调试。耐心、细致的观察和对每一个中间结果的验证,是通往正确答案的唯一路径。养成将关键中间变量打印出来检查的习惯,这能帮你节省大量时间。
327

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



