重新审视 Teensy 可执行文件
速览
本文回顾了 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_type、p_offset等字段被巧妙地复用为文件头中的e_phnum、e_shentsize等字段。 - 虽然看起来混乱,但每个字段在各自的上下文中都是合法的。
寻找被忽视的“未指定”字段
在 83 字节的基础上,作者再次深入研读 ELF 规范,试图寻找更多的压缩空间。经过排查:
- 寄存器初始值:除了
edx(可能为 0 或关机过程地址)外,其他寄存器没有保证,无法利用。 - 零填充字段:大多数不适用于 Intel 架构或可执行文件的字段必须设为 0。
- 突破口:程序头表结构中的
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 C0,C0的高位为 1,会导致地址进入保留区域,被 Linux 拒绝。- 而
mov bl, 42指令表现为B3 2A,2A是安全的。 - 因此,作者将加载地址改为
0x2AB30000,并将mov bl, 42放在代码起始处,利用地址空间存储指令。 - 此时文件大小缩减至 79 字节。
76 字节:终极优化——跨越 p_filesz
这是最精妙的一步。作者希望再塞入一个字节来执行 inc eax,从而移除跳转指令,让所有代码都合法地躺在头部结构中。
- 障碍:
p_filesz字段指定了文件中程序段的大小。如果程序溢出到这个字段,0x80(int 0x80指令的最后一字节)会被误认为是文件大小,导致逻辑错误。 - 突破:
p_memsz字段指定了加载到内存中的大小。规范允许p_memsz大于p_filesz(例如用于 BSS 段)。 - 操作:作者调整代码布局,让程序跳过
p_filesz字段,直接跳转到p_memsz字段所在的内存位置执行。 - 在
p_memsz的位置,作者放置了inc eax指令,并在其后填充了p_flags和p_align所需的值(dd 7和dd 0x1000)。 - 通过这种“跳跃”策略,程序完全避开了
p_filesz的限制,将所有逻辑指令嵌入到头结构中。
最终,这个严格符合 ELF 规范的可执行文件被压缩到了 76 字节。
关键要点
- 规范与实现的差异:之前的 45 字节版本依赖于内核的宽容,而 76 字节版本严格遵循 ELF 规范,证明了在标准框架内仍有巨大的优化空间。
- 数据结构重叠:ELF 规范允许文件头和数据结构重叠,这是压缩体积的关键技巧之一。
- 利用“未指定”字段:
p_paddr字段的内容在规范中是未指定的,可以安全地用于存储代码,这是突破字节限制的重要突破口。 - 加载地址的灵活运用:通过改变默认加载地址(如
0x2AB30000),可以利用地址空间的高位字节来存储指令,前提是确保地址符合 Linux 内存布局的安全要求。 p_memsz与p_filesz的差异:利用p_memsz(内存大小)可以大于p_filesz(文件大小)这一特性,可以绕过文件大小字段的限制,将代码“跳跃”到内存大小字段中执行。- 小端序的影响:在利用内存地址存储指令时,必须考虑小端序对字节排列的影响,确保生成的地址字节不会落入受保护的内存区域。
意义与影响
这篇博文不仅是一次技术炫技,更展示了底层系统编程中对规范理解的深度。它揭示了 ELF 格式中许多被忽视的细节,如字段重叠、未指定字段的可用性以及内存与文件大小字段的语义差异。
对于系统程序员和逆向工程师而言,这种极致的压缩技术提供了新的视角:
- 理解内核行为:深入理解内核如何解析 ELF 头,有助于编写更高效的启动代码或恶意代码分析(尽管本文旨在合规)。
- 规范解读的重要性:许多优化机会隐藏在规范的“灰色地带”或看似无用的字段中,细致的规范阅读往往能带来意想不到的突破。
- 资源受限环境的应用:虽然 76 字节的 ELF 在通用 Linux 发行版中意义有限,但在嵌入式系统、Bootloader 或极端的资源受限环境中,这种对二进制格式的极致掌控具有潜在价值。
最终,这个 76 字节的 ELF 文件证明了,即使在最严格的规范约束下,通过创造性的思维和深入的技术洞察,我们依然能在方寸之间创造出令人惊叹的成果。
