我们必须自己来构造程序的ELF HEADER.
你也可以查看ELF文档和/usr/include/linux/elf.h得到相关信息,一个空的ELF可执行文件应该
象如下:
BITS 32
org 0x08048000
ehdr: ; Elf32_Ehdr
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 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 ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsize equ $ - ehdr
phdr: ; Elf32_Phdr
dd 1 ; p_type
dd 0 ; p_offset
dd $$ ; p_vaddr
dd $$ ; p_paddr
dd filesize ; p_filesz
dd filesize ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsize equ $ - phdr
_start:
; your program here
filesize equ $ - $$
该映象包含了一个ELF header ,没有section header table ,一个program header table 包含了
一个入口。该入口指示程序转载器把完整的文件装载到内存(一般的是包含自己的ELF header 和
program header table)开始地址为0x08048000(这是可执行文件装载的默认地址)的地方,并且
开始执行_start处代码,_start紧跟着program header table.没有.data段,没有.bss段
没有.comment段。
好了,现在我们的程序就变成这样了:
[alert7@redhat]# cat tiny.asm
; tiny.asm
org 0x08048000
ehdr: ; Elf32_Ehdr
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 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 ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsize equ $ - ehdr
phdr: ; Elf32_Phdr
dd 1 ; p_type
dd 0 ; p_offset
dd $$ ; p_vaddr
dd $$ ; p_paddr
dd filesize ; p_filesz
dd filesize ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsize equ $ - phdr
_start:
mov bl, 42
xor eax, eax
inc eax
int 0x80
filesize equ $ - $$
[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
再看看大小:
[alert7@redhat]# wc -c a.out
93 a.out
真是奇迹,才93个字节大小了。
假如我们明白在可执行文件中的每个字节,我们或许还可以更小,也许很是极限了哦:)
--------------------------------------------------------------------------------
你可能已经注意到了:
1)ELF文件的不同部分允许被定位在任何地方(除了ELF header,它必须放在文件的开始),
并且它们可以交叠。
2)事实上一些字段到目前还没有被用到。
在鉴别文件字段最后有9个字节为0,我们的代码只有7个字节长,所以我们试图把代码放入
鉴别文件字段最后9个字节中,还有2个剩余。....
[alert7@redhat]# cat tiny.asm
; tiny.asm
BITS 32
org 0x08048000
ehdr: ; Elf32_Ehdr
db 0x7F, "ELF" ; e_ident
db 1, 1, 1, 0
_start: mov bl, 42
xor eax, eax
inc eax
int 0x80
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 ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsize equ $ - ehdr
phdr: ; Elf32_Phdr
dd 1 ; p_type
dd 0 ; p_offset
dd $$ ; p_vaddr
dd $$ ; p_paddr
dd filesize ; p_filesz
dd filesize ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsize equ $ - phdr
filesize equ $ - $$
[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
84 a.out
现在我们的程序只有一个elf header和一个program header table入口,为了装载和运行程序,
这些是我们必要的。所以现在我们不能减少了!除非....
我们使elf header和program header table一部分重合或者说是交叠,有没有可能呢?
答案当然是有的,注意我们的程序,就会注意到在elf header最后8个字节和program header table
前8个字节是一样的,所以...
[alert7@redhat]# cat tiny.asm
; tiny.asm
BITS 32
org 0x08048000
ehdr:
db 0x7F, "ELF" ; e_ident
db 1, 1, 1, 0
_start: mov bl, 42
xor eax, eax
inc eax
int 0x80
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 ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
phdr: dd 1 ; e_phnum ; p_type
; e_shentsize
dd 0 ; e_shnum ; p_offset
; e_shstrndx
ehdrsize equ $ - ehdr
dd $$ ; p_vaddr
dd $$ ; p_paddr
dd filesize ; p_filesz
dd filesize ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
phdrsize equ $ - phdr
filesize equ $ - $$
[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
76 a.out
现在已经不能够再更多的重叠那两个结构了,因为两个结构的字节没有再相同的了。
但是,我们可以再构造这两个结构,使它们有更多的相同部分。
到底linux会检查多少字段呢?例如,它会检查e_machine字段吗?
事实上很另人惊讶,一些字段居然被默默的忽略了。
因此:哪些东西才是ELF header中最重要的呢?最前的四个字节当然是的,它包含了一个
魔术数,否则linux不会继续处理它。在e_ident字段的其他3个字节不被检查,那就意味着
我们有不少于12个连续的字节我们可以设置为任意的值。e_type必须被设置为2(用来表明
是个可执行文件),e_machine必须为3。就象e_ident中的版本号一样,e_version被完全的
忽略。(这样做可以理解,因为现在只有一个版本的ELF标准)。e_entry当然要设置为正确
的值,因为它指向程序的开始。毫无疑问,e_phoff应该是program header table在文件中
的正确偏移量,e_phnum是program header table中所包含的正确的入口数。然而,e_flags
没有被当前的Intel体系使用,所以我们应该可以重新利用。e_ehsize用来校验elf header
所期望的大小,但是LINUX忽略了它。e_phentsize同样的确认program header table入口的
大小。但是只有在2.2.17以后的2.2系列内核中这个字段才是被检查的。早于2.2的和2.4.0的
内核是忽略它的。
program header table又是如何呢?
p_type必须是1(即PT_LOAD),表明这是个可载入的段。p_offset是开始装载的文件偏移量。
同样的,p_vaddr是正确的装载地址。注意:我们没有要求把它装载到0x08048000.
可用的地址为0-0x80000000,并且要页对齐。文档上说p_paddr被忽略,因此这个字段更是可
用的。p_filesz 指示了从文件中装载到内存中有多少字节,p_memsz指示了需要多大的内存段。
因此,他们的值应该是相关的。p_flags指示了给于内存段什么权限。可设置读,写,执行,
其他位也可能被设置,但是我们只需要最小权限。最后,p_align给出了对齐需求。该字段主要
使用在当重定位段包含了与位置无关的代码时,岂今为止,可执行文件将被LINUX忽略这个字段。
根据分析,我们从中可以看出一些必要的字段,一些无用的字段,这样,我们就可以重叠更多的
字数了。
[alert7@redhat]# cat tiny.asm
; tiny.asm◆
BITS 32
org 0x00200000
db 0x7F, "ELF" ; e_ident
db 1, 1, 1, 0
_start:
mov bl, 42
xor eax, eax
inc eax
int 0x80
db 0
dw 2 ; e_type
dw 3 ; e_machine
dd 1 ; e_version
dd _start ; e_entry
dd phdr - $$ ; e_phoff
phdr: dd 1 ; e_shoff ; p_type
dd 0 ; e_flags ; p_offset
dd $$ ; e_ehsize ; p_vaddr
; e_phentsize
dw 1 ; e_phnum ; p_paddr
dw 0 ; e_shentsize
dd filesize ; e_shnum ; p_filesz
; e_shstrndx
dd filesize ; p_memsz
dd 5 ; p_flags
dd 0x1000 ; p_align
filesize equ $ - $$
正如你看到的,program header table的前12个字节重叠在ELF header的最后12个字节里。
相当的吻合。ELF header重复中只有两部分会有麻烦。一是e_phnum字段,相对应的是p_paddr
是会被忽略。第二个是e_phentsize字段,它和p_vaddr前两个字节相一致,为了这个相一致,
使用了非标准的加载地址0x00200000,那么前面的两个字节就是0x0020.
[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
64 a.out
well,现在大小为64字节了
如果我们使 program header table完全放在ELF header中,那么,呵呵,大小就可以更小了,
但是这样做行吗?
是的,是可能的。使program header table从第四个字节就开始,精心构造可执行的ELF文件。
我们注意到:
第一P_memsz指出了为内存段分配多少内存。明显的,它必须至少跟P_filesz一样大,
当然更大是没有关系的。
第二, 可执行位可以从p_flags字段中丢弃,linux会为我们设置它的。为什么这样会工作呢?
作者说不知道,又猜测了原因说是否因为入口指针指向了该段?
[★译者注:
但我知道,linux根本就没有为我们设置p_flags字段中的可执行位,可以工作,
只是因为Intel体系上根本就不具有执行保护功能,就是这个原因,才使得有人有
必要设计了类似堆栈不可运行的内核补丁程序。
]
[alert7@redhat]# cat tiny.asm
; tiny.asm
BITS 32
org 0x00001000
db 0x7F, "ELF" ; e_ident
dd 1 ; p_type
dd 0 ; p_offset
dd $$ ; p_vaddr
dw 2 ; e_type ; p_paddr
dw 3 ; e_machine
dd filesize ; e_version ; p_filesz
dd _start ; e_entry ; p_memsz
dd 4 ; e_phoff ; p_flags
_start:
mov bl, 42 ; e_shoff ; p_align
xor eax, eax
inc eax ; e_flags
int 0x80
db 0
dw 0x34 ; e_ehsize
dw 0x20 ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
filesize equ $ - $$
p_flags字段从5变为4,这个4也是e_phoff字段的值,它给出了program header table在文件中
的偏移量。代码被放在从e_shoff 开始到e_flags内部结束。
注意到:装载地址被改变了更低了。只是为了保持e_entry的值到一个比较合适的小值,它刚好
也是P_mensz的数值。
[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
52 a.out
现在,程序代码本身和program header table完全嵌入了ELF header,我们的可执行文件现在和
elf header一样大。而且可以正常运行。
最后,我们不禁还要问,是否到达了最小的极限呢?毕竟,我们需要一个完整的ELF header,否则
linux不会给我们运行的机会。
真的是这样吗 ?
错了,我们还可以运用最后一招卑鄙的哄骗技术了。
如果文件大小还没有整个ELF header大的话,linux还是会运行它的。并且把那些少的字节填充为
0。我们在文件的最后有不少于7个0,可以丢弃。
[alert7@redhat]# cat tiny.asm
; tiny.asm
BITS 32
org 0x00001000
db 0x7F, "ELF" ; e_ident
dd 1 ; p_type
dd 0 ; p_offset
dd $$ ; p_vaddr
dw 2 ; e_type ; p_paddr
dw 3 ; e_machine
dd filesize ; e_version ; p_filesz
dd _start ; e_entry ; p_memsz
dd 4 ; e_phoff ; p_flags
_start:
mov bl, 42 ; e_shoff ; p_align
xor eax, eax
inc eax ; e_flags
int 0x80
db 0
dw 0x34 ; e_ehsize
dw 0x20 ; e_phentsize
db 1 ; e_phnum
; e_shentsize
; e_shnum
; e_shstrndx
filesize equ $ - $$
[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
45 a.out
讨论到此,一个elf可执行文件最小大小为45 bytes,我们被迫终止我们的讨论了。
--------------------------------------------------------------------------------
一个45字节大小的文件比一个用标准工具创建的最小可执行文件的1/8还要小,比用纯C代码
创建的1/50还要小。
这片文章中的一半ELF字段变量违反了标准的ELF规范,
以上程序中打上◆ 的程序,会使readelf core dump
[alert7@redhat]# readelf -a a.out
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 b3 2a 31 c0 40 cd 80 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 179
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x200008
Start of program headers: 32 (bytes into file)
Start of section headers: 1 (bytes into file)
Flags: 0x0
Size of this header: 0 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 0 (bytes)
Number of section headers: 64
Section header string table index: 0
readelf: Error: Unable to read in 0 bytes of section headers
Program Header:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00200000 0x00000001 0x00040 0x00040 R E 0x1000
There is no dynamic segment in this file.
Segmentation fault (core dumped)
呵呵,居然出现了可爱的core dumped
[alert7@redhat]# ls -l /usr/bin/readelf
-rwxr-xr-x 1 root root 132368 Feb 5 2000 /usr/bin/readelf
:(不是带s位的,也就懒的去看它到底哪里出问题了。
创建的这种超小的elf文件的确比较畸形,连objdump都不能dump它们了。
[alert7@redhat]# objdump -a a.out
objdump: a.out: File format not recognized








