资讯动态

CTF Misc中的PDF隐写分析:从工具到实战流程

发布时间:2026/10/9 12:34:49 来源:尧图企业网站定制
CTF 的 Misc 题里PDF 是个挺高频的载体。攻防世界平台上每隔一阵就能看到一道跟 PDF 有关的题目很多新手拿到文件的第一反应就是打开看一眼发现是一段正常的文字或者图片然后就卡住了。实际上 PDF 这类题目考的不是“你认不认识这份文件”而是“你知不知道 PDF 内部还能藏着什么”。我前前后后做过不少这类题也踩过一些坑这里把常用的分析思路和完整流程整理出来对刚入门 Misc 的选手应该能省下不少弯路。先说清楚这类题能做什么、适合谁看。PDF 文件在 CTF 里常见的出题方式有几种把 flag 藏在元数据里、藏在对象流的压缩数据里、藏在图片里、藏在不可见的文字层里、或者用滤镜编码把 flag 变形后混进文件尾部。这篇文章会从 PDF 的基础结构讲起手把手带你把一个典型的 PDF 题目从头到尾拆一遍适合刚接触 Misc、想系统了解 PDF 分析方法的选手也适合那些已经在做题但总是漏掉关键步骤、想看完整排查思路的人。1. 拿到 PDF 先别急这类题到底在考什么1.1 PDF 题的核心套路拆解PDF 格式本身是一种容器里面既能有文本、图片、字体也能塞进去几乎任何二进制数据。出题人利用的就是这个容器的“包容性”常见的藏东西方式就这么几类我按遇见的频率排一下元数据隐藏flag 直接放在 PDF 的 Info 字典里比如 Author、Title、Keywords 字段。这类最简单strings或者exiftool扫一眼就能看到。对象流隐藏PDF 1.5 之后支持把多个对象压缩进一个 Object Stream对象流里用 FlateDecode 压缩。flag 可能藏在某个没被渲染的文本对象里直接打开 PDF 看不出来但用 pdf-parser 之类的工具把对象流解开就能翻到。内容流隐藏页面内容流本身也是压缩的里面可能写了不可见的文字颜色跟背景一样、字号为 0、或放在可视区域之外。这种题要解析 Content Stream把文字内容提取出来。图片隐写PDF 里嵌了一张图片flag 用 LSB 隐写或直接拼在图片尾部需要把图片抠出来单独分析。附加文件或嵌入文件PDF 支持嵌入式文件EmbeddedFile可能直接塞了一个压缩包或文本解出来就是 flag。尾部附加数据出题人直接在 PDF 文件的 EOF 标记之后追加一段 base64 或十六进制字符串用strings或binwalk就能发现。这些套路单独看都不难难的是你手里拿到的题目往往不止一层。比如压缩对象流里藏了一段编码解码出来又是一段十六进制再转一遍才是 flag。分析的时候不能找到一个可疑点就停得把整个文件吃透才算完事。1.2 工具准备别一上来就把环境搞复杂做 PDF 类的 Misc 题最顺手的其实还是 Linux 环境Kali 或者任何装了常用分析工具的系统都行。我这里列一份常用清单按照“先用什么后用什么”的顺序排工具用途备注file确认文件真实类型有时候后缀是 .pdf实际是 ZIP 或其他格式strings提取可打印字符串加-n控制最短长度减少噪音exiftool查看元数据重点看 Author、Creator、Keywordspdftotext提取文本层内容能看到被渲染的文本但看不到隐藏对象pdfinfo查看 PDF 基本信息页面数、标题、PDF 版本pdf-parser.py深入解析 PDF 对象结构分析对象流、过滤器的核心工具qpdf解压对象流、重构 PDF把压缩对象释放出来方便二次分析binwalk扫描嵌入文件查找隐藏的压缩包、图片等foremost文件分离从 PDF 中提取嵌入的图片、文件zsteg检测 PNG/BMP 隐写抠出图片后的下一步新手常犯的一个问题就是在 Windows 下装了好几个 PDF 编辑器然后试图用“肉眼打开看”解决问题。这是方向性错误。PDF 题考的是格式解析不是文档阅读。你需要的是一堆能“拆开”PDF 的命令行工具而不是一个能“美化”PDF 的阅读器。装好上面的工具你就能覆盖 90% 的题目场景。1.3 为什么推荐命令行工具而不是图形化工具图形化工具比如 PDF 阅读器、在线解析网站它们能展示的是“渲染后的结果”而 flag 经常藏在“不参与渲染”或者“被特殊编码包裹”的部分。命令行工具操作的是原始字节流你能看到的东西跟出题人写进去的东西是同一个层面的。举个例子你在阅读器里看到的是一段正常文字“Hello World”这段文字实际存储在内容流里可能是明文也可能是压缩过的。如果是压缩过的阅读器会在内部解压并渲染但你拿不到原始流数据。用pdf-parser.py你可以直接看到内容流的 Filter 类型、长度、解码后的数据有没有奇怪的额外内容一眼就能看出来。另外命令行工具更容易写脚本批量处理。比如一道题里有 50 个对象你可以写个循环逐个解码把每个对象的原始数据都 dump 出来这种方式是手工点击永远做不到的。2. PDF 文件结构基础不懂结构就找不到 flag2.1 从文件头部到尾部PDF 的组织方式PDF 文件从结构上看分成四个部分文件头、对象集合、交叉引用表xref、尾部trailer。文件头一般就一行%PDF-1.x告诉你版本号对象集合是主体包含页面、字体、内容流、图片等各类对象交叉引用表记录了每个对象的偏移地址方便阅读器快速定位尾部则指向交叉引用表并包含一些全局信息。这里面最关键、做题时最常打交道的部分是“对象集合”。每个对象有编号和类型常见类型有 Catalog目录、Pages页面、Page具体页面、Font字体、Stream内容流或图片流。pdf-parser.py -a会把所有对象列出来你可以逐个查看。对象和对象之间还能互相引用比如一个 Page 对象会引用一个 Contents 对象来获取该页的绘制指令引用一个 Resources 对象来获取字体和图片资源。做题时往往需要顺着这些引用关系一层层追下去。我画个简单的关系说明你打开 PDF 看到的第一页其实是由一个 Page 对象描述的这个 Page 对象的 /Contents 指向了若干内容流对象内容流里是绘图指令和文字指令。flag 如果被放在内容流中但用了特殊方式隐藏比如白色文字你用阅读器看不出来但用文本提取工具把内容流解出来就能看到。2.2 过滤器Filter理解压缩与编码破解一半的题目PDF 里的 Stream 都带有一个 /Filter 属性用来标记这个流用了什么压缩或编码方式。常见的有FlateDecodezlib 压缩最最常见几乎所有内容流都用它ASCIIHexDecode十六进制编码把二进制数据转成可打印十六进制字符串ASCII85DecodeASCII Base85 编码用得少偶尔出现LZWDecodeLZW 压缩老版本 PDF 会用到DCTDecodeJPEG 图像压缩一般在图片流里理解 Filter 的作用在于你可以用 Python 的zlib或命令行工具把流解压出来看到真实的数据。很多时候flag 不是明文写在对象里而是先被 FlateDecode 压缩解压之后才露出真面目。你如果不解压拿strings去扫压缩过的二进制数据只能扫出一堆乱码。有一种常见的情况PDF 中有多个 Stream 对象其中一个本身就是纯文本但被 FlateDecode 压缩了。strings可能扫不到完整的 flag 字符串因为压缩后数据是无规律的。你必须找到这个 Stream用pdf-parser.py -f或qpdf解压再提取文本。2.3 字体和内容流的关系为什么看得见却搜不到PDF 显示文字的一个容易忽略的点是文字指令引用的是字体对象字体对象里定义了字符映射。换句话说内容流里显示的是字形 ID而不是直接的 Unicode 字符。打个比方比如某个自定义字体把字符 A 映射到了字形 0x01。内容流里写的是 0x01渲染出来是 A。你用pdftotext提取可能得到 A如果工具能处理该编码也可能得到乱码或不显示。在 CTF 里这种机制常被用来做“文字隐藏”或“文字替换”。有些题把 flag 的每个字符做映射然后把这个映射关系藏在字体对象的某个字段里。你如果不深入字体对象就只看到一堆数字根本对不上号。这时候需要用pdf-parser.py查看字体对象的详细字典找到 /ToUnicode 或 /Differences 之类的映射表。大部分情况不是特别复杂但你要有这个意识PDF 里显示出来的文字不等于原始数据里的文字必要时要回头去对映射。2.4 交叉引用表和尾部追加隐藏数据的最佳位置交叉引用表和 trailer 区是出题人藏数据的另一个重灾区。因为 PDF 解析器通常不会严格要求 xref 表中的偏移量准确无误很多阅读器在遇到 xref 损坏时会重建所以出题人可以在文件尾部追加一段字节不影响正常打开。另外PDF 还允许有增量更新也就是在文件末尾追加一个新的 xref 和 trailer形成多个版本叠加。有的题目会利用这一点把一份正常 PDF 和隐藏数据拼接在一起。你用binwalk去扫能清楚地看到文件尾部还有独立的压缩包或字符串块。我记得有一道题就是把 flag 换成 base64 后直接拼到%%EOF后面再用strings就能发现。所以拿到题目文件后第一件事永远是strings不要想复杂了。3. 实操走一遍把 flag 从 PDF 里扒出来3.1 第一步文件类型与元数据检查为了演示完整流程我构造一个比较典型的题目场景你拿到一个名为flag_in_pdf.pdf的文件。先别急着双击打开按顺序跑命令file flag_in_pdf.pdf strings -n 8 flag_in_pdf.pdf exiftool flag_in_pdf.pdf pdfinfo flag_in_pdf.pdf假设file输出显示这是一个 PDF 文档版本 1.5。strings -n 8输出的内容里除了正常文字还发现了一段可疑的 base64 字符串ZmxhZ3tQREZfaU5fTTA3TVAzfQ先用 base64 解码看看echo ZmxhZ3tQREZfaU5fTTA3TVAzfQ | base64 -d得到flag{PDF_iN_M0TMP3}。这里注意这种直接藏在可打印字符串里的 flag 通常是最简单的签到题但你不能因为发现了它就把题目抛到一边因为很多题目会有多个 flag 片段或者这个字符串是假的、故意放在这里诱导你的。exiftool的输出也要逐个看。Author、Creator、Keywords、Subject 这些字段一旦出现 base64 或者十六进制字符串都值得记下来。比如有时候 Keywords 字段值是666c61677b68336467652d68756e747d明显是十六进制编码的flag{h3dge-hunt}。经验是把strings和exiftool的结果全部保存到文件里逐行审阅不要只盯着终端屏看因为终端会截断长行容易漏掉关键内容。strings -n 8 flag_in_pdf.pdf strings_dump.txt exiftool flag_in_pdf.pdf exif_dump.txt保存下来之后再慢慢看效率高很多。3.2 第二步对象与内容流分析这一步是核心。先看 PDF 包含哪些对象pdf-parser.py -a flag_in_pdf.pdf输出会冗长建议先只看对象的类型和编号pdf-parser.py -a flag_in_pdf.pdf | grep -E ^\d \d obj假设输出里有 12 个对象其中序号 4 和 7 写着/Type /Catalog、/Type /Page序号 8 是一个/Type /Stream还带着/Filter /FlateDecode。直接对该流对象进行解码pdf-parser.py -f -o 8 flag_in_pdf.pdf-f表示通过过滤器解码-o 8指定对象编号。如果这个流是内容流你可能会看到类似下面的内容BT / F1 12 Tf 72 720 Td (Key: flag{first_part_}) Tj ET ...如果内容流里直接出现了 flag那这道题就是纯粹的“解压对象流”题型。如果没出现那还要进一步想内容流里只是页面的绘制指令flag 可能绘制成了不可见文字。什么叫不可见文字比如把文字颜色设置成跟背景一致白色背景白色字或者把文字坐标设置到页面可视区域之外比如 x100000, y100000或者字号设为 1 甚至更小。指令里依然有Tj或TJ操作符文字内容还在只是人眼看不见。用pdftotext有时候能把这些文字提取出来pdftotext flag_in_pdf.pdf但它输出的顺序不一定跟文件内一致而且可能把多个页面的文字混在一起。更稳妥的做法还是解析内容流后直接读指令。比如上面那个72 720 Td后面的(Key: flag{first_part_}) Tj虽然可能坐标超出页面但你手动看内容流时能发现它。注意Tj前面的括号内容如果是普通字符串一般能直接看到如果是十六进制字符串那需要用...括号包起来。比如666c61677b7365636f6e645f706172747d也是内容流里常见的表示转成文本就是flag{second_part}。3.3 第三步图片与隐写提取如果对象流里没有现成的 flag下一步就是找图片资源。继续用pdf-parser.py看资源对象pdf-parser.py -a flag_in_pdf.pdf | grep -E Image|XObjectPDF 里的图片通常以/Subtype /Image的对象存在有的带/Filter /DCTDecodeJPEG有的带/Filter /FlateDecode和/ColorSpace /DeviceRGB原始 RGB 数据流。对于 JPEG直接用binwalk或foremost就能提取binwalk flag_in_pdf.pdfbinwalk 输出中如果看到DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 PDF document ... 2345 0x929 JPEG image data, ...那就说明 PDF 里嵌了 JPEG用foremost分离出来foremost flag_in_pdf.pdf -o extractedforemost会把识别出的文件按类型放到extracted/jpg/或extracted/png/目录下。拿到图片之后继续走图片隐写的流程zsteg查 LSB、steghide查隐写、strings扫文本。针对 LSB 隐写我一般先跑zsteg -a extracted/jpg/00000000.jpg如果图片是 PNG 格式zsteg能力很强能看到 LSB、MSB、各种通道组合下的隐藏数据。如果输出里有类似b1,r,lsb,xy之类的通道标记后面跟着一段文本或 flag那就直接在对应通道提取。还有一种情况是图片本身看起来正常但你strings扫到图片末尾有一串密文仔细看可能是栅栏密码或十六进制编码。我自己遇到过把 flag 直接写在 JPEG 的 EXIF 里的exiftool一查就出来了。因此从 PDF 里抠出图片后要把图片当成独立的隐写题来做不能认为抠出来就结束了。3.4 第四步内容流解码与脚本处理有些题的隐藏数据不是靠现成工具直接吐出来的它可能经过了两层编码或者藏在一个你以为只是“页面布局”的流里。这时候需要自己写脚本处理。举个例子内容流里有一串看似随机的数字109 108 97 103 123 80 68 70 95 100 51 51 112 125这是十进制 ASCII用 Python 转成字符串nums [109, 108, 97, 103, 123, 80, 68, 70, 95, 100, 51, 51, 112, 125] print(.join(chr(n) for n in nums))得到flag{PDF_d33p}。还有一种更隐蔽的内容流里大量使用Td命令坐标数字本身有玄机。比如(a) Tj之前有一串Tn操作符参数像10 20 30 40 50你把它们当成 ASCII 码或者做差值计算也可能得出 flag。这一般是出题人临时想的“数字坐标隐写”没有固定套路只能靠敏感度。我的习惯是把所有对象的内容流文件 dump 出来然后统一丢进一个文本文件里用正则去搜可疑模式pdf-parser.py -f flag_in_pdf.pdf all_streams.txt grep -aE flag|[a-zA-Z0-9]{20,}|[0-9]{3,} all_streams.txt注意-f参数会让所有流都经过解码输出是解码后的真实内容。这样即使 flag 藏在某个不起眼的小流里也能被扫描到。另外推荐一个万能技巧用qpdf把对象流展开生成一个新的、未压缩的 PDF 文件然后用普通方法重新分析一遍qpdf --qdf --object-streamsdisable flag_in_pdf.pdf flag_decoded.pdf这条命令会把所有对象流Objects Stream转换成普通对象同时把 FlateDecode 压缩的内容流保留压缩如果要解压内容流还是要用 pdf-parser 或 Python zlib。展开对象流后strings的命中率会明显提升因为很多原本被压缩包裹的文本现在变成明文形式出现在新文件里了。3.5 综合例题走查从拿到文件到提交 flag为了让你感受一下完整的实战节奏我模拟一个稍微综合的题目流程。假设文件名是misc_pdf_challenge.pdf一开始pdfinfo显示有 3 页但打开看只有第 1 页有正常文字后面两页是空白。顺着流程走strings -n 8扫出f1a9、c0de这样的片段不像 flag更像是干扰项。exiftoolAuthor 字段是adminTitle 字段是一串 base64cGFydDE6IGZsYWd7U2VhcmNo解码后得到part1: flag{Search说明 flag 是分片藏在多个位置的。pdf-parser.py -a发现对象 5 是/Type /Page对象 6 是/Type /Stream对象 9 也是/Type /Stream对象 6 指向对象 9 作为其内容流。pdf-parser.py -f -o 9解码对象 9里面有一行/Im0 10 0 R说明页面引用了一个图片资源对象 10。pdf-parser.py -o 10查看对象 10发现是 DCTDecode 的 JPEG 图片流。用foremost分离出 JPEGzsteg扫出part2: _th3_H1dd3n_}。把part1和part2拼起来得到flag{Search_th3_H1dd3n_}。这个流程覆盖了元数据隐藏、对象流内容引用、图片隐写三个常见环节。每道题的环节顺序可能不同但分析路径都类似核心就是“不放过任何一个对象”。4. 常见问题与排查技巧实录4.1 常见问题速查表做过的题多了遇到的问题也就那么几类我整理成表格方便你对照排查现象可能原因解决办法strings找不到任何有效字符串flag 被压缩或编码过用 pdf-parser 逐个解开 Stream或用 qpdf 展开对象流后再扫PDF 打开后页面空白文字被设置为不可见或坐标越界提取内容流检查 Tj/TJ 指令关注颜色、坐标、字号参数有多个 Stream 但不知道哪个有用flag 可能在任意对象中写脚本循环 dump 所有 Stream统一搜flag或 base64 特征pdftotext提取出乱码字体编码映射特殊查看字体对象的 ToUnicode / Differences手动映射字符file显示不是 PDF 而是 ZIP/PNG题目是伪装的按实际文件类型走对应分析流程binwalk 扫出多个嵌入文件可能是干扰或分片逐个分离后检查注意压缩包内可能还有密码提取的图片打开失败图片数据被裁剪或加密确认 DCTDecode 流是否完整必要时从 PDF 原始字节中手工截取PDF 有多个版本/增量更新flag 藏在旧版本对象中用qpdf --qdf展开全部对象对比新旧对象差异这里面最坑的其实是“多个 Stream 但不知道哪个有用”这种情况。pdf-parser.py -a的输出可能很长对新手很不友好。我的建议是先把所有 Stream 的长度记录下来重点关注长度异常的太长或太短。出题人一般会把 flag 藏在一个独立的、跟页面切换无关的小 Stream 里这种流往往字节长度并不大但内容完整。4.2 独家避坑技巧为什么你漏掉了 flag我见过不少选手卡在“明明有提示说 flag 在 PDF 里但怎么搜都搜不到”的状态。这里分享几个我自己踩出来的经验。第一编码感知要广。PDF 里可打印字符串不一定都是 UTF-8。有些题用 UTF-16 编码字符串在strings默认编码下看起来是隔一个字节一个空字符的乱码。我遇到过一次flag 是用 UTF-16 存在元数据里的strings显示成了f\0l\0a\0g\0{\0...\0}肉眼扫的时候很容易忽略。解决办法是用strings -e l16位小端再跑一遍strings -e l flag_in_pdf.pdf第二base64 的解码结果可能不是最终 flag。有些出题人会先把 flag 进行某种变换再 base64 编码。比如rot13、十六进制、逆向字符串等。拿到一段看起来像 base64 的字符串先别急着提交解码出来看看是不是可读文本如果是乱码再尝试rot13、URL 解码、hex 解码。第三不要只依赖自动扫描工具。binwalk不是万能的有些嵌入的数据没有文件头特征binwalk 扫不出来。这时候需要人工盯十六进制视图比如在 PDF 的 EOF 后面看到PD9waHA...?php的 base64binwalk 很大概率不会识别。用xxd查看文件尾部区域xxd flag_in_pdf.pdf | tail -n 30这个习惯能帮你发现很多被工具忽略的追加数据。第四注意文件名和题目描述。攻防世界上的 PDF 题目有些文件名本身就有暗示比如stego.pdf、hidden_text.pdf、two_face.pdf。题目描述里也可能提到“注意看每一页”“第几页有东西”之类的提示。不要只埋头分析文件内容。4.3 实操心得拿到 PDF 后的标准动作清单最后把我自己的标准流程整理成一个清单适合每次做题时按这个顺序走file确认类型strings快速扫描。exiftool看元数据保存输出。pdfinfo看页数和版本。pdf-parser.py -a列出对象清单标记所有 Stream。对每个 Stream 尝试pdf-parser.py -f解码搜索 flag 特征。qpdf --qdf --object-streamsdisable展开对象流重新跑strings。binwalk扫描嵌入文件foremost分离图片或压缩包。抠出的图片走zsteg、exiftool、steghide检查。全部查漏后xxd查看文件头和尾部确认没有多余字节。如果还不行回到题目描述和文件名找线索。这套流程做完至少覆盖了 95% 以上的 PDF 类 Misc 题。耗时不会太长熟练之后 5 到 10 分钟能完整走一遍。相比之下漫无目的地点开 PDF 看半天往往什么都发现不了。从我个人的体会来看PDF 题的难点不在于某个工具不会用而在于分析时缺少系统性。很多人一看到文件就急着用binwalk或者直接拖进十六进制编辑器跳过了对象流解析这一步结果一条路走到黑。反过来只要把 PDF 当成一个“有结构的容器”按部就班地拆开每一层绝大多数题目都能在半小时内解决。希望这篇文章能帮你少走点弯路下次在攻防世界上再碰到 PDF 题至少心里有个底。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