CTF实战:从二进制到二维码的底层生成与自动化脚本解析

📅 2026/8/3 15:38:06
CTF实战:从二进制到二维码的底层生成与自动化脚本解析
1. 从“0和1的故事”到CTF实战一次关于二维码的深度解构最近在整理CTFCapture The Flag的MISC杂项类题目时遇到一个挺有意思的案例题目名字叫“0和1的故事”核心是围绕二维码生成。乍一看这题可能被归类为简单的隐写或编码转换但实际深入下去你会发现它巧妙地串联了二进制数据处理、图像生成原理以及自动化脚本编写等多个知识点。对于刚接触CTF的新手或者想深入理解数据如何在不同形态间转换的朋友来说这是一个绝佳的练手项目。它不单单是让你“做出一个二维码”而是让你理解从一串最原始的“0”和“1”开始到最终能被手机摄像头识别的图像这中间究竟发生了什么。今天我就结合这个题目把二维码生成的底层逻辑、在CTF中常见的考察方式以及如何用Python和Excel这类看似普通的工具玩出花样从头到尾捋一遍。2. 理解“0和1的故事”二进制数据与二维码的本质这道题的核心提示“0和1的故事”直指计算机世界的基石——二进制。在CTF的MISC题目中这通常意味着你拿到手的初始数据可能就是一段由纯“0”和“1”字符组成的文本。例如题目附件可能是一个名为flag.txt的文件里面密密麻麻地写满了“0100110101101001...”这样的字符串。2.1 二进制字符串的常见编码与转换拿到这样一串二进制字符第一步是理解它的组织方式。它可能直接代表的是ASCII或UTF-8编码的文本。标准的转换方法是每8位一个字节二进制数对应一个字符。例如“01001000”转换为十进制是72在ASCII码中对应大写字母“H”。在Python中我们可以轻松完成这个转换binary_str 0100100001100101011011000110110001101111 # 示例Hello # 确保长度是8的倍数 if len(binary_str) % 8 ! 0: print(二进制字符串长度不是8的倍数可能需要检查填充或编码方式。) # 有时题目会故意去掉前导零需要手动补位这是一个常见坑点。 # 按8位分割并转换 n 8 bytes_list [binary_str[i:in] for i in range(0, len(binary_str), n)] text .join(chr(int(byte, 2)) for byte in bytes_list) print(f解码后的文本: {text})但“0和1的故事”往往没这么简单。这些二进制位可能不是直接对应文本而是图像像素数据每个“1”代表一个黑色像素或白色“0”代表白色像素或黑色。直接按照特定宽度和高度排列就能生成一张二值图像Bitmap而这张图像可能就是二维码。压缩或编码后的数据可能是经过Base64、莫尔斯电码等其他编码后的二进制表示需要先按二进制理解再二次解码。二维码的编码区域数据直接对应二维码编码矩阵中数据模块和纠错模块的值排除了定位图形、格式信息等固定部分。这就需要你了解二维码的物理结构。2.2 二维码的物理结构不只是黑白方块一个标准的QR码Quick Response Code是一个二维矩阵由黑白方块模块组成。但它的结构是高度标准化的并非随意排列。主要部分包括位置探测图形三个角上的“回”字形大方块用于快速定位和旋转校正。分隔符位置探测图形周围的一圈白色区域。定位图形水平和垂直的两条黑白相间的虚线用于辅助模块坐标定位。格式信息存储纠错等级和掩模模式。版本信息版本7以上标识二维码的版本大小。数据和纠错码字核心区域存储实际编码的信息和用于纠错的冗余数据。剩余位填充空白区域。在CTF题目中出题人有时会给你最“纯净”的数据——即仅包含“数据和纠错码字”区域的二进制流甚至可能已经应用了掩模。你的任务就是根据给定的二进制流反向构建出完整的、包含所有必要结构的二维码图像使其能够被标准解码器识别。注意直接画图时一个极易忽略的细节是模块Module的大小。在图像中一个“1”或“0”通常对应图像中的一个像素点。但为了确保扫描成功率生成的二维码图像中每个模块最好由多个像素例如10x10像素组成否则可能因为模块太小或边缘模糊而无法识别。这是从“数据”到“可用图像”的关键一步。3. 工具化实现从Python脚本到Excel的奇思妙想理解了原理接下来就是实操。我们有多种工具可以将二进制流变为二维码图片。3.1 使用Python和qrcode库进行标准生成如果二进制流已经是经过正确编码的、可供二维码编码器使用的数据例如一段文本的URL编码那么使用成熟的库是最快的方式。import qrcode # 假设我们解码二进制后得到的文本是某个URL或Flag data flag{This_is_a_sample_flag} qr qrcode.QRCode( version1, # 版本号控制大小1最小40最大可设为None自动确定 error_correctionqrcode.constants.ERROR_CORRECT_L, # 纠错等级 L/M/Q/H box_size10, # 每个模块的像素数 border4, # 边框包含的模块数最小为4 ) qr.add_data(data) qr.make(fitTrue) img qr.make_image(fill_colorblack, back_colorwhite) img.save(standard_qr.png) print(标准二维码已生成。)但“0和1的故事”题目往往需要你从更底层做起。3.2 手动构建二维码图像PIL库的像素级操作当题目给的是直接的像素数据时我们需要手动绘制。使用Python的PILPillow库是首选。from PIL import Image def binary_to_qr_image(binary_str, width, height, module_size10): 将二进制字符串转换为二维码图像。 :param binary_str: 二进制字符串如101010...长度应为width*height。 :param width: 二维码矩阵的逻辑宽度模块数。 :param height: 二维码矩阵的逻辑高度模块数。 :param module_size: 图像中每个模块的像素大小。 :return: PIL Image对象。 if len(binary_str) ! width * height: raise ValueError(f二进制字符串长度{len(binary_str)}与指定的图像尺寸{width}x{height}{width*height}不匹配) # 创建新图像放大module_size倍 img_width width * module_size img_height height * module_size # 初始化为白色背景 img Image.new(1, (img_width, img_height), 1) # 1模式为1位像素黑白0为黑1为白 for y in range(height): for x in range(width): index y * width x pixel_value 0 if binary_str[index] 1 else 1 # 假设1为黑 # 填充该模块对应的所有像素块 for i in range(module_size): for j in range(module_size): img.putpixel((x * module_size i, y * module_size j), pixel_value) return img # 示例假设我们知道一个5x5的二维码数据且‘1’代表黑 binary_data 1111110001101011111 # 长度需为25此处为示例实际需补全 width height 5 img binary_to_qr_image(binary_data, width, height, module_size20) img.save(manual_qr.png) img.show()关键点尺寸判断width和height是关键参数。有时题目会直接给出有时需要你根据二进制字符串长度去推测例如长度是某个完全平方数。颜色映射务必确认“1”和“0”哪个代表黑色。通常“1”代表深色黑“0”代表浅色白但有时会反过来。如果生成的二维码扫不出来可以尝试反转颜色pixel_value 1 if binary_str[index] 1 else 0。模块大小module_size不能太小否则识别困难。一般不小于5。3.3 利用Excel进行可视化分析与辅助生成你可能会好奇Excel和二维码有什么关系在CTF解题中Excel是一个强大的数据分析和可视化辅助工具尤其当二进制数据需要被审视、转换或调试时。场景一数据规整与转换假设你拿到一段非常长、没有换行的二进制字符串用眼睛看几乎不可能。你可以将其粘贴到Excel的一个单元格比如A1然后使用MID函数将其拆分成单个字符或8位一组。在B1单元格输入公式MID($A$1, ROW(), 1)然后向下拖动填充。这会将A1中的每个字符拆到一列。或者要按8位拆分可以在B1输入MID($A$1, (ROW()-1)*81, 8)然后向下拖动。场景二利用条件格式生成“热力图”这是Excel在解决此类问题中的“杀手锏”。我们可以让Excel根据单元格的值“0”或“1”自动着色从而直观地“看到”二维码的轮廓。将拆分后的单字符数据每个单元格一个“0”或“1”填充到一个矩形区域比如从B2开始宽度和高度你猜测的二维码尺寸。选中这个数据区域。点击“开始”选项卡 - “条件格式” - “新建规则”。选择“只为包含以下内容的单元格设置格式”。设置“单元格值”“等于”“1”假设1是黑色。点击“格式”在“填充”选项卡中选择纯黑色。点击确定。再新建一个规则为值等于“0”的单元格设置白色填充。调整所有单元格为正方形设置相同的行高和列宽单位用像素。这时一个二维码的雏形就会在Excel中显现出来这个方法能帮你快速验证二进制数据对应的图像大概是什么判断宽度高度设置是否正确颜色映射是否反了。如果看到了清晰的定位图形三个角上的“回”字框那就成功了一大半。你可以截图或用Excel的导出功能再导入到画图工具中微调最后用手机扫码。实操心得在CTF比赛中时间紧张。当Python脚本因为一个参数错误反复调试时用Excel快速可视化一下往往能立刻发现问题所在比如数据流方向是行优先还是列优先。它充当了一个快速的“预览器”。4. CTF实战中的进阶技巧与常见“坑点”掌握了基本方法我们来看看在真实CTF场景下围绕“二维码生成”还会有哪些变形和陷阱。4.1 尺寸与方向的陷阱尺寸未知题目只给二进制流不告诉宽度。你需要尝试不同的因数分解。如果二进制流长度是L尝试找到w和h使得w * h L。通常二维码是正方形所以先尝试w h sqrt(L)是否为整数。如果不是则可能是长方形图像或者数据包含了非图像内容如文件头。扫描顺序二进制流填充图像的顺序可能是行优先一行一行填也可能是列优先一列一列填。如果按一种顺序生成的图无法识别尝试另一种。在Python循环中交换x和y的循环次序即可测试。4.2 嵌入多层编码与隐写“0和1”可能只是第一层。例如二进制流解码后是一串Base64字符串。解码Base64得到另一串二进制或十六进制数据。该数据可能是一个PNG文件的字节流将其写入文件后得到的图片里藏着二维码。或者直接生成的二维码图片其像素最低有效位LSB里还藏着别的信息需要用Stegsolve等工具进一步分析。4.3 纠错码的利用二维码有纠错能力L/M/Q/H四个等级。出题人有时会故意损坏二维码的部分模块将一些“0”改成“1”或反之。如果你知道原始数据的一部分比如Flag的格式flag{并且知道使用的纠错等级理论上可以利用纠错算法尝试恢复。不过在实际CTF中更常见的做法是尝试用不同的掩模模式重新编码已知数据看哪个能生成与损坏区域匹配的图案或者直接用PS等工具手动修补损坏的定位图形。4.4 非标准二维码变种题目可能生成的是微型QR码、艺术二维码中间有Logo或彩色二维码。核心依然是找到那些代表数据的黑白模块。对于彩色二维码可能需要提取特定颜色通道如红色通道或计算灰度值后二值化。5. 构建完整的解题工作流与脚本范例结合以上所有点一个稳健的解题流程应该是初步分析检查文件类型file命令、查看字符串strings命令、用十六进制编辑器查看判断数据是纯文本、二进制还是其他格式。提取二进制确认核心数据是“0”和“1”的字符串。可能需要从文件、网络流量或图片的像素中提取。尝试直接转换先假设每8位是一个ASCII字符解码看看是否有明文Flag或提示。可视化探索如果上一步失败将二进制流导入Excel或用简单Python脚本尝试不同的宽度/高度和扫描顺序进行可视化预览寻找二维码的结构特征定位图形。精确生成根据可视化结果确定正确的参数宽度、高度、颜色映射、扫描顺序用PIL库生成高分辨率、带合适边框的二维码图片。解码与验证使用手机扫码软件或Python的pyzbar、opencv库进行解码。深入检查如果解码失败检查生成的图片模块是否清晰边框是否足够至少4个模块宽。如果解码成功但Flag不对考虑是否还有下层编码或隐写。下面是一个综合性的、容错性更好的示例脚本框架import argparse from PIL import Image import math def try_decode_as_text(binary_str): 尝试将二进制字符串解码为文本 print(尝试解码为ASCII文本...) # 补充前导零到8的倍数 padding 8 - len(binary_str) % 8 if padding ! 8: binary_str 0 * padding binary_str print(f补充了{padding}个前导零。) try: text .join(chr(int(binary_str[i:i8], 2)) for i in range(0, len(binary_str), 8)) if all(ord(c) 128 and c.isprintable() or c in \n\r\t for c in text): print(f可能的文本: {text[:200]}...) # 只打印前200字符 return text else: print(解码结果包含非打印字符可能不是纯文本。) except Exception as e: print(f解码失败: {e}) return None def generate_and_save_image(binary_str, width, height, is_black_oneTrue, module_size10, output_prefixqr): 根据给定参数生成图像 if len(binary_str) ! width * height: print(f警告: 数据长度{len(binary_str)} ! 指定尺寸{width}x{height}{width*height}。尝试自动调整...) # 可以尝试裁剪或填充这里简单返回 return False img_width width * module_size img_height height * module_size img Image.new(1, (img_width, img_height), 1) for y in range(height): for x in range(width): idx y * width x # 行优先 # idx x * height y # 列优先 (注释掉需要时启用) pixel_val 0 if (binary_str[idx] 1) is_black_one else 1 for dy in range(module_size): for dx in range(module_size): img.putpixel((x*module_sizedx, y*module_sizedy), pixel_val) filename f{output_prefix}_w{width}_h{height}_{1black if is_black_one else 0black}.png img.save(filename) print(f图像已保存为: {filename}) img.show() return True def main(): parser argparse.ArgumentParser(description处理‘0和1的故事’类型CTF题目) parser.add_argument(input_file, help包含二进制字符串的文本文件) parser.add_argument(--width, typeint, help图像宽度模块数) parser.add_argument(--height, typeint, help图像高度模块数) parser.add_argument(--invert, actionstore_true, help反转颜色‘0’代表黑) parser.add_argument(--module, typeint, default10, help每个模块的像素大小) args parser.parse_args() with open(args.input_file, r) as f: content f.read().strip().replace( , ).replace(\n, ).replace(\r, ) print(f读取到 {len(content)} 位二进制数据。) # 第一步尝试直接解码文本 text_result try_decode_as_text(content) if text_result and flag in text_result.lower(): print(可能在文本解码中直接发现了Flag!) # 这里可以进一步处理text_result # 第二步尝试作为图像生成 if args.width and args.height: generate_and_save_image(content, args.width, args.height, not args.invert, args.module) else: # 自动尝试可能的正方形尺寸 print(未指定尺寸尝试自动寻找正方形尺寸...) L len(content) possible_sides [] for i in range(int(math.sqrt(L)), 0, -1): if L % i 0: possible_sides.append((i, L//i)) print(f可能的尺寸宽x高: {possible_sides[:10]}) # 只显示前10种可能 for w, h in possible_sides[:5]: # 尝试前5种 if w h or abs(w-h) 5: # 优先尝试正方形或接近正方形的 print(f尝试尺寸: {w}x{h}) if generate_and_save_image(content, w, h, True, args.module, fauto_{w}x{h}): input(按回车键尝试下一个尺寸或颜色反转...) # 尝试颜色反转 if generate_and_save_image(content, w, h, False, args.module, fauto_{w}x{h}_inv): input(按回车键尝试下一个尺寸...) if __name__ __main__: main()这个脚本提供了从尝试文本解码到自动探测图像尺寸的完整流程并且每次生成图片后暂停方便你手动扫码测试。在实际比赛中这种系统化的尝试方法比盲目猜测高效得多。最后解决这类问题的乐趣在于它强迫你从最底层的比特流开始思考一步步构建出有意义的信息。这不仅是CTF技能更是理解数据表示、编码和计算机图形学基础的一个绝佳窗口。下次再看到“0和1的故事”希望你不仅能想到二维码还能联想到图像隐写、数据恢复和协议分析等更广阔的场景。