揭秘libmagic:一个古老而神秘的二进制文件识别库

本文深入探讨了libmagic库的内部工作机制,包括其独特的领域特定语言(DSL)、实现过程中的各种挑战,以及为何要开发纯Python替代方案PolyFile。文章揭示了libmagic中许多未文档化的特性和潜在bug,并展示了PolyFile如何提供更安全、功能更丰富的替代方案。

libmagic: The Blathering

几年前我们发布了PolyFile:一个用于识别和映射文件语义结构的工具,包括混合文件、嵌合文件和"精神分裂"文件。它有点像file、binwalk和Kaitai Struct的结合体。PolyFile最初使用TRiD定义数据库进行文件识别。然而,这个数据库既太慢又容易误分类,所以我们决定改用libmagic——file命令背后无处不在的库。

下面是我们开发纯Python版libmagic时发现的各种奇怪现象的汇编。

魔法奥秘

libmagic库比地球上超过一半的人类年龄还要大,但它仍在积极开发中,并且是安装频率最高的Ubuntu软件包之一(位于99.9百分位)。该库的持续开发不仅限于bug修复和支持新文件格式匹配;它还经常收到破坏性变更,为其匹配引擎添加新的核心功能。

libmagic有一个自定义的领域特定语言(DSL)用于指定文件格式模式。运行man 5 magic可以阅读其文档。该程序将其文件格式模式的DSL数据库编译成单个定义文件,通常安装到/usr/share/file/magic.mgc。libmagic是用C语言编写的,包含几个手动编写的解析器来识别各种难以用其DSL表示的文件类型(例如JSON和CSV)。不出所料,这些解析器导致了许多内存安全漏洞和大量CVE。

魔法DSL

为了理解我们在重新实现libmagic时发现的可怕事物,我们需要简要概述其深奥的DSL。每个DSL文件包含一系列测试(每行一个),用于匹配文件的子区域。这些测试可以简单到匹配魔法字节序列,也可以复杂到看似图灵完备的表达式。(证明图灵完备性留给读者作为练习。)

file命令执行DSL测试来分类输入文件。测试在DSL中组织为树状层次结构。首先,执行每个顶级测试。如果测试通过,则按顺序测试其子项。任何级别的测试都可以选择打印消息或将输入文件与MIME类型分类关联。

DSL文件中的每一行都是一个测试,包括偏移量、类型、期望值和消息,用空格分隔。例如:

1
10    lelong    0x00000100    this is a test

这行将执行以下操作:

  1. 从输入文件的字节偏移量10开始
  2. 读取一个有符号小端长整型(4字节)
  3. 如果这些字节等于0x100,则打印"this is a test"

现在让我们添加一个子测试,并将其与MIME类型关联:

1
2
3
10        lelong    0x00000100    this is a test
>20       ubyte 0xFF       test two
!:mime    application/x-foo

第二行测试前的">“表示它是先前定义的高层测试的子项。

这个新版本将执行以下操作:

  1. 当且仅当第一个测试匹配时,尝试第二个测试
  2. 如果文件偏移量20处的字节等于0xFF,则打印"test two"并将整个文件与MIME类型application/x-foo关联

到目前为止,这些示例中的所有偏移量都是绝对的,但libmagic DSL也允许相对偏移量:

1
2
10      lelong    0x00000100    this is a test
>&20    lelong    0x00000200    this will test 20 bytes after its parent match offset, equivalent to absolute offset 10 + 20 = 30

以及间接偏移量:

1
(20.s)      lelong    0x00000100    indirect offset!

这里的(20.s)表示:在文件的绝对字节偏移量20处读取一个小端短整型,并将该值用作要测试的有符号小端长整型(lelong)的偏移量。

未受控制的恶作剧

在开发libmagic的独立实现过程中(直到它能解析file命令的整个魔法定义集合并通过所有官方单元测试),我们发现了许多未文档化的DSL特性和明显的上游bug。

文档不全的语法

例如,匹配MSDOS文件的DSL模式中包含一个文档不全的间接偏移量括号用法:

1
(&0x10.l+(-4))

语义是模糊的;这可能意味着"从父匹配点过去0x10字节处读取的小端长整型减4”,或者"从父匹配点过去0x10字节处读取小端长整型并加上从文件最后四个字节读取的值"。事实证明是后者。

未文档化的语法

elf模式使用了一个未文档化的${x?true:false}三元运算符语法。这种语法也可以出现在!:mime指令中!

一些规范,如CAD文件格式,使用未文档化的regex /b修饰符。从libmagic源代码中不清楚这个修饰符是被忽略还是有目的。PolyFile目前忽略它并允许正则表达式应用于ASCII和二进制数据。

Bug

libmagic DSL有一个专门用于匹配全局唯一标识符(GUID)的类型,遵循RFC 4122定义的标准结构。DSL中微软高级系统格式(ASF)多媒体容器的一个定义不符合RFC 4122——它短了两个字节。大概libmagic会静默忽略无效的GUID。我们发现了它,因为PolyFile会根据RFC 4122验证所有GUID。这个bug从2019年12月就存在于libmagic中,直到我们2022年4月向libmagic维护者报告。在此期间,PolyFile有一个针对该bug的变通方案,并且一直使用正确的GUID。

元游戏

PolyFile是libmagic的一个更安全的替代品,几乎功能兼容。

1
2
3
4
5
6
$ polyfile -I suss.png
image/png………………………………………………………..PNG image data
application/pdf…………………………………………………..Malformed PDF
application/zip…………………………………………………..ZIP end of central directory record Java JAR archive
application/java-archive…………………………………………..ZIP end of central directory record Java JAR archive
application/x-brainfuck……………………………………………Brainf*** Program

PolyFile甚至有一个交互式调试器,模仿gdb,用于在匹配过程中调试DSL模式。(见-db选项。)这对于libmagic和PolyFile的DSL开发人员都很有用。但PolyFile能做的远不止这些!例如,它可以可选地输出一个交互式HTML十六进制查看器,映射出文件的结构。它是免费和开源的。您现在可以通过运行pip3 install polyfile或克隆其GitHub仓库来安装它。

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