PDF文件格式的奥秘:从CTF挑战看多语言文件与二进制修复

本文详细解析了justCTF竞赛中的“PDF is broken”挑战,探讨了PDF文件格式的独特特性、Ruby/PDF多语言文件构造、二进制修复技巧,以及使用Polyfile等工具进行自动化分析的方法。

PDF is Broken: a justCTF Challenge

Trail of Bits赞助了最近的justCTF竞赛,我们的工程师帮助设计了多个挑战,包括D0cker、Go-fs、Pinata、Oracles和25519。在本文中,我们将介绍另一个挑战,标题为“PDF is broken, and so is this file”。它通过一个不寻常的隐写谜题展示了PDF文件格式的一些特性。CTF挑战通常涉及在干草堆中寻找隐写针,这很少具有启发性,更不用说令人愉快了。LiveOverflow最近有一个关于文件格式技巧的优秀视频,并以类似的观点结束。因此,我们设计了这个挑战,以教授justCTF参与者一些PDF技巧,以及Trail of Bits的开源工具如何轻松处理这些取证挑战。

其中PDF是一个Web服务器,提供自身的副本

挑战中的PDF文件实际上是损坏的,但大多数PDF查看器通常只会将其渲染为空白页面而不报错。file命令将挑战报告为“data”。在十六进制编辑器中打开文件,我们看到它看起来像一个Ruby脚本:

1
2
3
4
5
6
7
8
require "json"
require "cgi"
require "socket"
=begin
%PDF-1.5
%ÐÔÅØ
% <code>file</code>  sometimes lies
% and  <code>readelf -p .note</code> might be useful later

第5行的PDF头嵌入在第4行开始的Ruby多行注释中,但这并不是损坏的部分!几乎所有PDF查看器都会忽略%PDF-1.5头之前的所有内容。第7行和第8行是PDF注释,确认了我们从file命令看到的内容,以及一个我们稍后会提到的readelf提示。

Ruby脚本的其余部分嵌入在PDF对象流中——“9999 0 obj”行——它可以包含PDF忽略的任意数据。但PDF的其余部分呢?它如何不影响Ruby脚本?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
9999 0 obj
<< /Length 1680 >>^Fstream
=end
port = 8080
if ARGV.length > 0 then
  port = ARGV[0].to_i
end
html=DATA.read().encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace).split(/<\/html>/)[0]+"\n"
v=TCPServer.new('',port)
print "Server running at http://localhost:#{port}/\nTo listen on a different port, re-run with the desired port as a command-line argument.\n\n"

__END__

Ruby有一个特性,词法分析器会在__END__关键字处停止,并有效地忽略其后的一切。果然,这个奇特的PDF有这样一个符号,后面是封装PDF对象流的结尾和PDF的其余部分。

这是一个Ruby/PDF多语言文件,您可以使用类似的方法将任何PDF转换为这样的多语言文件。如果您的脚本足够短,您甚至不需要将其嵌入PDF流对象中。您可以将其全部附加在%PDF-1.5头之前。尽管一些PDF解析器如果在文件的前1024字节内找不到头会报错。

您没想过会这么容易,对吧?

所以让我们勇敢地尝试将PDF作为Ruby脚本运行。果然,它运行了一个Web服务器,提供一个带有“flag.zip”下载链接的网页。哇,这很容易,对吧?进一步检查Ruby脚本,您会发现下载是PDF文件本身重命名为.zip。是的,除了是Ruby脚本之外,这个PDF也是一个有效的ZIP文件。PoC||GTFO多年来一直使用这个技巧,这也可以通过运行binwalk -e在挑战PDF上观察到。

解压PDF会产生两个文件:一个MμPDF mutool二进制文件和false_flag.md,后者建议玩家通过mutool二进制文件运行损坏的PDF。

显然,这个版本的mutool被修改为正确渲染损坏的PDF,尽管它有什么“损坏”。CTF玩家是否应该逆向工程这个二进制文件来找出修改了什么?如果有人尝试了,或者他们尝试了上面嵌入为PDF注释的readelf线索,他们可能会注意到这一点:

您应该做的第一件事是:在十六进制编辑器中打开PDF。您可能需要“修复”PDF,以便它可以被普通的PDF阅读器解析。您可以逆向这个二进制文件来找出如何做到这一点,但使用它来渲染PDF、跟随线索并将原始PDF对象与“常规”PDF的对象进行比较可能更容易。您可能只需要用bbe修复它!

