# ELF 文件格式

ELF 文件由 4 部分组成,分别是 ELF 头、程序头表、节和节头表。其中,只有 ELF 头的位置是固定的。

# ELF Header

ELF 头的格式定义如下:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define EI_NIDENT (16)

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

这里,我们仅关注几个我们需要用到的项:

  1. e_entry 程序的入口地址。
  2. e_phoff 程序头表在文件中的偏移量(以字节计数)。这里的值是 0x34。
  3. e_ehsize ELF 头的大小。这里的值为 0x34。

# Program Header Table

程序头表的结构如下:

C
1
2
3
4
5
6
7
8
9
10
11
12
/* Program segment header.  */
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;

程序头表描述的是系统准备程序运行所需的一个段或其他信息。我们关注以下几个项:

  1. p_offset 段的第一个字节在文件中的偏移。
  2. p_vaddr 段的第一个字节在内存中的虚拟地址。
  3. p_filesz 段在文件中的长度。
  4. p_memsz 段在内存中的长度。

# 执行视图

ELF文件中程序头表和节头表指向的区域
ELF文件链接视图和执行视图对照

上图左边为链接视图,主要指前面 ELF 头部的节头表定义的节区分布,它指明了目标代码文件的内容布局。

上图右边为执行视图,主要指前面 ELF 头部的程序头表定义的段区分布,它指明了程序运行时的内存布局。

# 篡改流程

# 创建 ELF 头和程序头的结构体

我们创建 ELF 头和程序头的结构体,用来存储相关的原始数据,并根据嵌入需要来更新结构体中的数据。

C
1
2
3
4
5
6
7
8
printf("Start inject code:\n");
int old_entry; //原始程序入口地址
int old_phsize; //ELF头中存放的程序头表的偏移

// ELF Header Table struct
Elf32_Ehdr elf_ehdr;
// Program Header Table struct
Elf32_Phdr elf_phdr;

# 读取 ELF 头的信息并存储

我们将 ELF 头信息赋值给 elf_ehdr 结构体,将入口地址赋值给 old_entry,为后面更改入口地址做准备。

C
1
2
3
4
//read elf
int old_file = open(elf_file, O_RDWR);
read(old_file, &elf_ehdr, sizeof(elf_ehdr));
old_entry = elf_ehdr.e_entry;

# 读取程序头表信息并存储

我们先将 elf 头信息丢弃到 buffer 中(因为我们上一步已经存储了相关信息),然后读取程序头表信息并将其存进 elf_phdr 结构体中。

C
1
2
3
4
5
//read program head table    
close(old_file);
old_file = open(elf_file, O_RDWR);
read(old_file, &buffer , elf_ehdr.e_phoff);
read(old_file, &elf_phdr, sizeof(elf_phdr));

# 更新程序入口地址

我们将预期的入口地址设为程序头表第一项指向的段的后面(当然也可以新增一个节,找到空闲位置并将入口地址指向这个节区,这里做是为了方便)。e_entry = p_vaddr + p_filesz,p_vaddr 也就是第一个程序头表指向的段的第一个字节所在的地址,而 p_filesz 也就是程序头表指向的段的大小,因此两者相加就是指向程序头表指向的段之后的区域。

C
1
2
3
// update new entry 
elf_ehdr.e_entry = elf_phdr.p_vaddr + elf_phdr.p_filesz;
printf("new entry: %x\n", elf_ehdr.e_entry);

# 更新 ELF 头

重新做 close 和 open 操作是为了重置 old_file 的值(也相当于指针),以便从头开始读取文件信息。

我们将前面已经更新信息后的 elf_ehdr 结构体数据写入可执行文件中,完成 ELF 头的更新。这里 old_phsize 计算的值是为了后面嵌入代码做的准备工作,也就是可执行文件开始到嵌入代码位置的偏移。

随后将第一个程序头表指向的段扩容,增加一页 (4k) 大小,以便存放我们嵌入的代码。

C
1
2
3
4
5
6
7
//update ELF head
close(old_file);
old_file = open(elf_file, O_RDWR);
write(old_file, &elf_ehdr, sizeof(elf_ehdr));
old_phsize = elf_phdr.p_filesz + elf_phdr.p_offset;
elf_phdr.p_filesz += PAGE_SIZE;
elf_phdr.p_memsz += PAGE_SIZE;

# 更新程序头表

将 elf_phdr 结构体写入可执行文件,完成程序头部表的更新。随后进入到嵌入代码函数 insert 中。

C
1
2
3
4
5
6
// update program head table
close(old_file);
old_file = open(elf_file, O_RDWR);
read(old_file, &buffer, elf_ehdr.e_phoff);
write(old_file, &elf_phdr, sizeof(elf_phdr));
insert(elf_ehdr, old_file, old_entry, old_phsize, elf_file);

# 嵌入函数

insert 函数定义如下:

C
1
void insert(Elf32_Ehdr elf_ehdr, int old_file, int old_entry, int old_phsize, char *elf_file)

我们计算原始入口地址的二进制表示,然后为后面嵌入代码做准备,因为嵌入的代码是机器码表示形式。

C
1
2
int old_entry_addr[4];
cal_addr(old_entry, old_entry_addr);

