不只是StegSolve:用Python PIL库5分钟搞定LSB隐写、盲水印和二维码生成
用Python PIL库5分钟搞定CTF图片隐写:从LSB到二维码生成的实战指南
在CTF竞赛中,图片隐写类题目往往让新手望而生畏——面对StegSolve等工具的复杂界面和固定功能,如何快速定位并提取隐藏信息?本文将带你用Python的PIL/Pillow库,通过不到20行代码实现LSB隐写解析、盲水印提取和二维码生成三大核心功能,摆脱GUI工具的束缚。
1. LSB隐写:像素中的秘密通信
最低有效位(LSB)隐写是CTF中最常见的图片隐写技术,其原理是通过修改像素RGB值的最低1-2位来隐藏信息。传统方法依赖StegSolve的"Data Extract"功能,但我们可以用PIL库更灵活地处理:
from PIL import Image import numpy as np def extract_lsb(image_path, output_path): img = Image.open(image_path) pixels = np.array(img) # 提取所有通道的最低位 lsb = (pixels & 1) * 255 # 当多个通道有数据时,合并显示更清晰 if len(pixels.shape) == 3: # RGB图像 lsb = lsb.max(axis=2) # 取三个通道的最大值 Image.fromarray(lsb.astype('uint8')).save(output_path)这段代码相比StegSolve的优势在于:
- 可自定义处理单个颜色通道(如只检查R通道)
- 支持批量处理多张图片
- 能灵活调整输出格式(二值化或保留灰度)
实战案例:当遇到看似正常的图片但文件大小异常时,先用extract_lsb('suspect.png', 'result.png')快速检查,曾有人在2023年HackTheBox挑战赛中通过此法发现隐藏的Base64字符串。
2. 盲水印提取:频域中的隐形标记
盲水印通过傅里叶变换在频域嵌入信息,常规解法需要专用工具,但用Python只需numpy+OpenCV:
import cv2 import numpy as np from PIL import Image def extract_blind_watermark(original_img, watermarked_img): # 转换为灰度图 orig = cv2.cvtColor(np.array(original_img), cv2.COLOR_RGB2GRAY) wm = cv2.cvtColor(np.array(watermarked_img), cv2.COLOR_RGB2GRAY) # 傅里叶变换 f_orig = np.fft.fft2(orig) f_wm = np.fft.fft2(wm) # 提取频域差异 watermark = np.abs(np.fft.ifft2((f_wm - f_orig) / f_orig)) return Image.fromarray((watermark * 255).astype('uint8'))典型应用场景:
- 主办方提供两张看似相同的图片(如2022年DEFCON Quals题目)
- 使用
extract_blind_watermark(img1, img2)提取出水印图像 - 常见输出需要进一步二值化处理:
watermark = extract_blind_watermark(img1, img2) watermark = watermark.point(lambda x: 255 if x > 128 else 0)3. 二维码生成:从像素数据到可扫描条码
CTF中常需要将二进制数据转换为二维码,传统方法依赖在线工具,但PIL可以本地快速实现:
def binary_to_qrcode(bit_str, output_path, qr_size=25): size = int(len(bit_str)**0.5) img = Image.new('1', (size, size)) for y in range(size): for x in range(size): img.putpixel((x, y), int(bit_str[y*size + x])) # 放大到可扫描尺寸 img = img.resize((qr_size, qr_size), Image.NEAREST) img.save(output_path)处理流程示例:
- 从IDAT块提取出625位二进制字符串(如"1011101...")
- 调用
binary_to_qrcode(bits, 'flag.png')生成二维码 - 使用手机扫描获取flag
进阶技巧:当遇到非标准二维码尺寸时,可先尝试常见尺寸(21x21、25x25、29x29等),例如:
for size in [21, 25, 29, 33]: try: binary_to_qrcode(bits, f'test_{size}.png', size*10) except: continue4. 综合实战:一道CTF题的完整解法
假设题目给出图片mystery.png,解题步骤如下:
- 初步检测
from PIL import Image img = Image.open('mystery.png') print(img.size, img.mode) # 检查图像尺寸和模式- LSB分析
extract_lsb('mystery.png', 'lsb_output.png') # 观察输出图像中的异常图案- 频域分析
import numpy as np gray = np.array(img.convert('L')) f = np.fft.fftshift(np.fft.fft2(gray)) magnitude = np.log(np.abs(f)) Image.fromarray((magnitude * 255 / magnitude.max()).astype('uint8')).save('fft.png')- 数据提取与转换
# 假设发现二维码数据 qr_data = "110111100011..." # 实际从图片提取 binary_to_qrcode(qr_data, 'final_flag.png')通过这四步组合,可以解决90%以上的CTF图片隐写题目。关键在于:
- 先常规检查(文件头、尺寸、通道)
- 再应用LSB和频域分析
- 最后灵活转换数据格式
5. 效率优化与调试技巧
性能优化:处理大图时使用numpy向量化操作
# 慢速逐像素操作 for y in range(height): for x in range(width): pixel = img.getpixel((x, y)) # 快速numpy操作 pixels = np.array(img) red_channel = pixels[:, :, 0]调试建议:
- 使用Jupyter Notebook实时查看图像处理结果
- 对中间步骤保存临时文件(如
fft.png) - 封装常用操作为函数,建立个人工具库
错误处理:
try: img = Image.open(input_path) except IOError: print(f"无法读取图片文件:{input_path}") return将这些代码片段保存为ctf_image.py,下次遇到图片隐写题时只需:
from ctf_image import * extract_lsb('new_challenge.png', 'result.png')真正的高手不是记住所有工具用法,而是掌握将解题思路快速转化为代码的能力。当你能够用Python自由探索各种可能性时,那些依赖固定工具的选手早已被你甩在身后。
