目录
摘 要 1
1 ELF 文件介绍 1
1.1 ELF 文件格式 1
1.1.1 ElF 文件的类型 1
1.1.2 ELF 文件的组织 1
1.2 深入探究 3
② ELF 文件头(ELF header) 3
③程序头表( program header table ) 5
2 ELF 文件解析 7
2.1 解析 ELF 头文件 7
2.4 解析符号表 18
3 注入功能的实现 25
3.1 shellcode 生成 25
3.3 注入总结 31
4 参考文献 32
3 注入功能的实现
注入功能的实现,是指改变一个 elf 可执行文件,在其中嵌入一段代码, 从而让该程序在运行之后能够先指向一个自己写的特殊的附加功能(如输出一个字符串 helloworld),之后再执行原来的可执行文件的功能。
3.1 shellcode 生成
首先由于在注入前就应当了解到,因为是准备内嵌一段独立的代码,所以该段代码就不能使用其他的库,只能使用系统调用来完成它所需要的功能。在
linux 中,可以通过系统调用直接访问内核,这就决定了必须要使用汇编语言来编写附加功能。
经过查资料,我发现了这段代码叫 shellcode。简单的说,Shellcode 是一段能够完成某种特定功能的二进制代码。具体完成什么任务是由攻击者决定
的,可能是开启一个新的 shell 或者下载某个特定的程序也或者向攻击者返回一个 shell 等等。也可以说,shellcode 就是一段不依靠 pe 加载器加载和处理
的(不需要重定位表、不需要导入表、不需要数据段);在任意进程都空间都可以执行的一段二进制代码。
由于不太了解汇编,所以就去网上查阅了资料,实现了一段基本的功能。汇编代码如图 3-1 所示。
图 3-1 汇编源码
然后在本机器上运行该汇编代码,结果以及过程如图 3-2 所示。
图 3-2 汇编运行结果
该汇编程序主要实现的一段功能就是输出一段字符串 helloworld。核心功能就是 movl $4,%rax,使用系统调用完成 sys_write 功能。如图 3-3 是机器码显示。
至此,所需要的功能已经准备完毕,就可以得出被嵌入部分的汇编代码, 其中主要的思路,首先将 rax,rbx 等寄存器 pushq 入栈,进行现场保存,因为注入的代码经常会变化,所以每次都需要一个栈来支持其变化。最后再 popq 出栈,防止对外界环境进行污染。运行完毕之后,在进行跳转指令。
因为汇编代码测试在 ubuntu 中总是出现段错误,无法进行测试。经过查询资料得知,所谓的段错误就是指访问的内存超过了系统所给这个程序的内存空间,通常这个值是由 gdtr 来保存的,他是一个 48 位的寄存器,其中的 32 位是保存由它指向的 gdt 表,后 13 位保存相应于 gdt 的下标,最后 3 位包括了程序是否在内存中以及程序的在 cpu 中的运行级别,指向的 gdt 是由以 64 位为一个单位的表,在这张表中就保存着程序运行的代码段以及数据段的起始地址以及相应的断限和页面交换还有程序运行级别和内存粒度等信息,本文转载自http://www.biyezuopin.vip/onews.asp?id=16724一旦一个程序发生了越界访问,CPU 就会产生相应的异常保护,于是 segmentation fault 就出现了。
即“当程序试图访问不被允许访问的内存区域(比如,尝试写一块属于操作系统的内存),或以错误的类型访问内存区域(比如,尝试写一块只读内
存)。这个描述是准确的。总的来说,段错误应该就是访问了不可访问的内存, 这个内存要么是不存在的,要么是受系统保护的。
由于编译器版本、函数调用约定,大小端格式、机器指令格式等各方面的因素都会对 shellcode 测试结果有影响。高版本的 linux 系统中,shellcode 保存在内存的.data 数据段是不能被执行的(比较的低版本的 linux 能够执行),所以就产生了段错误。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <elf.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//一页的大小,默认 4K
#define PAGESIZE 4096
// 计算新的地址,如:数据段地址,跳转地址等
void cal_addr(int entry, int addr[]);
// 判断是否是一个 ELF 文件
int is_elf(Elf64_Ehdr elf_ehdr);
// 注入函数
void inject(char *elf_file);
// 插入函数
void insert(Elf64_Ehdr elf_ehdr, int old_file, int old_entry, int old_phsize);
// 计算新的地址
void cal_addr(int entry, int addr[])
{
int temp = entry;
int i;
for (i = 0; i < 4; i++)
{
addr[i] = temp % 256; // 256 == 8byte
temp /= 256;
}
}
// 判断是否是一个 ELF 文件
int is_elf(Elf64_Ehdr elf_ehdr)
{
// ELF文件头部的 e_ident 为 "0x7fELF"
if ((strncmp(elf_ehdr.e_ident, ELFMAG, SELFMAG)) != 0)
return 0;
else
return 1; // 相等时则是
}
// 注入函数
void inject(char *elf_file)
{
printf("开始注入\n");
int old_entry; // elf 文件的原入口地址
int old_shoff; // elf 文件 节区头部表格的原偏移量
int old_phsize; // 节区在文件中的字节数
// ELF Header Table 结构体
Elf64_Ehdr elf_ehdr;
// Program Header Table 结构体
Elf64_Phdr elf_phdr;
// Section Header Table 结构体
Elf64_Shdr elf_shdr;
// 打开文件并读取ELF头信息到 elf_ehdr 上
int old_file = open(elf_file, O_RDWR);
read(old_file, &elf_ehdr, sizeof(elf_ehdr));
// 判断是否是一个 ELF 文件
if (!is_elf(elf_ehdr))
{
printf("此文件不是 ELF 文件\n");
exit(0);
}
old_entry = elf_ehdr.e_entry;
old_shoff = elf_ehdr.e_shoff;
// 节区头部表格的偏移量增加一页
elf_ehdr.e_shoff += PAGESIZE;
int flag = 0;
int i = 0;
printf("开始修改程序头部表\n");
// 读取并修改程序头部表
for (i = 0; i < elf_ehdr.e_phnum; i++)
{
// 寻找并读取到 elf_phdr 中
lseek(old_file, elf_ehdr.e_phoff + i * elf_ehdr.e_phentsize, SEEK_SET);
read(old_file, &elf_phdr, sizeof(elf_phdr));
if (flag)
{
// 增加 p_offset 一页大小 4k
elf_phdr.p_offset += PAGESIZE;
// 寻找并更新 程序头部
lseek(old_file, elf_ehdr.e_phoff + i * elf_ehdr.e_phentsize, SEEK_SET);
write(old_file, &elf_phdr, sizeof(elf_phdr));
}
else if (PT_LOAD == elf_phdr.p_type && elf_phdr.p_offset == 0)
{ // 数组元素可加载的段,程序段入口
if (elf_phdr.p_filesz != elf_phdr.p_memsz)
exit(0);
// 修改新的程序入口虚拟地址
elf_ehdr.e_entry = elf_phdr.p_vaddr + elf_phdr.p_filesz;
// 寻找并更新 ELF 头部
lseek(old_file, 0, SEEK_SET);
write(old_file, &elf_ehdr, sizeof(elf_ehdr));
old_phsize = elf_phdr.p_filesz;
// 增加 p_filesz 和 p_memsz 一页大小 4k
elf_phdr.p_filesz += PAGESIZE;
elf_phdr.p_memsz += PAGESIZE;
// 更新程序头部表
lseek(old_file, elf_ehdr.e_phoff + i * elf_ehdr.e_phentsize, SEEK_SET);
write(old_file, &elf_phdr, sizeof(elf_phdr));
flag = 1;
}
}
printf("开始修改节区头部表\n");
// 读取并修改节区头部表
for (i = 0; i < elf_ehdr.e_shnum; i++)
{
// 寻找并读取节区头部表
lseek(old_file, i * sizeof(elf_shdr) + old_shoff, SEEK_SET);
read(old_file, &elf_shdr, sizeof(elf_shdr));
if (i == 0)
{
// 第一个节区增加一页
elf_shdr.sh_size += PAGESIZE;
}
else
{
// 节区偏移地址增加一页
elf_shdr.sh_offset += PAGESIZE;
}
// 寻找并更新节区头部表
lseek(old_file, old_shoff + i * sizeof(elf_shdr), SEEK_SET);
write(old_file, &elf_shdr, sizeof(elf_shdr));
}
printf("开始插入注入程序\n");
// 插入注入程序
insert(elf_ehdr, old_file, old_entry, old_phsize);
}
// 插入
void insert(Elf64_Ehdr elf_ehdr, int old_file, int old_entry, int old_phsize)
{
// 程序的原始入口地址
int old_entry_addr[4];
cal_addr(old_entry, old_entry_addr);
// 数据段的地址, 73为数组中程序数据段的相对位置
int data_entry = elf_ehdr.e_entry + 45;
int data_addr[4];
cal_addr(data_entry, data_addr);
// 每一行对应一条汇编代码
char inject_code[] = {
0x50,
0x53,
0x51,
0x52,
0x48, 0xc7, 0xc0, 0x04, 0x00, 0x00, 0x00,
0x48, 0xc7, 0xc3, 0x01, 0x00, 0x00, 0x00,
0x48, 0xc7, 0xc1,data_addr[0], data_addr[1], data_addr[2], data_addr[3],
0x48, 0xc7, 0xc2, 0x0a, 0x00, 0x00, 0x00,
0xcd, 0x80,
0x5a,
0x59,
0x5b,
0x58,
// 此处在原来的汇编程序中为程序中断指令,修改为跳转到原入口地址elfh.e_entry
0xbd, old_entry_addr[0], old_entry_addr[1], old_entry_addr[2], old_entry_addr[3], 0xff, 0xe5,
//数据区域 helloworld
0x68, 0x65, 0x6c, 0x6c, 0x6f,
0x77, 0x6f,
0x72, 0x6c,
0x64,
0x00
};
int inject_size = sizeof(inject_code);
// 防止注入代码太大
if (inject_size > (PAGESIZE - (elf_ehdr.e_entry % PAGESIZE)))
{
printf("注入代码太大\n");
exit(0);
}
struct stat file_stat;
fstat(old_file, &file_stat);
char *data = (char *)malloc(file_stat.st_size - old_phsize);
// 存储原程序从节区末尾到目标节区头的数据
lseek(old_file, old_phsize, SEEK_SET);
read(old_file, data, file_stat.st_size - old_phsize);
// 插入注入代码到原 elf 文件中
lseek(old_file, old_phsize, SEEK_SET);
write(old_file, inject_code, inject_size);
// 扩充到一页
char tmp[PAGESIZE] = {0};
memset(tmp, PAGESIZE - inject_size, 0);
write(old_file, tmp, PAGESIZE - inject_size);
// 再将原始的数据接在注入代码后面插入
write(old_file, data, file_stat.st_size - old_phsize);
free(data);
printf("注入完成\n");
}
int main(int argc, char **argv)
{
//检查参数数量是否正确
if (argc != 2)
{
printf("缺少 elf 文件参数,格式:./main <elf_filepath>\n");
exit(0);
}
inject(argv[1]);
return 0;
}