libmagic: 喋喋不休 - Trail of Bits博客
Evan Sultanik
2022年7月1日
研究实践
几年前我们发布了PolyFile:一个用于识别和映射文件语义结构的实用工具,包括多语言文件、嵌合文件和分裂文件。它有点像将file、binwalk和Kaitai Struct全部融合在一起。PolyFile最初使用TRiD定义数据库进行文件识别。然而,该数据库速度太慢且容易误分类,因此我们决定转向libmagic,这是file命令背后无处不在的库。
以下是我们开发libmagic纯Python净室实现时发现的奇特现象的汇编。
魔法奥秘
libmagic库比地球上一半以上的人口还要古老,但它仍在积极开发中,并且位于最常安装的Ubuntu软件包的前99.9%。该库的持续开发不仅限于错误修复和支持匹配新文件格式;该库经常收到破坏性更改,为其匹配引擎添加新的核心功能。
libmagic有一个自定义领域特定语言(DSL)用于指定文件格式模式。运行man 5 magic
阅读其文档。该程序将其文件格式模式的DSL数据库编译成单个定义文件,通常安装到/usr/share/file/magic.mgc。libmagic用C语言编写,并包含几个手动编写的解析器来识别各种文件类型,这些类型在其DSL中难以表示(例如JSON和CSV)。不出所料,这些解析器导致了许多内存安全错误和大量CVE。
PolyFile用Python编写。虽然libmagic确实有官方和独立的Python包装器,但我们选择创建净室实现。除了原生库的安全问题外,我们决定创建新东西还有几个额外原因:
- PolyFile已经用纯Python编写,我们不想引入原生依赖(如果可能避免的话)。
- PolyFile旨在检测多语言文件和其他libmagic会遗漏的时髦文件格式,因此我们无论如何都必须扩展libmagic。
- PolyFile在整个解析过程中保留词法信息(如输入字节偏移),以便将语义映射回原始文件位置。使用libmagic没有直接的方法可以做到这一点。
在用比C语言更具内存安全性的语言重新实现libmagic的想法并不新颖。与PolyFile实现同时进行的Ruby努力(称为Arcana)仍然不完整。另一方面,PolyFile正确解析了libmagic的整个模式数据库,并通过了除两个之外的所有libmagic单元测试,并且在Ange Albertini的900多个文件Corkami语料库上正确识别了至少与libmagic一样多的MIME类型。
魔法DSL
为了理解我们在重新实现libmagic时发现的诡异恐怖,我们需要简要概述其深奥的DSL。每个DSL文件包含一系列测试——每行一个——匹配文件的子区域。这些测试可以简单到匹配魔术字节序列,也可以复杂到看似图灵完备的表达式。(证明图灵完备性留给读者作为练习。)
file命令执行DSL测试以对输入文件进行分类。测试在DSL中组织为树状层次结构。首先,执行每个顶级测试。如果测试通过,则按顺序测试其每个子项。任何级别的测试都可以选择打印消息或将输入文件与MIME类型分类关联。
DSL文件中的每一行都是一个测试,包括偏移量、类型、预期值和消息,用空格分隔。例如:
|
|
这行将执行以下操作:
- 从输入文件的字节偏移10开始
- 读取一个有符号小端长整型(4字节)
- 如果这些字节等于0x100,则打印“this is a test”
现在让我们添加一个子测试,并将其与MIME类型关联:
|
|
第二个测试中偏移“20”前的“>”表示它是先前定义的高级测试的子项。
这个新版本将执行以下操作:
- 当且仅当第一个测试匹配时,尝试第二个测试。
- 如果文件偏移20处的字节等于0xFF,则打印“test two”并将整个文件与MIME类型application/x-foo关联。
请注意,即使其子项不匹配,父测试的消息也会被打印。子测试仅在其父项匹配时执行。子项可以通过额外的“>”前缀任意嵌套:
|
|
如果测试通过,则测试其所有子项。
到目前为止,这些示例中的所有偏移都是绝对的,但libmagic DSL也允许相对偏移:
|
|
以及间接偏移:
|
|
这里的(20.s)表示:在文件的绝对字节偏移20处读取一个小端短整型,并使用该值作为偏移量来读取将被测试的有符号小端长整型(lelong)。间接偏移还可以包括算术修饰符:
|
|
相对和间接偏移也可以组合:
|
|
偏移量非常复杂!
尽管存在了几十年,libmagic模式DSL仍在积极开发中。
恶作剧,未管理
在开发libmagic的独立实现——达到可以解析file命令的整个魔术定义集合并通过所有官方单元测试的程度——我们发现了许多未文档化的DSL功能和明显的上游错误。
文档不全的语法
例如,匹配MSDOS文件的DSL模式包含间接偏移中括号的文档不全用法:
|
|
语义是模糊的;这可能意味着“从父匹配后0x10字节处的小端长整型读取偏移量减四”,或者可能意味着“从父匹配后0x10字节处的小端长整型读取偏移量并添加从文件最后四个字节读取的值”。事实证明是后者。
未文档化的语法
elf模式使用未文档化的${x?true:false}三元运算符语法。这种语法也可以出现在!:mime指令内部!
一些规范,如CAD文件格式,使用未文档化的regex /b修饰符。从libmagic源代码不清楚这个修饰符是被忽略还是有目的。PolyFile目前忽略它并允许正则表达式应用于ASCII和二进制数据。
根据文档,search关键字——从给定偏移执行字面字符串搜索——应该后跟整数搜索范围。但这个搜索范围显然是可选的。
一些规范,如BER,使用“search/b64”,这是未文档化的语法。PolyFile将其视为等效于合规的search/b/64。
regex关键字有一个未文档化的T修饰符。什么是T修饰符?从libmagic的代码判断,它似乎从结果匹配中修剪空白。
错误
libmagic DSL有一个专门用于匹配全局唯一标识符(GUID)的类型,遵循RFC 4122定义的标准结构。DSL中Microsoft高级系统格式(ASF)多媒体容器的一个定义不符合RFC 4122——它短了两个字节。大概libmagic静默忽略无效的GUID。我们捕获了它,因为PolyFile根据RFC 4122验证所有GUID。这个错误从2019年12月存在于libmagic中,直到我们在2022年4月向libmagic维护者报告。在此期间,PolyFile有一个针对该错误的变通方法,并且一直使用正确的GUID。
元游戏
PolyFile是libmagic的更安全替代品,几乎功能兼容。
|
|
PolyFile甚至有一个交互式调试器,模仿gdb,用于在匹配期间调试DSL模式。(参见-db选项。)这对于libmagic和PolyFile的DSL开发人员都很有用。但PolyFile可以做得更多!例如,它可以可选地输出交互式HTML十六进制查看器,映射出文件的结构。它是免费和开源的。您可以通过运行pip3 install polyfile或克隆其GitHub存储库立即安装它。