打靶回来那天,我对着一个pdf题盯到半夜。文件不大,内存里跑出来是一张特别正常的图片,压着一段说明文字,怎么看都只是个普通的文档。但题就是题,flag不可能平白无故消失。后来我耐着性子把pdf拆到对象级,才发现里面藏了个体积明显偏大的对象,把它拖出来一看,是纯文本的flag。从那以后我做pdf类的Misc题有了固定套路,今天把这套东西完整写出来。
1. Misc题里PDF的几种出题姿势,先看懂出题人在干什么
攻防世界Misc分类下的PDF题,表面是考文件格式,实际考的是你对PDF对象模型的理解程度。PDF在底层是一堆对象的集合,页面、字体、图像、元数据全都以对象形式存在,这就给藏东西提供了天然的温床。
我按藏匿方式把这类题目分了几类,每类的解法思路完全不同,先建立框架再做具体分析,比一上来就盲目找flag高效得多。
| 题型类型 | 藏匿位置 | 难度 | 关键突破口 |
|---|---|---|---|
| 元数据型 | /Info字典的Title、Author等字段 | 入门 | 直接strings或pdf-parser扫对象 |
| 隐藏文本型 | 页面文字白色/透明/偏移/极小字号 | 入门 | alltext提取,肉眼检查 |
| 图片内置型 | 图片内部藏另一张图或文件 | 中等 | binwalk分离,二次隐写挖掘 |
| 隐藏对象型 | 独立对象存flag,页面不引用 | 中等 | 逐个对象检查,识别异常字节量 |
| 密码保护型 | 加密后flag在打开后才能看到 | 中等 | 弱口令爆破,哈希提取 |
| 文件损坏型 | 文件头缺失导致无法正常解析 | 偏难 | 修复结构后用解析工具提取 |
我遇到最多的是前四类。覆盖了八成以上的题面,先把这些吃透,考试和比赛都够用了。
稍微说句大白话:出题人把flag藏进PDF,本质上就是在对象堆里塞一些“看上去正常、实际违反常规”的东西。我们的任务就是把这堆对象逐层扒开,找出那个不符合常规的对象。所以做题第一步,永远是先看对象的数量和体积。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工具链准备:不需要装一堆东西,这几样配齐就能开始
很多人一上来就装全家桶,这其实是浪费时间的。PDF分析场景下,工具要的是“能拆到对象级”和“能提取子文件”两个能力,满足这两个能力就够用了。
我个人习惯在Linux环境下操作,Kali或Ubuntu都行。不想开虚拟机的话,Windows下装WSL也一样跑。以下是我一直保留的固定配置:
2.1 必备工具清单与安装方式
我不爱用图形界面工具,命令行为主,因为调试和脚本化方便。工具列表如下:
- qpdf:可以快速查看PDF对象结构、提取任意对象、解压对象流。安装方式:apt install qpdf
- pdf-parser.py:这是我自己最常用的工具,某安全工具包自带,适用于逐对象分析。作用是遍历PDF的所有对象并列出详情。安装方式:从官方源下载pdf-parser.py,Python直接运行
- binwalk:检测文件中的嵌入文件和异常结构。安装方式:apt install binwalk
- foremost:按文件特征批量提取残留数据。安装方式:apt install foremost
- strings:系统自带,用于提取可打印字符序列,查元数据和隐藏字符串最快。
- pdftotext / pdfimages:poppler-utils里面带的,负责抽取文字和图片。安装方式:apt install poppler-utils
- hashcat:跑密码保护型PDF的爆破。也可以先用john。安装方式:apt install hashcat
我一开始只用strings,后来发现strings在压缩状态数据面前完全失效,才逐步补全了上面这套。实话说,真正的高效组合是:pdf-parser负责定位可疑对象,qpdf负责精准提取,binwalk负责分离内嵌文件,三件套互相补位。
2.2 环境配置里的两个细节
第一个细节是确认poppler-utils装好了,因为pdftotext的名字在不同的发行版上可能被改成pdftotext或mutool,如果命令找不到就搜索一下包名。第二个细节是脚本类工具需要python3环境,个别工具依赖旧版python库,建议单独用venv跑,避免污染系统环境。
配置这块我踩过一次坑:顺手装了某个图形化PDF分析工具,结果它把所有对象全部解压后又重新压缩,原文件的隐藏对象被“修复”得干干净净。从那之后我有一个原则——先用只读工具排查,确认可疑目标后再用可写工具提取。
提示:任何可能改写文件本身的操作(比如另存为、重压缩)都要放到最后做,前置阶段尽量只读分析。PDF隐写最怕的就是你无意中把flag“修复”没了。
3. 一题一练:从完整排查链路看隐藏PDF对象的提取
下面用一道典型的“隐藏对象型”题目来演示整个过程。题目拿到手,文件名叫misc_question.pdf,大小2.1MB。双击打开,是一个完整页面,右侧有一张图,中间一小段文字,没看出任何异常。这种体型2MB以上、却只显示一张图和一小段字的PDF,本身就该引起警觉——正常纯图文PDF哪里需要这么大的体积。
3.1 第一步:strings粗扫元数据与明文内容
先跑最简单的命令:
bash复制strings misc_question.pdf | head -50
输出内容是常见的PDF字典结构,还有字体子集名,看起来很正常。这其实给了第一个信息:文件没有加密(如果有加密流,strings里通常会看到类似Encrypt字样的对象引用)。
再看一眼完整strings输出里有没有可疑的base64长串,以及有没有http链接。如果flag恰好是纯文本存放在未被压缩的对象里,这一步就能直接写答案,省掉后面的功夫。但对这个题来说,strings没有给任何关键词,说明flag大概率被藏进了压缩数据或深层对象里。
3.2 第二步:pdf-parser.py遍历对象,逐个盯体积
接着用pdf-parser.py做对象级体检:
bash复制python3 pdf-parser.py -f misc_question.pdf
输出列出了每个对象的编号、类型、字节长度、是否被压缩。我做的第一件事是扫一眼对象长度列表。正常文档里,图像对象会很大(几百KB级别),字体对象几十KB,内容流几百字节到几KB。如果有一个对象体积和同类型对象差异特别大,它就有问题。
这个题的输出里,对象12是一个FlateDecode压缩的流,长度大约是180KB。这个数字很扎眼,因为周围其他内容流基本都只有几百字节,唯独它占据如此大的体积。180KB的“内容流”,里面十有八九不只是内容。
3.3 第三步:qpdf精准提取与解压被怀疑的对象
确认对象12可疑后,用qpdf把它抠出来:
bash复制qpdf --qdf --object-streams=disable misc_question.pdf unpacked.pdf
这个命令会把所有对象流拆散,保留为独立对象并解压内容流。处理之后我直接在解包结果里搜索flag文本:
bash复制strings unpacked.pdf | grep -i flag
搜索结果显示了一段看起来像flag的文本,但格式是段落形式,不是标准的flag字符串。这证明思路是对的,只是数据仍嵌套在某种编码里。
此时可以更进一步,针对性地导出对象12:
bash复制qpdf --show-object=12 misc_question.pdf
仔细观察输出后发现,对象12实际包含的是一段base64编码的长文本。把base64解码还原后,得到的是一个纯文本内容块,里面醒目地写着flag字样。
到这里题目已经解完了,但我额外做了一步验证:检查这个对象是否被页面字典引用。结果页面引用的内容流其实是对象8,对象12完全是一个“孤儿对象”——不在任何页面引用链上。这就直接坐实了它是人为塞进去的。
3.4 第四步:提取内容后的二次检查
拿到flag之后不要急着提交,先确认它是否完整。很多题目会用base64再加一层编码,或者把flag劈成几段分藏在多个对象中。验证方法是在解包后的pdf上跑一次全量strings,把所有疑似flag片段都拼出来看,别落下任何一个。
这种“塞进孤立对象”的出题方式在攻防世界的Misc题里出现频率不低。我一见到2MB以上但页面元素极少的PDF文件,就会先把对象列出来检查一遍体积差异,基本百发百中。
4. 图片不是终点:PDF内的图片隐写和双层嵌套
PDF题里很多附件本身就是图片,或者PDF页面中含大图,flag藏在图片的隐写数据里,再以PDF为载体打包发送。这种题最难的点在于:新手会觉得PDF分析要围绕PDF本身进行,但实际情况是你得把PDF里的图片提取出来,然后进入图片隐写的分析流程。
4.1 图片提取与判断是否值得二次分析
提取图片用pdfimages最方便:
bash复制pdfimages -list misc_question.pdf
pdfimages -png misc_question.pdf img_prefix
前一个命令列出所有图片的参数,后一个命令直接导出为PNG图片。导出的图片文件名格式是img_prefix-000.png、img_prefix-001.png,顺序对应页面里出现的图片顺序。
控制台输出的每行记录里,重点看“image size”和“colorspace”两列。一张藏了数据的图片经常会有尺寸和视觉表现不匹配的情况,比如明明只在页面右下角占了一小块区域,尺寸却特别大。用这个特征筛一遍图片,能帮自己省下大量盲目核对的时间。
4.2 从PNG到PNG:夹带文件与LSB隐写
假设导出的PNG图片在steghide或zsteg下报出隐藏数据,就得继续做图片隐写的处理。这个链条初学者容易在中间断掉:一开始分析PDF,分析到出图后不知道该切换到图片隐写工具,导致功亏一篑。
我对这类题的操作顺序是:
- 先用binwalk扫一遍导出的图片,确认有没有嵌入zip/rar文件。有的话用foremost或dd按偏移切割,优先解出压缩包内容。
- 没嵌文件就上LSB隐写检测,zsteg或stegsolve都行,处理PNG特别好用。
- 检测完后用steghide试试,如果题目备注里给了密码或者常见的弱密码,steghide解出来的概率不低。
binwalk在PDF分析里还有一个额外妙用:可以不经pdfimages,直接对原PDF文件做整包扫描,有时候能直接看到内部嵌入的ZIP明文偏移量。这相当于一种“绕路”解法,PDF繁琐的对象结构直接跳过,直奔文件特征码。
4.3 双层嵌套的一个实战提醒
有次解一道题,binwalk从PDF里切出了一个jpg,jpg里扫出了隐藏的rar,rar里面又有一个txt,txt里才是flag。整个过程像套娃,但每一步工具都没变,就是binwalk+foremost的组合不断重复。所以我对做Misc题的建议一直是:拿到任何中间产物,先过一遍binwalk再继续阅读,形成条件反射,能省掉大量走回头路的时间。
5. 文本被藏起来的类型:压缩流、透明字体和偏移排版
另一种常见题型是flag被以“页面可见但不是肉眼可见”的方式藏起来。这类题考验的是PDF文本提取能力,而不是对象分析能力。
5.1 白色文字与透明文字的提取思路
页面里的文字在视觉上被隐藏,常见手法有白色字体、字号缩小到几乎不可见、文字被图片覆盖住等。这类文字在PDF的内容流里依然是正常的“绘制文字”操作,只是颜色或坐标不对,pdftotext依然能把它抓出来。
bash复制pdftotext misc_question.pdf extracted.txt
cat extracted.txt
很多情况下,flag会以正常文本形式出现在提取结果里,只是人眼没看到。如果pdftotext没有抓到任何额外内容,则可以看一下内容流指令,检索Tj、TJ操作后的字符串。PDF内容流里文本是通过这两个操作绘制的,编写脚本时扫描到Tj或TJ,把随后的括号内字符串全部打印出来,这种方法能抓出所有可视或不可视的文本。
5.2 字体子集与编码的坑
用pdftotext抓不到内容时,要考虑字体子集的因素。用自定义字体嵌入的PDF会把字符映射到私有Unicode区域,pdftotext在这种场景下经常形成乱码或空白。这时候需要借助一些支持字体映射的工具,把内容流里的字码映射表识别出来,再手工替换成可见字符。说实话,这类题是比较少见的,真遇上了就得做好手摘数据的准备,把内容流中每一段的字码和字体ToUnicode映射进行对照。
我见过一个题目,所有flag字符被随机打散到页面坐标里,每个字符间距特别大,pdftotext虽然提取到了全部字码,但字母顺序错乱。解决办法是解析内容流里每个文本绘制指令的坐标,按坐标重新排序字符。这一步看起来繁琐,但逻辑很直接:先按y坐标分行,再按x坐标排序。
5.3 一个快速排查顺序
我再把自己的排查顺序写出来,供直接抄作业:
- 先pdftotext提取全文。
- 提取不到就用pdf-parser.py看每个内容流对象,用zlib解压后搜索flag字样。
- 内容流里有字码但读不懂,考虑字体映射。
- 页面显示正常文字但没有任何隐藏内容,检查页面对象里是否另有不被引用的内容流。
这个顺序能把90%的隐藏文本题解决掉。我遇到最多的问题是大家只走到第二步就卡住,其实再往下走一步就能看到答案。
6. 密码保护型与损坏修复:两道偏向进阶的题型
这两类题型频率低于前面几类,但在攻防世界确实出现过。它们对工具选型和原理理解的要求更高一些,所以我单独拎出来讲。
6.1 密码保护型PDF:爆破前的三件事
PDF加密分两种:口令加密和权限加密。口令加密是打开就需要密码,权限加密则是可以打开但不能复制打印。Misc里常见的要爆破的是打开口令。
爆破之前先确认三件事:
- 文件是否真的加密,用pdf-parser.py看看有没有Encrypt字典。
- 密码字典范围,有没有可能在题目描述里给出线索,比如文件名暗示或题目文案含某个单词。
- 暴力程度,弱密码用john自带的password.lst先跑跑,复杂密码才考虑hashcat。
提取PDF内部哈希并交给john跑:
bash复制pdf2john.pl misc_question.pdf > hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
如果题目明确是四位纯数字,hashcat可以更快:
bash复制hashcat -m 10500 -a 3 hash.txt '?d?d?d?d'
模式10500对应PDF 1.1到1.3的加密哈希,近几年的PDF 1.7格式则是10700模式,先确认版本再选择模式,不然跑半天出不来结果。
6.2 损坏型PDF的修复逻辑
遇到打不开的PDF,不要急着扔进修复工具里一键修复。一手损坏文件在CTF里往往是“人为制造”的结果,常见的标志是文件头被替换、xref表被清除、或部分对象被改写。修复时要抓住一个核心逻辑:PDF的解析入口在根对象,只要根对象还在,剩下的对象即使链表断裂也能手工拼回来。
用编辑器把文件头还原为%PDF-1.x,如果仍然打不开,就搜索/Root关键字,找到根对象引用,手工修正一个合理的偏移值。这个概念听起来复杂,实操中有几个标志性线索能帮你调整:startxref后面标注的字节偏移一旦与实际不符,PDF阅读器就报错,把它改成Root对象正确的偏移即可。
一个简化的修复流程:
- 找到最后一个
%%EOF之前的startxref位置。 - 确认Root对象编号,再去文件里搜索该对象的实际偏移。
- 修正
startxref的值为实际偏移。 - 用qpdf的
--check验证修复结果。
修复完就能正常打开,隐藏内容就会在页面里显示出来。这类题考察的核心是PDF文件结构,会的人三分钟搞定,不会的人一直在那边打不开文件。它提醒我们,PDF文件的物理结构不仅在解析题里有用,在修题型里同样重要,值得花时间理解。
7. 我踩过的坑和沉淀下来的几条判断经验
见到的PDF题越多,越发现真正的问题不在题目本身,而在做题时容易进入思维误区。总结几条自己踩过或看别人踩过的坑,给后来者一点参照。
7.1 误区一:上来就strings,忘记看体积差异
strings确能解决入门题,但同时也让人错过大量分析信号。对象体积列表的价值比strings高得多,任何时候都应该先扫一眼对象级的信息。一个正常的PDF文件,对象数量和类型分布与页面内容强相关;一旦出现某类对象的大小远超同类,这就是最明确的警报信号。
推荐的习惯是:拿到PDF文件先跑一遍pdf-parser.py,并且列出每个对象的字节数,把异常数据记下来再进入下一步。
7.2 误区二:分析完PDF就停止,不知道还要继续拆图片
PDF里藏图片的内嵌文件,图片又再藏压缩包,这一套路在Misc里非常普遍。很多人在PDF对象级翻了个遍,没发现flag就直接认为题出错了,殊不知中间产物这一步就被浪费掉了。我现在养成的习惯是:任何从PDF里提取出来的非文本内容,全部交给binwalk过一次,宁可多扫不可跳过。
7.3 误区三:用编辑器打开PDF就去找flag文本
PDF内部文字大量被压缩和编码,直接打开文件看到的是乱码居多。如果非要看原文字,务必先解压内容流。这个过程一般用qpdf把对象流禁用掉一遍,再搜索目标字符串。我见不少人用记事本打开PDF之后对着乱码发愁,真替他们着急。
7.4 几个对解题帮助很大的判断信号
- 文件大小明显偏大:页面元素很少但文件几MB,先怀疑是否有附加对象或嵌入文件。
- 页面图片数量与可见图片数量不一致:pdfimages -list看列表,对比页面上实际出现的图数。
- 多个对象引用同一份内容流:检查是否为权限验签或人为藏匿。
- 根对象附近有可疑的孤儿对象:页面不引用但对象存在,优先提取。
- 题目文件名带数字或特殊词:可能是密码提示、偏移提示或压缩包密码提示。
用这一套判断逻辑,即便面对全新的PDF题,也能在几分钟内根据经验确定优先级,而不是盲目翻找。
8. 写在最后的操作习惯
经常有人问我,做PDF的Misc题需要很深的PDF格式知识吗。我的回答是:了解对象模型就够用,剩下的靠固定流程和反复练习。
我做这类题目前会先跑一遍固定流程:查元数据、列对象、看体积差异、提取可疑对象、分离子文件。流程跑完再针对性地做图片隐写或文本提取。坚持这套顺序之后,我做PDF题的效率比过去高了一大截,基本上拿到题目都能快速定位到关键位置。
最后分享一个我自己的小技巧:把常用命令和脚本封装成一个简单的分析脚本,自动列出对象列表和可疑文件类型,每次拿到新题目先跑脚本,输出结果后再人工细分析。这样既省体力又不会漏掉明显的异常。
对付PDF的Misc题,耐心比技巧重要。对象堆里找异常就像在一堆快递盒里找夹带包裹,先把所有盒子过一遍安检,再逐个拆开验证,答案早晚会出来。
