1. 项目概述当CTF遇上记事本与Python如果你对网络安全竞赛CTF感兴趣尤其是那些看起来像天书一样的“杂项”Misc题目那么“二维码”绝对是一个绕不开的经典题型。你可能在网上见过这样的题目附件里就一个flag.txt打开一看里面密密麻麻全是0和1或者夹杂着一些奇怪的字符题目描述却暗示你“扫一扫”就能得到答案。新手看到这里往往一头雾水这明明是个文本文件跟二维码有什么关系这正是CTF二维码题的魅力所在它考验的往往不是你对二维码协议有多深的理解而是你能否将看似无关的数据通过一系列逻辑转换还原成二维码图像的能力。而我们的工具简单到令人难以置信Windows自带的记事本和几乎无所不能的Python。这个组合听起来可能有些“原始”但恰恰是这种从最基础工具开始的解题路径最能帮助我们理解数据转换的本质也是从零基础到入门CTF的绝佳实践。本文将带你彻底拆解这类题目的核心从理解题意到动手写出解题脚本并用一个完整的实战案例让你体验从一堆乱码中“扫”出Flag的成就感。2. 核心思路拆解从文本到图像的魔法面对一个CTF二维码题我们的目标非常明确将题目给出的文本数据最终转换成一个能被标准二维码扫描器如手机摄像头、微信扫一扫成功识别的图片文件通常是PNG格式。这个过程就像一场解码游戏我们需要扮演“翻译官”和“画家”的双重角色。2.1 理解数据形态常见的“二维码”伪装术题目给出的原始数据很少是直接的图片文件。为了增加难度出题人会将二维码的图像信息进行编码或转换。常见的数据形态有以下几种01二进制字符串这是最经典也最直接的一种。文本内容全部由字符“0”和“1”组成可能还包含换行。例如1111111000100001101111111 1000001011101000100000010 1011101010001010101110101 ...这里的“1”通常代表二维码图片中的黑色模块黑块“0”代表白色模块白块。每一行字符串对应二维码图像的一行像素。其他字符表示的二进制为了增加迷惑性“0”和“1”可能被替换成其他字符对。例如用“#”和空格“ ”分别代表黑块和白块或者用“”和“.”。解题的关键在于识别出这种映射关系。经过编码的文本原始二维码的二进制信息可能先被进行了一次通用编码比如Base64、Hex十六进制等。你首先需要解码这段文本得到原始的二进制数据流然后再将这些数据流按一定宽度和高度还原成图像。坐标点数据有时文件里会给出类似(0,0), (0,1), (5,7)...的坐标列表这通常表示黑色像素点在图像中的位置。你需要根据坐标范围推断出图像尺寸然后生成一个全白的底图再把对应坐标的点涂黑。我们的核心任务就是识别出题目属于以上哪种类型并设计出对应的转换流程。2.2 解题工具箱选择为什么是记事本Python记事本Notepad它在这里扮演的是“数据预处理”和“肉眼观察”的角色。对于简单的字符替换比如把“#”批量换成“1”记事本的“替换”功能CtrlH简单粗暴且有效。更重要的是你可以直接打开文件观察数据的排列规律例如是否等宽、是否有特定的分隔符这是任何自动化脚本都无法替代的“第一眼分析”。Python它是整个转换过程的“发动机”。我们需要用它来读取和解析复杂的文本文件。执行批量字符替换、编码解码Base64, Hex。计算图像尺寸如根据数据行数确定高度根据每行字符数确定宽度。创建和操作图像将文本数据绘制成像素。保存最终的二维码图片。Python的PILPillow库是处理图像的瑞士军刀而内置的字符串处理方法足以应对大部分文本解析工作。这个组合实现了从手动探查到自动化解题的平滑过渡。3. 核心工具准备与环境搭建工欲善其事必先利其器。我们不需要复杂的IDE一个清晰的思路和几个正确的工具就够了。3.1 Python环境与必备库安装对于零基础的朋友建议直接访问Python官网下载最新稳定版本。安装时务必勾选“Add Python to PATH”这样可以在命令行全局使用Python。安装完成后打开命令行CMD或PowerShell输入python --version确认安装成功。接下来安装我们唯一必需的第三方库Pillow它是PIL库的友好分支功能更强且维护活跃。pip install Pillow如果下载速度慢可以使用国内镜像源例如pip install Pillow -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 记事本使用技巧不止是替换虽然记事本看起来简单但在CTF解题中用好它能事半功倍。显示所有字符在“格式”菜单中勾选“自动换行”。但这对于分析长行数据可能造成误导。更好的方法是观察状态栏或拖动滚动条感受数据的原始长度。替换功能CtrlH这是核心功能。例如将“#”替换为“1”将空格替换为“0”。关键技巧替换时先处理那些不会产生歧义的字符。例如如果“.”代表白“”代表黑而文本中可能本身就有“.”作为句号。这时你需要更谨慎或者直接使用Python进行精确替换。保存格式注意保存文件的编码格式。通常保持默认的“ANSI”或“UTF-8”即可。如果从某些题目平台下载的文件打开是乱码可以尝试用记事本“另存为”并在编码中选择“UTF-8”或“GB2312”试试。注意对于大型文件超过几百KB记事本可能会卡顿。此时Python脚本就是唯一的选择了。这也引出了我们的核心观点记事本用于快速探索和简单处理Python用于最终和复杂的自动化解决方案。4. 实战案例解析从01字符串到可扫二维码现在我们用一个模拟真实CTF赛题的完整案例来走通整个流程。假设我们获得了一个名为qr_data.txt的文件。4.1 第一步数据观察与清洗用记事本打开qr_data.txt你看到的内容可能是这样的11111110101010100000101111111 10000010110010011101101000001 10111010100100010010101011101 10111010011001001110101011101 10111010100100110000101011101 10000010101010101101101000001 11111110101010101010101111111 00000000001110011111000000000 ...为节省篇幅此处为示例片段实际数据可能有多行比如37x37、41x41等常见二维码尺寸对应的行数观察要点一致性每一行的字符数是否相同是的看起来都是27个字符。这很可能就是二维码的宽度27像素。字符集只有“0”和“1”非常理想。结构特征注意看开头几行是否有1111111和1000001这样重复出现的模式这很可能是二维码的“位置探测图形”Finder Pattern是二维码三个角上的大方块。这证实了我们数据的正确性。清洗操作在这个案例中数据已经很干净了无需用记事本替换。但如果数据里有空格、制表符或无关的注释就需要先用记事本的替换功能清理掉。例如去除所有空格。4.2 第二步Python脚本编写与图像生成数据观察清楚后我们就可以用Python来“画图”了。创建一个新的Python脚本文件比如solve_qr.py。from PIL import Image def txt_to_qr(txt_file_path, output_image_path): 将纯01文本文件转换为二维码图片。 参数: txt_file_path: 包含01矩阵的文本文件路径。 output_image_path: 输出的二维码图片路径。 # 1. 读取文本文件 with open(txt_file_path, r) as f: lines f.readlines() # 2. 数据清洗去除每行首尾的空白字符如换行符 # 同时确保我们只处理由0和1组成的行过滤掉可能的空行 data_lines [] for line in lines: cleaned_line line.strip() # 去除首尾空格和换行 if cleaned_line and all(c in 01 for c in cleaned_line): # 检查是否为非空且只含01 data_lines.append(cleaned_line) if not data_lines: print(错误未找到有效的01数据行。) return # 3. 确定图像尺寸 height len(data_lines) # 行数即高度 width len(data_lines[0]) # 第一行的长度即宽度 # 安全检查确保所有行长度一致 for i, line in enumerate(data_lines): if len(line) ! width: print(f警告第{i1}行长度({len(line)})与第一行宽度({width})不一致可能导致图像畸形。) # 一种处理方式取最大宽度短行右侧补0白 # 这里为简单起见我们选择报错或截断。通常CTF题数据是规整的。 # 我们选择以第一行为准截断或报错。这里采用静默截断取前width个字符 data_lines[i] line[:width].ljust(width, 0) # 不足补0 # 4. 创建一张新的黑白图像 # 模式‘1’表示1位像素黑白二值图非常节省内存且适合二维码。 img Image.new(1, (width, height), color1) # color1 初始化为白色(1) pixels img.load() # 获取像素访问对象 # 5. 将文本数据绘制到图像上 for y in range(height): line data_lines[y] for x in range(width): # 如果字符是1则画黑点在模式‘1’中0代表黑色 if line[x] 1: pixels[x, y] 0 # 黑色 # 白色‘0’已经由初始化的1白色表示了所以无需操作 # 6. 保存图像 # 为了获得更好的扫描兼容性可以放大图像。二维码模块太小可能难以扫描。 scale_factor 10 # 每个原始像素放大为10x10的方块 large_img img.resize((width * scale_factor, height * scale_factor), Image.NEAREST) large_img.save(output_image_path) print(f二维码图片已生成并保存至{output_image_path}) print(f原始尺寸{width}x{height} 输出尺寸{width*scale_factor}x{height*scale_factor}) # 使用函数 if __name__ __main__: input_file qr_data.txt # 你的数据文件 output_file flag_qr.png # 输出的二维码图片 txt_to_qr(input_file, output_file)4.3 第三步运行脚本与获取Flag确保qr_data.txt和solve_qr.py放在同一个文件夹下。在命令行中导航到该文件夹运行命令python solve_qr.py脚本会打印处理信息并在当前目录生成flag_qr.png。用你的手机微信、支付宝或任何二维码扫描APP打开这张flag_qr.png图片进行扫描。如果一切正确扫描结果应该就是本题的Flag可能格式如flag{This_is_a_sample_flag}或CTF{You_Found_It!}。5. 进阶技巧与变种题型处理实战中题目不会总是规整的01文本。下面我们探讨几种变种及其处理策略。5.1 处理非“01”字符的映射假设数据文件里用“”表示黑“.”表示白。我们只需要修改Python脚本中的判断逻辑即可。修改脚本中的绘制部分# 原判断逻辑 # if line[x] 1: # pixels[x, y] 0 # 新判断逻辑 char line[x] if char : # 根据题目定义代表黑色模块 pixels[x, y] 0 elif char .: # .代表白色模块 pixels[x, y] 1 # 设置为白色 else: # 如果遇到意外字符可以按白色处理或报错 pixels[x, y] 1 # print(f未知字符 {char} 在位置({x},{y})已按白色处理。)或者更通用的方法是在读取数据后先做一次统一的字符串替换data_lines [line.strip().replace(, 1).replace(., 0) for line in lines if line.strip()]这样后续的绘制代码就无需改动。5.2 处理Base64或Hex编码的数据有时你拿到的flag.txt内容可能是一长串像“R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7”这样的字符串。这很明显是Base64编码。解题步骤识别编码Base64字符串通常由A-Z, a-z, 0-9, , /, 组成。Hex字符串则是0-9, a-f的组合。Python解码import base64 import binascii # 假设 encoded_data 是从文件读取的字符串 encoded_data R0lGODlhAQABAIAAAAAAAP... # 尝试Base64解码 try: decoded_bytes base64.b64decode(encoded_data) print(Base64解码成功得到字节数据。) except: print(不是有效的Base64。) # 或者尝试Hex解码 try: decoded_bytes binascii.unhexlify(encoded_data.strip()) print(Hex解码成功得到字节数据。) except: print(不是有效的Hex。)分析解码后数据解码得到的decoded_bytes可能直接就是一张图片的二进制数据你可以直接将其写入文件with open(decoded_output.png, wb) as f: # 注意是wb二进制写入 f.write(decoded_bytes)然后尝试打开这个图片文件。如果它本身就是一个完整的PNG二维码那就直接扫描。如果不是你可能需要进一步分析这些字节看它们是否是描述二维码像素的原始位图数据这时可能需要结合图像宽度和高度信息来解析。5.3 处理坐标点数据如果文件内容是这样的(0,0) (0,1) (0,2) ... (5,7)这表示在坐标(0,0), (0,1)等处是黑点。我们需要解析出所有坐标。找出X和Y坐标的最大值以确定图像宽度和高度通常是max_x1和max_y1。创建一个全白的图像。将所有坐标点涂黑。示例代码片段from PIL import Image import re coord_data [] max_x, max_y 0, 0 # 假设每一行有多个用空格或逗号分隔的坐标格式如 (x,y) with open(coords.txt, r) as f: for line in f: # 使用正则表达式找出所有坐标 matches re.findall(r\((\d),\s*(\d)\), line) for x_str, y_str in matches: x, y int(x_str), int(y_str) coord_data.append((x, y)) max_x max(max_x, x) max_y max(max_y, y) width, height max_x 1, max_y 1 img Image.new(1, (width, height), color1) pixels img.load() for (x, y) in coord_data: if 0 x width and 0 y height: pixels[x, y] 0 # 画黑点 img.resize((width*10, height*10), Image.NEAREST).save(qr_from_coords.png)6. 常见问题排查与调试心得即使按照步骤操作生成的二维码也可能扫不出来。别急这是学习过程的一部分。以下是几个常见问题及排查思路生成的二维码图片一片黑或一片白原因最可能的原因是数据映射关系弄反了。在二值图像模式‘1’中0是黑1是白。检查你的脚本是否把该画黑的地方画成了白。解决交换绘制逻辑中的0和1。或者检查原始数据确认“1”是否真的代表黑块。有时题目会反着来。二维码扫描器无反应或提示格式错误原因A尺寸错误二维码有固定的尺寸版本从21x21到177x177每增加4个模块一个版本。虽然我们不需要知道确切版本但生成图像的宽高必须严格相等并且所有行等长。用print(width, height)检查一下。原因B位置探测图形损坏二维码三个角上的“回”字形方块是扫描器定位的关键。如果你的数据最外几行有缺失或错位会导致定位失败。检查你的原始数据边缘是否完整。原因C模块太小如果你生成的图片尺寸是37x37像素在手机屏幕上可能只是一个小点扫描器无法识别。解决务必使用缩放就像示例代码中的scale_factor10将每个模块放大到10x10像素以上大大提升识别率。可以尝试放大到20甚至30。数据行长度不一致原因文本文件可能在编辑或传输过程中某些行末尾多了空格或者本身数据就不规整。解决在脚本中加入健壮性处理。如示例代码中可以采用ljust或rjust进行对齐补0或补1需根据背景色判断或者取所有行的最大长度作为宽度短行进行填充。最稳妥的方式是仔细检查原始数据手动在记事本中调整。脚本运行报编码或解码错误原因文件编码问题。特别是当数据中包含中文或其他非ASCII字符时。解决在Python打开文件时指定编码如open(‘file.txt’, ‘r’, encoding‘utf-8’)或encoding‘gbk’。尝试不同的编码直到能正确读取。我的调试心得当二维码扫不出来时不要只盯着代码看。把生成的小图片用画图工具打开放大人工对比一下。看看三个角上是不是都有明显的“回”字框黑白黑白黑比例是1:1:3:1:1。这是二维码最显著的特征。如果这个特征对不上那肯定是数据到图像的转换环节出了根本性问题。