二进制块编辑器(bbe)是一个类似于sed的实用程序,用于编辑二进制序列。这意味着导致PDF渲染为空白页面的任何问题都可以通过二进制正则表达式轻松修复。

深入探索

当我们使用修改版的mutool渲染PDF时,它会产生这个表面上无意义的模因蒙太奇:

使用修改版的mutool渲染的“损坏”挑战PDF。

在Google中搜索LMGTFY字符串将带您到Didier Stevens的优秀文章,详细描述PDF流格式,包括PDF对象如何编号和版本化。一个重要因素是,两个PDF对象可以有相同的编号但不同的版本。

页面上的第一个提示标识了PDF对象1337,所以那可能很重要。Stevens文章中的图表单独与损坏PDF的流对象的十六进制转储并列,清晰地描绘了更改的内容。

Didier Stevens的PDF流对象注释图。

1
2
3
4
5
5 0 obj
<< /Length 100 >>^Fstream
endstream
endobj

挑战PDF中的PDF流对象。

正如提示所建议的,PDF规范只允许六种空白字符:\0、\t、\n、\f、\r和空格。ZIP中的mutool版本被修改为也允许ACK(0x06)作为第七种空白字符!果然,在文件的第十二行我们看到:

1
>>^Fstream

那个“^F”是一个ACK字符,而PDF规范说那里应该是空白!所有PDF对象流都类似地损坏了。这可以通过以下方式修复:

1
bbe -e "s/\x06stream\n/\nstream\n/" -o challenge_fixed.pdf challenge.pdf

解决谜题

修复文件是否严格必要来解决挑战?不,可以使用十六进制编辑器在PDF对象0x1337中找到标志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
4919 0 obj
<< /Length 100 /Filter /FlateDecode >>^Fstream
x<9c>^MËA^N@0^PFá}OñëÆÊÊ        <88>X;^Ba<9a>N<8c>N£#áöº~ßs<99>s^ONÅ6^Qd<95>/°<90>^[¤(öHû       }^L^V   k×E»d<85>fcM<8d>^[køôië<97><88>^N<98> ^G~}Õ\°L3^BßÅ^Z÷^CÛ<85>!Û
endstream
endobj
4919 1 obj
<< /Length 89827 /Filter [/FlateDecode /ASCIIHexDecode /DCTDecode] >>^Fstream
endstream
endobj

并手动解码流内容。Binwalk甚至会自动解码第一个流,因为它可以解码Flate压缩。其中包含:

1
2
3
pip3 install polyfile
Also check out the `--html` option!
But you’ll need to “fix” this PDF first!

Binwalk不会自动扩展第二个流,因为它还使用ASCIIHex和DCT PDF过滤器编码。一个没有跟随所有线索且还不熟悉PDF规范的随意观察者可能甚至没有意识到PDF流对象0x1337的第二个版本存在!而那个就是包含标志的版本。当然,可以梳理binwalk提取的数十个文件来手动解码标志,甚至直接从十六进制编辑器中的流内容,通过快速实现PDF的解码器。但为什么要这样做,当Polyfile可以为您做呢?

1
polyfile challenge_fixed.pdf -html challenge_fixed.html

挑战PDF的PolyFile HTML输出。

哦,嘿,那是PDF对象的分层表示,带有交互式十六进制查看器!我们去看对象0x1337的流怎么样?

我们立即看到PDF对象0x1337。

PolyFile可以自动解码对象。

最后,让我们看看对象0x1337的第二个版本,包含多层编码的标志:

PolyFile自动解码分层的PDF过滤器以产生标志。

标志!

结论

PDF是一个非常……灵活的文件格式。仅仅因为PDF看起来损坏,并不意味着它是。而仅仅因为PDF损坏,并不意味着PDF查看器会告诉您它是。PDF核心是一个容器格式,让您编码任意二进制块,这些块甚至不必贡献于文档的渲染。而这些块可以堆叠任意数量的编码,其中一些是PDF的定制特性。如果这让您感兴趣,请查看我们关于文件 treacherous 的演讲,以及我们驯服它们的工具,如Polyfile和PolyTracker。

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

页面内容 其中PDF是一个Web服务器,提供自身的副本 您没想过会这么容易,对吧? 深入探索 解决谜题 结论 最近文章 Trail of Bits的Buttercup在AIxCC挑战中获得第二名 Buttercup现已开源! AIxCC决赛:记录表 攻击者的提示注入工程:利用GitHub Copilot 作为新员工发现NVIDIA Triton中的内存损坏 © 2025 Trail of Bits。 使用Hugo和Mainroad主题生成。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计