ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

BUUCTF Misc第16-20题实战:隐写分析、二维码修复与AES解密全解析

BUUCTF Misc第16-20题实战:隐写分析、二维码修复与AES解密全解析 刷BUUCTF Misc的题很多人都是从第1题开始一路往后啃的我最近正好啃到题单里的第16到20题。这一批题画风很杂有签到题里藏着看不见的字符有二维码图片扫不出来有从一百张图里挑一个有问题的还有一个名叫 Online Tool 的题其实在教你怎么读命令执行的回显最后甚至撞上一道需要手动处理 AES 加密的 babyaes。单独看每道题都不算特别硬核但把它们串在一起就会发现Misc 这方向真正考的不是某个单项技术而是你面对一个未知文件时有没有一套稳定的“拆开—分类—提取—解码”习惯。这篇博文就把这几道题的完整思路拆开写清楚包括每一步操作背后的原因、我实际踩过的坑、以及可以直接复用的命令和脚本。不管你是刚接触 CTF 的入门选手还是已经在杂项里扑腾过一阵子的新手只要你准备刷 BUUCTF Misc这篇应该能帮你省下不少试错时间。1. 这批题目在考什么先看题型再动手1.1 五道题整体盘点按我手头题单的顺序第16到第20题分别对应下面五个题名虽然不同平台的题号偶尔会错位但这五道题覆盖的考点是稳定的顺序题目实际考点常用工具第16题[GKCTF 2021] 签到不可见字符/零宽字符隐写010 Editor、Python、CyberChef第17题二维码图片宽高修改、二维码修复Python、010 Editor、QR Research第18题百里挑一批量文件筛查、LSB隐写md5sum、zsteg、Python、PIL第19题[BUUCTF 2018] Online Tool命令执行回显分析curl、Burp Suite、浏览器第20题babyaesAES参数识别与手动解密Python、pyCryptodome为什么要把这五道放在一起说因为它们正好形成一个能力闭环。第16题考的是“内容看不见怎么办”第17题考的是“图片看起来坏了怎么修”第18题考的是“文件太多怎么筛”第19题考的是“服务端返回的内容怎么提取”第20题又回到了“加密数据怎么解析”。你把这五道题按顺序刷完基本就把杂项日常会碰到的几种数据形态都接触了一遍。1.2 环境准备与工具链我先交代一下自己用的环境Windows 10 主力机装了 WSLUbuntu 20.04很多命令行工具在 Linux 下跑比较顺手。如果你只有 Windows也能做就是少数命令行工具要自己装或者找替代品。010 Editor / HxD十六进制查看器看文件头、改PNG宽高、查文件尾部追加数据都靠它。binwalk / foremost文件分离工具用于从图片或未知文件中提取内嵌的压缩包和文件。zsteg专门检测PNG/BMP里的LSB隐写比手动写脚本快得多。StegsolveJava写的老牌图像隐写分析工具可以逐通道查看图片的最低有效位。QR Research / ZXing二维码解码工具有些手机扫码扫不出来的情况QR Research 反而能给出更多提示。CyberChef英国GCHQ开源的编码/解码工具它的 Magic 按钮能自动识别很多常见编码组合适合做第一步试探。Python3 pyCryptodome / Pillow处理AES解密和图像像素级隐写的基本依赖。这里特别提醒一句工具贵精不贵多。有人桌面塞了几十个工具遇到题目还是不知道从哪下手。我的建议是先看文件头再跑 binwalk然后根据文件类型决定用 zsteg 还是 Stegsolve这套流程已经能覆盖大部分杂项题。不要一开始就堆工具不然你会被输出信息淹没。2. 第16~17题签到与二维码热身里的两个坑2.1 [GKCTF 2021] 签到看到“空文本”别急着交卷第16题叫“[GKCTF 2021] 签到”。听到“签到”两个字很多人第一反应是送分题结果下载下来发现是一个txt文件打开之后整个文件看起来就是一片空白或者只有几个“看不见的字符”。第一眼会觉得是不是平台传错文件了其实不是这就是考点本身。我当时的处理顺序是这样的先用file 签到.txt看格式结果是 UTF-8 Unicode text说明确实有文本内容。再用xxd 签到.txt | head看原始字节发现里面有很多e2 80 8b、e2 80 8c这样的字节序列这就是我熟悉的零宽字符。零宽字符是一类不可见字符常见的有 U200B零宽空格、U200C零宽非连接符、U200D零宽连接符、UFEFF零宽不换行空格。它们本身不显示任何图形但确实占用了Unicode编码空间。有些出题人会把它们当成二进制位来用比如 U200B 表示 0U200C 表示 18个字符一组还原成一个字节再把所有字节拼起来就是正常字符串或直接是flag。我用Python写了个小脚本进行解码data open(signin.txt, r, encodingutf-8).read() # 先输出所有不同字符的码点确认用了哪几个零宽字符 for ch in sorted(set(data)): print(hex(ord(ch)), repr(ch)) zero_chars {\u200b} # 0 one_chars {\u200c} # 1 bin_str for ch in data: if ch in zero_chars: bin_str 0 elif ch in one_chars: bin_str 1 # 其他正常字符跳过可能是干扰 print(bin_str)跑完之后得到一串二进制串按 8 位一组转成字节再尝试 ascii 解码。实际做的时候第一次解出来的内容看起来不像flag而是又一段 base64。把这段 base64 复制进 CyberChef 的 Magic 里跑一下很快就得到真正的 flag。这里我踩过的一个坑是直接用记事本打开txt记事本几乎不显示零宽字符所以一开始根本意识不到有内容。如果你也遇到一个“什么都没有”的文件先别怀疑人生用十六进制编辑器看一眼。宁可多花一分钟看文件头也不要对着空白文本发呆十分钟。2.2 二维码——扫不出来时先把图片当数据看第17题更直接题目名就叫“二维码”。下载下来是一张 PNG 图片看起来就是很普通的二维码。按照正常思路拿出手机扫码结果发现要么没反应要么解析出来是乱码。如果你也是先扫了五分钟然后把图片放大检查恭喜你你已经进入这道题的节奏了。二维码题最常见的出题手法就是“把图片的宽高改小”导致二维码的一部分内容被裁掉或者图片显示不完整。PNG 格式有个特点图片的实际宽高信息存在文件头部的 IHDR 数据块里而真正的像素数据在 IDAT 数据块。出题人如果把 IHDR 里的宽度或高度改小图片打开时就只显示一小块二维码自然就识别不出来。我的处理方法是先用 Python 把宽高改回原来的值。PNG 文件的 IHDR 结构很有规律从文件偏移 16 字节开始前 4 字节是宽度后 4 字节是高度都是大端序存储。我写了一个脚本直接把宽度或高度改成一个大一点的数然后重新计算并修复 CRC保证文件头校验不报错import struct import zlib def fix_png_size(png_path, new_width, new_height): data bytearray(open(png_path, rb).read()) # IHDR 在偏移 16 处开始宽度在第 16~19 字节高度在 20~23 字节 struct.pack_into(I, data, 16, new_width) struct.pack_into(I, data, 20, new_height) # 修正 IHDR 数据块的 CRC ihdr_crc_offset 29 crc zlib.crc32(data[12:29]) 0xffffffff struct.pack_into(I, data, ihdr_crc_offset, crc) open(fixed.png, wb).write(data) fix_png_size(qr.png, 400, 400)跑完脚本再打开fixed.png二维码果然完整出现了一扫就出了 flag。如果你遇到底图高度被调小的情况就把高度改大一点有时宽度也会被改需要两个都试试。改值的时候不用非要精确到原始尺寸只要宽高足够包含所有像素块图片就能正常显示。还有一个常见情况二维码的三个定位角左上、右上、左下那三个大方块被遮挡或删掉了一个导致扫码软件无法定位。这种题不能靠改宽高解决要手动把缺的定位角补回去。先用图像编辑软件复制一个完整的定位角再粘贴到缺失的位置注意按原来的尺寸等比缩放。有些人会把右下角也放一个定位角这其实不符合二维码规范但有些扫码软件反而能靠这个识别出来。还有更复杂的情况需要使用 QRazyBox 这类工具调整掩码和纠错级别因为有时候图里包含两层二维码信息一层是正常内容另一层可能是隐藏信息。二维码这类题目我总结的经验是当扫码失败时先判断是“图像显示不完整”还是“定位点缺失”再决定用脚本改宽高还是手动补图。不要一上来就用各种扫码App轮流扫那样大多数时候是无用功。3. 第18~19题一百张图与一个在线工具3.1 百里挑一——批量筛选图片中的隐写第18题叫“百里挑一”名字起得很直白下载解压后确实是一百张图片。这个题名基本就是在暗示flag藏在这一堆图里的某一张里你需要从一百张中把它挑出来。如果你这时候还想着用肉眼看一百张图足够你看到怀疑人生。正确姿势是先用哈希把所有文件过一遍找出跟绝大多数文件不一样的“异类”。md5sum *.png | sort | uniq -c -w32uniq -c -w32的意思是只比较前32个字符也就是md5值本身。如果这一百张图里有九十九张完全相同只有一张不一样这条命令会非常直观地把那张单独列出来。但如果一百张图的md5全都不同说明每张图表面都有所差异那就要换个思路。我当时跑完哈希发现有一张图的哈希确实和其他九十九张不一样于是把目标锁定到那张图。接着先跑binwalk看有没有内嵌文件没发现异常再跑strings找 flag 关键字也没找到最后用zsteg -a检查LSB隐写终于在最低有效位里提取到了可疑的字符串。zsteg 的批量用法也很适合这种题for i in *.png; do echo $i ; zsteg -a $i 2/dev/null | grep -i flag; done如果题目里的一百张图本身可见内容不同但只有某张的最低有效位藏了信息上述循环就是最高效的方案。跑完能直接定位到目标文件省去逐个打开的力气。如果环境里没有 zsteg也可以用 Python 手动提取 LSB。思路是读取每个像素的 RGB 值把每个颜色通道的最低比特位依次取出组成二进制数据。对 PNG 这种无损格式来说最低比特位的变化肉眼几乎无法察觉所以很适合藏信息。from PIL import Image img Image.open(target.png) pixels list(img.getdata()) bits [] for pixel in pixels: for channel in range(3): # R, G, B bits.append(pixel[channel] 1) # 把比特流按8位拼成字节 byte_list [] for i in range(0, len(bits) - 7, 8): b 0 for j in range(8): b (b 1) | bits[i j] byte_list.append(b) data bytes(byte_list) # 在字节流里找可打印的flag print(data)不过这里有个细节LSB隐写的读取顺序有“从上到下按行”和“从下到上按行”之分颜色通道的顺序也可能不同所以手写脚本经常要试几种排列方式。zsteg 的好处就是它把这些常见排列都试了一遍。所以我建议你先用现成工具工具跑不出来再手写。这道题让我印象最深的一点是很多人看到“一百张图”就慌了实际上出题人给的干扰项可能非常粗糙九十九张图完全一样唯一的差异就是藏了flag的那张。拿到压缩包第一个动作永远是批量计算哈希而不是双击打开图片。3.2 Online Tool——回显内容里找flag第19题是“[BUUCTF 2018] Online Tool”。这题在平台分类里有时会被归到 Web但在 Misc 题单里也能遇到因为在杂项视角下我们不是去深挖漏洞利用链而是关心“服务端执行了什么、返回了什么、flag藏在哪里”。题目界面通常是一个在线查询工具看起来像一个普通的 web 页面提供一个参数输入框。提交一个正常参数后页面会返回对应的执行结果这时候你要立刻意识到这个参数很可能被拼到了服务器端命令里。我先用浏览器直接访问目标地址加上一个普通参数比如?name1观察返回内容。再测试有没有命令分隔符能被识别比如分号、换行符、管道符。如果返回结果出现了预期的命令输出就说明这里存在命令执行点。接下来要考虑过滤规则。CTF 题目一般都会过滤flag关键字、空格、斜杠之类的内容。如果直接cat flag被过滤可以用 base64 编码输出绕过关键字把 flag 文件内容先编码再返回页面显示的就是 base64 字符串你再手动解码。如果空格被过滤可以用${IFS}代替比如cat${IFS}flag。由于每道题的过滤规则不同我给不出一个永远通用的 payload但思路路径是固定的先确认参数能不能带进命令用一个简单的无伤大雅的命令测试比如id或whoami。如果输出被直接回显就可尝试读取当前目录文件列表比如ls。发现文件后再尝试读取内容。如果cat flag被拦考虑tac flag、base64 flag、strings flag等替代方式。如果过滤了文件名可以用通配符f*甚至用反引号把命令拆开绕过。如果输出在返回前被处理掉例如只显示前几个字符可以配合head -c控制输出长度或者把文件逆序输出再在本地翻转。这题的很多 writeup 里都会写得很长绕来绕去但核心就是命令注入加回显分析。我实际做的时候卡在了一个很不起眼的地方题目只接受 GET 参数我一开始却用 POST 提交所以怎么测都没反应。后来用 Burp Suite 抓包看了一眼请求才发现问题。经验就是当某个参数怎么测都不生效先回头确认请求方法、Content-Type、参数名这三个基础信息再考虑是不是有更复杂的过滤。因为这是 BUUCTF 平台上的靶场环境所以做命令注入测试没有问题。如果你自己在别的地方看到类似页面不要随手去试先确认它是不是一个授权的 CTF 题目环境。4. 第20题 babyaesMisc里的加密题怎么快速摸清算法4.1 拿到加密脚本先读哪几行第20题叫 babyaes从名字就能看出来又是一道“拿小菜考验基本功”的题。很多人在杂项里看到 AES 就头疼总觉得要“破解加密”其实不是。CTF 里的加密题绝大多数不是让你暴力破解而是让你从给出的加密脚本、输出数据和密钥参数里把加密过程逆回去。打开题目给的加密脚本首先不要逐行读先找几个关键点用的是哪个加密库通常是from Crypto.Cipher import AES。密钥 key 是什么是写死的字符串还是从某个可预测的地方生成的。有没有 IV初始向量以及它和 key 的关系。加密模式是 ECB 还是 CBC。ECB 不需要 IVCBC 需要 IV两者在处理上差别很大。输出结果是什么格式hex、base64 还是直接写的字节。一个典型的脚本长这样from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 key bflag_placeholder_? # 通常是固定字符串或从某变量来 iv b0123456789abcdef flag bflag{...} cipher AES.new(key, AES.MODE_CBC, iv) enc base64.b64encode(cipher.encrypt(pad(flag, AES.block_size))) print(enc)加密过程很简单先用 PKCS7 方式把 flag 填充到 16 字节的整数倍然后进行 CBC 模式 AES 加密最后做一次 base64 编码。你要做的就是反向操作base64 解码、AES 解密、去除填充。这里我要特别强调一个点CBC 模式下解密需要 IV。如果你看到脚本用的是AES.MODE_CBC解密时一定要把同一个 IV 传进去。很多人解密出来前面十几个字节还是对的后面全乱码多半是 IV 传错了或者用的是 ECB 模式尝试解密 CBC 的密文。4.2 解密脚本编写我的解密脚本一般是这么写的from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 key b... iv b0123456789abcdef enc bbase64字符串... cipher AES.new(key, AES.MODE_CBC, iv) data cipher.decrypt(base64.b64decode(enc)) flag unpad(data, AES.block_size) print(flag)如果你拿到的密文是 hex 字符串就用bytes.fromhex(...)转成字节而不是 base64 解码。这里有一个很容易搞混的点base64 和 hex 都是可打印文本形式但解码方式完全不同。判断依据是看题目的输出里有没有结尾base64特征或者是不是只有 0-9a-f 字符hex特征。有的变种题会更绕一点脚本里 key 不是直接给的而是通过某个随机数生成器生成的但是随机数种子是一个固定的时间戳或固定字符串。这时候你需要从题目描述里找到 seed 或者自己推算出 key。如果你看到random.seed(...)这种代码别慌直接用相同的 seed 重新生成一遍就能得到和加密时一样的 key。还有一种常见情况是输出文件里前半段是 IV后半段是密文脚本用了类似enc iv cipher.encrypt(...)的写法。这时你不能直接把整段当成密文应该先切出前16字节作为 IV后面的部分才是真正要解密的内容。数据结构都可能成为坑。4.3 常见解密错误排查babyaes 这题本身难度不高但我在给朋友排查的时候发现大家容易栽在几个非常基础的地方填充错误AES 加密前如果用了pad解密后必须用对应的unpad去掉填充。直接打印解密结果时会看到尾部有一串\x0f这样的内容这就是 PKCS7 的填充字节。key 类型错误pyCryptodome 要求 key 必须是 bytes不能是 str。从文件或用户输入读出来的字符串要记得编码。key 长度错误AES key 只能接受 16、24、32 字节。如果脚本里给的 key 是b1234567890这种长度不对的字符串通常说明你看漏了代码比如还经过了哈希或补零的步骤。密文格式错误有时候题目给的输出后面有换行符直接 decode 会报错可以先strip()。CBC 和 ECB 混淆如果你不确定模式先看看脚本里有没有 iv 变量。有 IV 就是 CBC 或 CFB 之类没有就可能是 ECB。解密成功那一下是很有成就感的。关键不是你会不会用 Crypto 库而是你能不能从一道看起来是“加密题”的文件里迅速抽出“密钥、IV、模式、输入输出格式”这四个要素。5. 通用排坑与实操心得5.1 常见问题速查表根据这批题目的经验我把杂项题里碰到的高频问题和排查路径整理成了一张速查表方便后续做题时快速对照。现象可能原因解决思路文本文件打开是空白零宽字符隐写用010 Editor看十六进制或用Python遍历码点二维码扫码无反应图片宽高被改、定位角缺失修复PNG宽高/CRC补齐定位角图片文件解压后只有一张异常图隐写藏在LSB或文件尾部先算md5再跑binwalk最后用zsteg页面参数提交后返回正常内容参数可能存在命令执行先测id/ls再读取目标文件AES解密出来尾部乱码没有unpad或填充方式不对用unpad去掉PKCS7/PKCS5填充解密出来不是可读文本IV或key不正确确认脚本中的key、iv、密文格式binwalk提取不到内容隐写不在文件附加段改用zsteg、Stegsolve或手动读像素这些判断不一定每道题都适用但能帮你在卡住的时候快速找到下一个尝试方向。很多时候你不是“不会做题”而是没有建立有效的排查顺序。5.2 推荐几条“肌肉记忆”我刷杂项题最大的收获是形成了几条不需要过脑的习惯遇到新题能自动执行第一凡是从网上下载下来的文件不管后缀是什么先扔进十六进制编辑器看文件头。PNG 是89 50 4E 47JPEG 是FF D8 FFZIP 是50 4B 03 04。后缀可以伪造文件头一般不会。如果文件头显示这是 ZIP但后缀是 PNG多半里面藏着压缩包。第二凡是图片题五步走exiftool看元数据binwalk扫描附加文件strings找可疑字符串zsteg -a检查LSB最后再用 Stegsolve 逐层看。这套流程熟练之后一题花不了两分钟就能锁定方向。第三凡是带加密脚本的题先找 key、iv、mode、data 四个变量的来源。如果有两行代码看不懂先不着急把加密调用写出来然后“从下往上倒着读”。加密是encrypt(data)解密就是decrypt(enc)加密前有pad解密后就有unpad加密后做了base64解密前就要先base64decode。所有步骤都是一一对应的。第四凡是遇到“正常但总差点意思”的情况比如图片能打开但扫码失败页面有结果但flag不对先检查自己的输入方式。GET 还是 POST是不是少了参数有没有把0和O、1和l弄混。低级错误的概率远远大于高深坑点。5.3 我这几道题踩过的坑写这篇题解的时候我特意回忆了几个在这几题里真实浪费过时间的地方也算给大家提个醒。第一个坑是在[GKCTF 2021] 签到里。我第一次把文件拖进 notepad发现一片空白第一反应是文件被加密了。后来转念一想不对先用xxd看了一眼才发现全是e2 80开头的字节。如果我一开始没有用十六进制验证大概会在这个签到题上卡很久。第二个坑在二维码那道题。我找到宽高异常后直接在网上找了个在线工具修改PNG宽高工具确实把图撑大了但图片下半部分没有像素数据扫出来还是不完整。后来才意识到需要重新计算 IHDR 的 CRC否则图片在某些查看器里会直接打不开。自己写脚本虽然多几行代码但能保证修改是可控的。第三个坑在 babyaes 解密时。第一次解密出来的末尾有填充字节我直接用print打印了 bytes 对象屏幕上确实能看出 flag 开头的内容但复制出来去提交时总是失败因为整个字符串末尾还带着\x0f\x0f...的填充序列。后来用unpad处理完再提交就通过了。第四个坑是 Online Tool 那题。我一直在 Burp Suite 里用 POST 发送参数页面始终不返回预期结果。后来重新看题目页面的前端代码才发现表单用的是 GET参数应该是拼在 URL 查询字符串里。这提醒我做题时先确认请求方式再构造payload顺序不对一切都白搭。最后再说点体己话这几道题做完我最大的体会是Misc 不是一个拼脑洞的科目它拼的是一个很朴素的习惯——拿到文件后不要被它的表面形态骗了任何文件本质都是一串字节而正常显示只是这些字节被解释后的结果。当你看到“正常”的时候反而应该多问一句有没有哪个字节位置不对劲有没有哪段数据没有显示出来有没有哪一层数据藏在了另一层后面把这种怀疑精神带到日常工具里也很有用。碰到一个打开空白的文本先看十六进制碰到一个扫不出来的二维码先看图片尺寸碰到一个解不出来的密文先看脚本的 key 和 IV。这些操作一点都不玄学只是花几十秒多用工具确认一下。希望这篇 BUUCTF Misc 第16到20题的详细题解能让你少走几个我走过的弯路。
返回列表