开发者生态
morning
重新审视青少年可执行文件
2026-06-24
1 阅读
ankitg12
重新审视青少年可执行文件(或“雷云聚集在地平线上”) 有几次,人们对我最初的文章做出了回应,并评论说我最终创建的内容并不是真正的 ELF 可执行文件。相反,当前版本的 Linux 内核恰好将它误认为是 ELF 可执行文件。这是一个公平的观点。这个 45 字节的文件显然不符合 ELF 规范的众多要求。但你能怪我吗?我怎么可能在我把 ELF 规范扔到窗外之前停下来,知道什么仍然是可能的?但为了满足这些纯粹主义者以及我们所有人清教徒的一面,我创作了这部续集。所以。我们有一个可执行文件,已缩减至 45 字节。我们现在希望使其严格符合已发布的标准,同时仍保持尽可能小。当我们开始摆弄 ELF 标头中的“未使用”字段时,我们就偏离了笔直而狭窄的道路。那么让我们回到这一点之前: BITS 32 org 0x08048000 ehdr: ; Elf32_Ehdr db 0x7F, "ELF", 1, 1, 1 ; e_ident 乘以 9 db 0 dw 2 ; e_type dw 3 ; e_machine dd 1 ; e_version dd _start ; e_entry dd phdr - $$ ; e_phoff dd 0 ; e_shoff dd 0 ; e_flags dw ehdrsz ; e_ehsize dw phdrsz; e_phentsize dw 1 ; e_phnum dw 0 ; e_shentsize dw 0 ; e_shnum dw 0 ; e_shstrndx ehdrsz equ $ - ehdr phdr: ; Elf32_Phdr dd 1 ; p_type dd 0 ; p_offset dd $$ ; p_vaddr dd $$ ; p_paddr dd 文件z ; p_filesz dd 文件z ; p_memsz dd 5 ; p_flags dd 0x1000 ; p_align phdrsz equ $ - phdr _start: xor eax, eax inc eax mov bl, 42 int 0x80 filesz equ $ - $$ 这是我们的 91 字节版本。那么:我们是否坚持将其作为最佳尺寸?不,不完全是。当我们将 ELF 头和程序头表重叠 8 个字节时,我们并没有违反任何规则。 ELF 规范明确允许文件内不同数据结构的重叠。那么让我们在这里这样做:; tiny.asm BITS 32 org 0x08048000 ehdr: db 0x7F, "ELF", 1, 1, 1 ; e_ident 乘以 9 db 0 dw 2 ; e_type dw 3 ; e_machine dd 1 ; e_version dd _start ; e_entry dd phdr - $$ ; e_phoff dd 0 ; e_shoff dd 0 ; e_flags dw ehdrsz ; e_ehsize dw phdrsz; e_phentsize phdr: dd 1 ; e_phnum; p_类型; e_shentsize dd 0 ; e_shnum ; p_偏移量; e_shstrndx ehdrsz equ $ - ehdr dd $$ ; p_vaddr dd $$ ; p_paddr dd 文件z ; p_filesz dd 文件z ; p_memsz dd 5 ; p_flags dd 0x1000 ; p_align phdrsz equ $ - phdr _start: xor eax, eax inc eax mov bl, 42 int 0x80 filesz equ $ - $$ 这给了我们八十三个字节。我们还能做什么?好像东西不多啊无奈之下,我们可能会回到 ELF 规范,重新阅读一遍,寻找一些东西。对于初始寄存器值有任何保证吗?仅适用于一个寄存器:edx。也就是说,它将包含零或最终关闭过程的地址。所以,真的,根本没有任何保证。继续寻找。 A-ha:程序头表结构的p_paddr字段! ELF 规范要求标头中不适用于英特尔架构或不适用于可执行文件(或者至少不适用于我们的可执行文件)的其他字段设置为零。但是对于 p_paddr 字段,规范说该字段具有未指定的内容。所以毕竟我们有四个字节可以使用。我们能用它们做什么?当然,用它来保存我们程序的一部分。当然,我们不能将整个程序放在那里,因此我们需要在 jmp 指令上浪费四个字节中的两个,以便访问其余部分。但这仍然留下两个字节可供我们使用,并且我们程序的第一条指令正好是两个字节长。 ; tiny.asm BITS 32 org 0x08048000 ehdr: db 0x7F, "ELF", 1, 1, 1 ; e_ident 乘以 9 db 0 dw 2 ; e_type dw 3 ; e_machine dd 1 ; e_version dd _start ; e_entry dd phdr - $$ ; e_phoff dd 0 ; e_shoff dd 0 ; e_flags dw ehdrsz ; e_ehsize dw phdrsz; e_phentsize phdr: dd 1 ; e_phnum; p_类型; e_shentsize dd 0 ; e_shnum ; p_偏移量; e_shstrndx ehdrsz equ $ - ehdr dd $$ ; p_vaddr _start: 异或 eax, eax ; p_paddr jmp 短第 2 部分 dd 文件z ; p_filesz dd 文件z ; p_memsz dd 5 ; p_flags dd 0x1000 ; p_align phdrsz equ $ - phdr 第 2 部分:inc eax mov bl, 42 int 0x80 filesz equ $ - $$ 所以。八十一个字节。这就是全部吗? p_paddr 字段之后的下一个字段是 p_filesz 字段。如果我们能够将 jmp 指令与该指令重叠,我们就可以在其中压缩另一条指令。但可惜的是,该字段的第一个字节是整个文件的大小,这是一个不明智的跳转。其余字节为零。这种方法看起来不太有希望。 p_paddr之前的字段呢?这是程序要加载的地址。好吧,我们已经知道不必使用默认值 0x08048000。我们至少需要保持地址页对齐,但我们应该能够将两字节指令放入地址的上半部分。然而,我们的异或不起作用