← 返回信息流
AI 资讯Hacker News·3 小时前

重新审视 Teensy 可执行文件

原标题:The Teensy Executable Revisited

速览

本文回顾了 Teensy 可执行文件的相关技术细节。分析了其在嵌入式开发中的优势与潜在问题。为开发者提供了更深入的理解与参考。

AI 深度解读

重新审视 Teensy Executable:在标准与极限之间走钢丝

背景

在之前的文章中,作者展示了一个仅 45 字节的 Linux ELF 可执行文件。这个文件之所以能运行,是因为它利用了 Linux 内核当前版本的宽容性,内核将其误认为是一个合法的 ELF 文件。然而,这一做法遭到了许多“纯粹主义者”的质疑,他们认为这 45 字节的文件并不符合 ELF 规范,仅仅是一个侥幸被内核接受的“畸形”文件。

为了回应这些批评,并满足我们内心对严谨性的追求,作者创作了这篇续作。目标非常明确:在严格遵循已发布的 ELF 规范的前提下,尽可能压缩可执行文件的大小。这不再是一场关于“内核会容忍什么”的游戏,而是一场关于“规范允许什么”的极限挑战。

核心内容

从 91 字节到 83 字节:利用规范允许的重叠

回溯到之前的 91 字节版本,作者发现 ELF 规范明确允许文件内的不同数据结构发生重叠。在 91 字节版本中,ELF 文件头(ELF Header)和程序头表(Program Header Table)之间存在 8 字节的空隙。既然规范允许重叠,作者便让这两个结构重叠了 8 字节。

通过这种重叠,文件大小缩减至 83 字节。此时,代码结构如下:

  • ELF 文件头的前部分与程序头表的前部分共享内存空间。
  • 程序头表中的 p_typep_offset 等字段被巧妙地复用为文件头中的 e_phnume_shentsize 等字段。
  • 虽然看起来混乱,但每个字段在各自的上下文中都是合法的。

寻找被忽视的“未指定”字段

在 83 字节的基础上,作者再次深入研读 ELF 规范,试图寻找更多的压缩空间。经过排查:

  1. 寄存器初始值:除了 edx(可能为 0 或关机过程地址)外,其他寄存器没有保证,无法利用。
  2. 零填充字段:大多数不适用于 Intel 架构或可执行文件的字段必须设为 0。
  3. 突破口:程序头表结构中的 p_paddr(物理地址)字段。规范指出,该字段的内容为“未指定”(unspecified contents)。这意味着我们可以随意填充这 4 个字节,而不会违反规范。

81 字节:利用 p_paddr 存储代码

既然 p_paddr 的 4 个字节是自由的,作者决定在其中存储部分程序代码。

  • 由于程序不能全部塞进这 4 个字节,作者使用了一个 2 字节的 jmp short 指令跳转到后续代码。
  • 剩下的 2 个字节正好容纳程序的第一条指令 xor eax, eax(2 字节)。
  • 通过这种“代码嵌入头部”的技巧,文件大小进一步缩减至 81 字节

79 字节:调整加载地址以优化指令布局

作者尝试进一步压缩,发现如果能让 jmp 指令与 p_filesz 字段重叠会更完美,但 p_filesz 的第一字节代表文件大小,若重叠会导致跳转逻辑错误。

于是,作者将目光转向 p_vaddr(虚拟地址/加载地址)。

  • 默认加载地址是 0x08048000
  • 作者发现,如果改变加载地址,可以利用地址高位字节来存储指令。
  • 由于是小端序(Little-Endian),且 Linux 32 位可执行文件的高半部分内存(Top half of memory)通常保留给堆、栈和共享库,因此地址的高字节不能设置高位(即不能是负数地址)。
  • xor eax, eax 指令在内存中表现为 31 C0C0 的高位为 1,会导致地址进入保留区域,被 Linux 拒绝。
  • mov bl, 42 指令表现为 B3 2A2A 是安全的。
  • 因此,作者将加载地址改为 0x2AB30000,并将 mov bl, 42 放在代码起始处,利用地址空间存储指令。
  • 此时文件大小缩减至 79 字节

76 字节:终极优化——跨越 p_filesz

这是最精妙的一步。作者希望再塞入一个字节来执行 inc eax,从而移除跳转指令,让所有代码都合法地躺在头部结构中。

  • 障碍p_filesz 字段指定了文件中程序段的大小。如果程序溢出到这个字段,0x80int 0x80 指令的最后一字节)会被误认为是文件大小,导致逻辑错误。
  • 突破p_memsz 字段指定了加载到内存中的大小。规范允许 p_memsz 大于 p_filesz(例如用于 BSS 段)。
  • 操作:作者调整代码布局,让程序跳过 p_filesz 字段,直接跳转到 p_memsz 字段所在的内存位置执行。
  • p_memsz 的位置,作者放置了 inc eax 指令,并在其后填充了 p_flagsp_align 所需的值(dd 7dd 0x1000)。
  • 通过这种“跳跃”策略,程序完全避开了 p_filesz 的限制,将所有逻辑指令嵌入到头结构中。

最终,这个严格符合 ELF 规范的可执行文件被压缩到了 76 字节

关键要点

  • 规范与实现的差异:之前的 45 字节版本依赖于内核的宽容,而 76 字节版本严格遵循 ELF 规范,证明了在标准框架内仍有巨大的优化空间。
  • 数据结构重叠:ELF 规范允许文件头和数据结构重叠,这是压缩体积的关键技巧之一。
  • 利用“未指定”字段p_paddr 字段的内容在规范中是未指定的,可以安全地用于存储代码,这是突破字节限制的重要突破口。
  • 加载地址的灵活运用:通过改变默认加载地址(如 0x2AB30000),可以利用地址空间的高位字节来存储指令,前提是确保地址符合 Linux 内存布局的安全要求。
  • p_memszp_filesz 的差异:利用 p_memsz(内存大小)可以大于 p_filesz(文件大小)这一特性,可以绕过文件大小字段的限制,将代码“跳跃”到内存大小字段中执行。
  • 小端序的影响:在利用内存地址存储指令时,必须考虑小端序对字节排列的影响,确保生成的地址字节不会落入受保护的内存区域。

意义与影响

这篇博文不仅是一次技术炫技,更展示了底层系统编程中对规范理解的深度。它揭示了 ELF 格式中许多被忽视的细节,如字段重叠、未指定字段的可用性以及内存与文件大小字段的语义差异。

对于系统程序员和逆向工程师而言,这种极致的压缩技术提供了新的视角:

  1. 理解内核行为:深入理解内核如何解析 ELF 头,有助于编写更高效的启动代码或恶意代码分析(尽管本文旨在合规)。
  2. 规范解读的重要性:许多优化机会隐藏在规范的“灰色地带”或看似无用的字段中,细致的规范阅读往往能带来意想不到的突破。
  3. 资源受限环境的应用:虽然 76 字节的 ELF 在通用 Linux 发行版中意义有限,但在嵌入式系统、Bootloader 或极端的资源受限环境中,这种对二进制格式的极致掌控具有潜在价值。

最终,这个 76 字节的 ELF 文件证明了,即使在最严格的规范约束下,通过创造性的思维和深入的技术洞察,我们依然能在方寸之间创造出令人惊叹的成果。

查看原文 →muppetlabs.com