如何使用十六进制编辑器在 Linux 中创建可执行 ELF 文件?
- 2024-10-14 08:40:00
- admin 原创
- 69
问题描述:
只是好奇。这显然不是实际编程的好解决方案,但假设我想在 Bless(十六进制编辑器)中制作一个可执行文件。
我的架构是 x86。我可以编写一个非常简单的程序吗?一个 hello world?一个无限循环?与这个问题类似,但在 Linux 中。
解决方案 1:
反编译 NASM hello world 并理解其中的每个字节
这个答案的版本具有很好的目录和更多内容:http://www.cirosantilli.com/elf-hello-world(此处达到 30k 个字符的限制)
标准
ELF 由 LSB 指定:
核心通用:http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/elf-generic.html
核心 AMD64:http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-AMD64/LSB-Core-AMD64/book1.html
LSB 基本上与其他标准相链接,并进行了少量扩展,特别是:
通用(均由 SCO 提供):
+ System V ABI 4.1 (1997) http://www.sco.com/developers/devspecs/gabi41.pdf,没有 64 位,尽管为其保留了一个魔法数字。核心文件也是如此。
+ System V ABI Update DRAFT 17 (2003) http://www.sco.com/developers/gabi/2003-12-17/contents.html,添加了 64 位。仅更新了上一文档的第 4 章和第 5 章:其他内容仍然有效并仍可引用。
架构特定:
+ IA-32: http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-IA32/LSB-Core-IA32/elf-ia32.html,主要指向http://www.sco.com/developers/devspecs/abi386-4.pdf
+ AMD64:http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-AMD64/LSB-Core-AMD64/elf-amd64.html,主要指向http://www.x86-64.org/documentation/abi.pdf
您可以在此处找到一份便捷的摘要:
man elf
readelf
可以通过和等实用程序以人类可读的方式检查其结构objdump
。
生成示例
让我们分解一个最小可运行的 Linux x86-64 示例:
section .data
hello_world db "Hello world!", 10
hello_world_len equ $ - hello_world
section .text
global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, hello_world_len
syscall
mov rax, 60
mov rdi, 0
syscall
编译自:
nasm -w+all -f elf64 -o 'hello_world.o' 'hello_world.asm'
ld -o 'hello_world.out' 'hello_world.o'
版本:
NASM 2.10.09
Binutils 版本 2.24(包含
ld
)Ubuntu 14.04
我们不使用 C 程序,因为那会使分析复杂化,那将是第 2 级 :-)
十六进制转储
hd hello_world.o
hd hello_world.out
输出于:https: //gist.github.com/cirosantilli/7b03f6df2d404c0862c6
全局文件结构
一个ELF文件包含以下几个部分:
ELF 头。指向节头表和程序头表的位置。
节头表(可执行文件上可选)。每个表都有
e_shnum
节头,每个节头指向一个节的位置。N 个部分,带有
N <= e_shnum
(可执行文件可选)程序头表(仅在可执行文件中)。每个程序都有
e_phnum
程序头,每个程序头都指向一个段的位置。N 段,带有
N <= e_phnum
(可执行文件可选)
这些部分的顺序不是固定的:唯一固定的是 ELF 头,它必须是文件中的第一部分:通用文档说:
ELF 头
观察标题的最简单方法是:
readelf -h hello_world.o
readelf -h hello_world.out
输出于:https: //gist.github.com/cirosantilli/7b03f6df2d404c0862c6
目标文件中的字节数:
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............|
00000020 00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 |........@.......|
00000030 00 00 00 00 40 00 00 00 00 00 40 00 07 00 03 00 |....@.....@.....|
可执行文件:
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 b0 00 40 00 00 00 00 00 |..>.......@.....|
00000020 40 00 00 00 00 00 00 00 10 01 00 00 00 00 00 00 |@...............|
00000030 00 00 00 00 40 00 38 00 02 00 40 00 06 00 03 00 |....@.8...@.....|
代表结构:
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry;
Elf64_Off e_phoff;
Elf64_Off e_shoff;
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
手动细分:
0 0:
EI_MAG
=7f 45 4c 46
=0x7f 'E', 'L', 'F'
: ELF 魔法数字0 4:
EI_CLASS
=02
=ELFCLASS64
: 64 位精灵0 5:
EI_DATA
=01
=ELFDATA2LSB
: 大端数据0 6:
EI_VERSION
=01
: 格式版本0 7:(
EI_OSABI
仅在 2003 年更新)=00
=ELFOSABI_NONE
:没有扩展。0 8:
EI_PAD
= 8x00
:保留字节。必须设置为 0。1 0:
e_type
=01 00
= 1 (大端) =ET_REl
: 可重定位格式
在可执行文件中它02 00
是ET_EXEC
。
1 2:
e_machine
=3e 00
=62
=EM_X86_64
: AMD64 架构1 4:
e_version
=01 00 00 00
: 必须为 11 8:
e_entry
= 8x00
:执行地址入口点,如果不适用,则为 0,例如对于目标文件,因为没有入口点。
在可执行文件中,它是b0 00 40 00 00 00 00 00
。TODO:我们还能将其设置为什么?内核似乎将 IP 直接放在该值上,它不是硬编码的。
2 0:
e_phoff
=8x00
:程序头表偏移量,如果不存在则为0。
40 00 00 00
在可执行文件中,即它在 ELF 头之后立即启动。
2 8:
e_shoff
=40
7x00
=0x40
:节头表文件偏移量,如果不存在则为 0。3 0:
e_flags
=00 00 00 00
TODO。特定于 Arch。3 4:
e_ehsize
=40 00
:此 elf 标头的大小。TODO 为什么是此字段?它如何变化?3 6:
e_phentsize
=00 00
:每个程序头的大小,如果不存在则为 0。
38 00
可执行文件:长度为 56 字节
3 8:
e_phnum
=00 00
:程序头条目的数量,如果不存在则为 0。
02 00
在可执行文件中:有 2 个条目。
3 A:
e_shentsize
和e_shnum
=40 00 07 00
:节头大小和条目数3 E:
e_shstrndx
(Section Header STRing iNDeX
) =03 00
:部分索引.shstrtab
。
节标题表
结构数组Elf64_Shdr
。
每个条目包含有关给定部分的元数据。
e_shoff
ELF 头给出了起始位置,这里是 0x40。
e_shentsize
从e_shnum
ELF 标头来看,我们有 7 个条目,每个0x40
条目长一个字节。
因此该表占用从 0x40 到0x40 + 7 + 0x40 - 1
0x1FF 的字节。
某些部分名称是为某些部分类型保留的:http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#special_sections例如.text
需要SHT_PROGBITS
类型和SHF_ALLOC
+SHF_EXECINSTR
readelf -S hello_world.o
:
There are 7 section headers, starting at offset 0x40:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .data PROGBITS 0000000000000000 00000200
000000000000000d 0000000000000000 WA 0 0 4
[ 2] .text PROGBITS 0000000000000000 00000210
0000000000000027 0000000000000000 AX 0 0 16
[ 3] .shstrtab STRTAB 0000000000000000 00000240
0000000000000032 0000000000000000 0 0 1
[ 4] .symtab SYMTAB 0000000000000000 00000280
00000000000000a8 0000000000000018 5 6 4
[ 5] .strtab STRTAB 0000000000000000 00000330
0000000000000034 0000000000000000 0 0 1
[ 6] .rela.text RELA 0000000000000000 00000370
0000000000000018 0000000000000018 4 2 4
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
struct
每个条目代表:
typedef struct {
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
章节
索引 0 部分
包含在字节 0x40 至 0x7F 中。
第一部分总是很神奇:http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html说:
如果节数大于或等于 SHN_LORESERVE (0xff00),则 e_shnum 的值为 SHN_UNDEF (0),并且节头表条目的实际数量包含在索引 0 处的节头的 sh_size 字段中(否则,初始条目的 sh_size 成员包含 0)。
中还详细介绍了其他魔法部分Figure 4-7: Special Section Indexes
。
空值
在索引 0 中,SHT_NULL
是强制性的。它还有其他用途吗:ELF 中的 SHT_NULL 部分有什么用处??
.data 部分
.data
是第 1 部分:
00000080 01 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 |................|
00000090 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 |................|
000000a0 0d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000000b0 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
80 0:
sh_name
= :字符串表01 00 00 00
中的索引 1.shstrtab
这里,1
表示本节的名称从该节的第一个字符开始,到第一个 NUL 字符结束,组成字符串.data
。
.data
是具有预定义含义的部分名称之一http://www.sco.com/developers/gabi/2003-12-17/ch4.strtab.html
这些部分保存了构成程序内存映像的初始化数据。
80 4:
sh_type
=01 00 00 00
:SHT_PROGBITS
: 节内容不是由 ELF 指定的,而是由程序如何解释它指定的。由于是.data
节,因此正常。80 8:
sh_flags
=03
7x00
:SHF_ALLOC
和SHF_EXECINSTR
: http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#sh_flags,根据需要从一.data
节90 0:
sh_addr
=8x00
:执行期间该部分将被放置在哪个虚拟地址中,0
如果没有放置90 8:
sh_offset
=00 02 00 00 00 00 00 00
=0x200
:从程序开始到本节第一个字节的字节数a0 0:
sh_size
=0d 00 00 00 00 00 00 00
如果我们从 sh_offset
200 开始取 0xD 字节,我们会看到:
00000200 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 0a 00 |Hello world!.. |
啊哈!所以我们的"Hello world!"
字符串位于数据部分,就像我们在 NASM 上所说的那样。
一旦我们从毕业hd
,我们将会像这样查找:
readelf -x .data hello_world.o
输出:
Hex dump of section '.data':
0x00000000 48656c6c 6f20776f 726c6421 0a Hello world!.
NASM 为该部分设置了合适的属性,因为它可以.data
神奇地处理:http://www.nasm.us/doc/nasmdoc7.html#section-7.9.2
还要注意,这是一个糟糕的部分选择:一个好的 C 编译器会将字符串放入其中.rodata
,因为它是只读的,并且可以进一步进行操作系统优化。
a0 8:
sh_link
和sh_info
= 8x 0:不适用于此部分类型。http ://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#special_sectionsb0 0:
sh_addralign
=04
= TODO: 为什么需要进行这种对齐?它仅适用于sh_addr
,还是也适用于 内部的符号sh_addr
?b0 8:
sh_entsize
=00
= 该节不包含表。如果 != 0,则表示该节包含固定大小条目的表。在此文件中,我们从输出中看到和节readelf
的情况就是这样。.symtab
`.rela.text`
.text 部分
现在我们已经手动完成了一个部分,让我们毕业并使用readelf -S
其他部分。
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 2] .text PROGBITS 0000000000000000 00000210
0000000000000027 0000000000000000 AX 0 0 16
.text
可执行但不可写:如果我们尝试写入它,Linux 段错误。让我们看看那里是否真的有一些代码:
objdump -d hello_world.o
给出:
hello_world.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_start>:
0: b8 01 00 00 00 mov $0x1,%eax
5: bf 01 00 00 00 mov $0x1,%edi
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
14: ba 0d 00 00 00 mov $0xd,%edx
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov $0x3c,%eax
20: bf 00 00 00 00 mov $0x0,%edi
25: 0f 05 syscall
如果我们b8 01 00 00
对 进行grep hd
,我们会发现这只发生在00000210
,这正是该部分所说的。而且 Size 是 27,也匹配。所以我们一定在谈论正确的部分。
这看起来是正确的代码:awrite
后跟exit
。
最有趣的部分是a
下面这一行:
movabs $0x0,%rsi
将字符串的地址传递给系统调用。目前,0x0
只是一个占位符。链接后,它将被修改为包含:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
由于该部分的数据,这种修改是可能的.rela.text
。
字符串表
带有 的部分sh_type == SHT_STRTAB
称为字符串表。
它们保存一个以空值分隔的字符串数组。
当要使用字符串名称时,其他部分将使用这些部分。using 部分说明:
他们正在使用哪个字符串表
目标字符串表上字符串开始的索引是什么
例如,我们可以有一个包含以下内容的字符串表:TODO:它必须以 开头吗