这里,我们用到了 cal_addr 计算地址的函数,该函数源码如下:

C
1
2
3
4
5
6
7
8
void cal_addr(int entry, int addr[]) {
int tmp = entry;
int i;
for (i = 0; i < 4; i++) {
addr[i] = tmp % 256;
tmp /= 256;
}
}

嵌入代码用一个 char 类型的数组存放,这里很简单,我们仅仅是嵌入了一个断点调试代码(xchg bx, bx),然后将原入口地址压栈并返回,继续执行原程序的代码。

C
1
2
3
4
char inject_code[] = {0x66, 0x87, 0xdb,              //xchg bx, bx
0x68, old_entry_addr[0], old_entry_addr[1], old_entry_addr[2], old_entry_addr[3], //push oldentryaddr
0xc3 //ret
};

这里我们先读取可执行文件 old_phsize 大小(之前提到过)的信息放入 buffer 无用缓存区中,然后将嵌入代码注入。这里我们可以将第一个程序头表指向段新增的一页除去嵌入代码的剩余部分全部补 0,当然,我们嵌入的可执行程序很简单,不补 0 也没关系。

C
1
2
3
4
5
6
7
8
close(old_file);
old_file = open(elf_file, O_RDWR);
read(old_file, &buffer, old_phsize);
write(old_file, inject_code, sizeof(inject_code));
//char tmp[PAGE_SIZE] = {0};
//memset(tmp, PAGE_SIZE - sizeof(inject_code), 0);
//write(old_file, tmp, PAGE_SIZE - sizeof(inject_code));
printf("inject end\n");

# 主函数

没什么好说的~

C
1
2
3
4
5
6
7
int main(int argc, char **argv){
if (argc != 2) {
printf("error! please input correct command!");
}
inject(argv[1]);
return 0;
}

# 完整代码及运行结果

我们在一个 shell 中输入命令,来执行嵌入程序并查看结果:

可以看出,嵌入之后的 pwd 可执行文件,会在断点处停止,也就是我们嵌入的断点。

再次按下 c 执行,才会打印出原本的内容。

完整源码如下:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include "stdio.h"
#include "elf.h"
#include "type.h"
#include "string.h"
#define PAGE_SIZE 4096

char buffer[10000];
void cal_addr(int entry, int addr[]);
int inject(char *elf_file);
void insert(Elf32_Ehdr elf_ehdr, int old_file, int old_entry, int old_phsize, char *elf_file);


void cal_addr(int entry, int addr[])
{
int tmp = entry;
int i;
for (i = 0; i < 4; i++)
{
addr[i] = tmp % 256;
tmp /= 256;
}
}

int inject(char *elf_file)
{
printf("Start inject code:\n");

int old_entry;
int old_phsize;

// ELF Header Table struct
Elf32_Ehdr elf_ehdr;
// Program Header Table struct
Elf32_Phdr elf_phdr;

//read elf
int old_file = open(elf_file, O_RDWR);
read(old_file, &elf_ehdr, sizeof(elf_ehdr));
old_entry = elf_ehdr.e_entry;

printf("old entry: %x\n", old_entry);

//read program head table
close(old_file);
old_file = open(elf_file, O_RDWR);
read(old_file, &buffer , elf_ehdr.e_phoff);
read(old_file, &elf_phdr, sizeof(elf_phdr));

// update new entry
elf_ehdr.e_entry = elf_phdr.p_vaddr + elf_phdr.p_filesz;
printf("new entry: %x\n", elf_ehdr.e_entry);

//update ELF head
close(old_file);
old_file = open(elf_file, O_RDWR);
write(old_file, &elf_ehdr, sizeof(elf_ehdr));
old_phsize = elf_phdr.p_filesz + elf_phdr.p_offset;
elf_phdr.p_filesz += PAGE_SIZE;
elf_phdr.p_memsz += PAGE_SIZE;

// update program head table
close(old_file);
old_file = open(elf_file, O_RDWR);
read(old_file, &buffer, elf_ehdr.e_phoff);
write(old_file, &elf_phdr, sizeof(elf_phdr));
insert(elf_ehdr, old_file, old_entry, old_phsize, elf_file);
}

void insert(Elf32_Ehdr elf_ehdr, int old_file, int old_entry, int old_phsize, char *elf_file)
{
int old_entry_addr[4];
cal_addr(old_entry, old_entry_addr);
char inject_code[] = {0x66, 0x87, 0xdb, //xchg bx, bx
0x68, old_entry_addr[0], old_entry_addr[1], old_entry_addr[2], old_entry_addr[3], //push oldentryaddr
0xc3 //ret
};

close(old_file);
old_file = open(elf_file, O_RDWR);
read(old_file, &buffer, old_phsize);
write(old_file, inject_code, sizeof(inject_code));
//char tmp[PAGE_SIZE] = {0};
//memset(tmp, PAGE_SIZE - sizeof(inject_code), 0);
//write(old_file, tmp, PAGE_SIZE - sizeof(inject_code));
printf("inject end\n");
}

int main(int argc, char **argv){
if (argc != 2) {
printf("error! please input correct command!");
}
inject(argv[1]);
return 0;
}