写本文的目的是,其一是工作需求,其二就是网上所有关于ASLR相关的文章基本都只讲解其中的概念以及一些场景应用,并没有详细解析相关实现具体过程的,所以一时兴起,写了这一篇文章,希望能够给有这方面需求的人,或者感兴趣的人提供一些帮助。
首先,我们需要一个config, 这个config是default 的有了这个config,就能生成地址随机化的kernel了
这个config会添加一个link flag,如图所示,这个flag的作用是使每个section会生成对应的rela section,其中记录每个对应section中需要relocation的symbol在文件中的位置。
接下来我们来compile一下image。
编译的是基于x86_64架构的bzImage,我们先通过readelf查看目录下的vmlinux,可以看到我们需要的rela section,如图
部分rela section内容如下图所示
vmlinux是未经压缩的Linux内核,是编译出来的最原始的文件。
接下来再来看看如何对该文件进行进一步的处理。
来到arch/x86/boot/compressed目录下,打开Makefile,这个Makefile主要用来编译压缩后的内核image的,具体如何生成压缩后的image,可以参考《深度探索Linux操作系统:系统构建和原理解析》,这本书讲解的非常详细。在这里我们只关注relocation相关的部分。
如上图所示,主要生成vmlinux.relocs文件,这个文件包含了所有需要relocation符号的信息,由上图可以看到是使用relocs这个工具对 vmlinux进行处理,现在我们看看这个工具的具体工作内容。
int main(int argc, char **argv)
{
int show_absolute_syms, show_absolute_relocs, show_reloc_info;
int as_text, use_real_mode;
const char *fname;
FILE *fp;
int i;
unsigned char e_ident[EI_NIDENT];
show_absolute_syms = 0;
show_absolute_relocs = 0;
show_reloc_info = 0;
as_text = 0;
use_real_mode = 0;
fname = NULL;
for (i = 1; i < argc; i++) {
char *arg = argv[i];
if (*arg == '-') {
if (strcmp(arg, "--abs-syms") == 0) {
show_absolute_syms = 1;
continue;
}
if (strcmp(arg, "--abs-relocs") == 0) {
show_absolute_relocs = 1;
continue;
}
if (strcmp(arg, "--reloc-info") == 0) {
show_reloc_info = 1;
continue;
}
if (strcmp(arg, "--text") == 0) {
as_text = 1;
continue;
}
if (strcmp(arg, "--realmode") == 0) {
use_real_mode = 1;
continue;
}
}
else if (!fname) {
fname = arg;
continue;
}
usage();
}
if (!fname) {
usage();
}
fp = fopen(fname, "r");
if (!fp) {
die("Cannot open %s: %s\n", fname, strerror(errno));
}
if (fread(&e_ident, 1, EI_NIDENT, fp) != EI_NIDENT) {
die("Cannot read %s: %s", fname, strerror(errno));
}
rewind(fp);
if (e_ident[EI_CLASS] == ELFCLASS64)
process_64(fp, use_real_mode, as_text,
show_absolute_syms, show_absolute_relocs,
show_reloc_info);
else
process_32(fp, use_real_mode, as_text,
show_absolute_syms, show_absolute_relocs,
show_reloc_info);
fclose(fp);
return 0;
}
上述代码为relocs工具的源码,在之前的Makefile图中可以知道工具在arch/x86/tools目录下。主要作用是对参数进行处理,打开vmlinux文件,并对该文件进行解析,提取relocation相关section的信息。其中比较重要的函数如下图所示。
static void emit_relocs(int as_text, int use_real_mode)
{
int i;
int (*write_reloc)(uint32_t, FILE *) = write32;
int (*do_reloc)(struct section *sec, Elf_Rel *rel, Elf_Sym *sym,
const char *symname);
#if ELF_BITS == 64
if (!use_real_mode)
do_reloc = do_reloc64;
else
die("--realmode not valid for a 64-bit ELF file");
#else
if (!use_real_mode)
do_reloc = do_reloc32;
else
do_reloc = do_reloc_real;
#endif
/* Collect up the relocations */
walk_relocs(do_reloc);
if (relocs16.count && !use_real_mode)
die("Segment relocations found but --realmode not specified\n");
/* Order the relocations for more efficient processing */
sort_relocs(&relocs32);
#if ELF_BITS == 64
sort_relocs(&relocs32neg);
sort_relocs(&relocs64);
#else
sort_relocs(&relocs16);
#endif
/* Print the relocations */
if (as_text) {
/* Print the relocations in a form suitable that
* gas will like.
*/
printf(".section \".data.reloc\",\"a\"\n");
printf(".balign 4\n");
write_reloc = write32_as_text;
}
if (use_real_mode) {
write_reloc(relocs16.count, stdout);
for (i = 0; i < relocs16.count; i++)
write_reloc(relocs16.offset[i], stdout);
write_reloc(relocs32.count, stdout);
for (i = 0; i < relocs32.count; i++)
write_reloc(relocs32.offset[i], stdout);
} else {
#if ELF_BITS == 64
/* Print a stop */
write_reloc(0, stdout);
/* Now print each relocation */
for (i = 0; i < relocs64.count; i++)
write_reloc(relocs64.offset[i], stdout);
/* Print a stop */
write_reloc(0, stdout);
/* Now print each inverse 32-bit relocation */
for (i = 0; i < relocs32neg.count; i++)
write_reloc(relocs32neg.offset[i], stdout);
#endif
/* Print a stop */
write_reloc(0, stdout);
/* Now print each relocation */
for (i = 0; i < relocs32.count; i++)
write_reloc(relocs32.offset[i], stdout);
}
}
该函数首先判断Linux运行在何种模式下,不同模式下的符号地址信息并不相同,在这里只考虑64bit环境,接下来就是对vmlinux ELF文件进行详细解析了,代码如下。
static void walk_relocs(int (*process)(struct section *sec, Elf_Rel *rel,
Elf_Sym *sym, const char *symname))
{
int i;
/* Walk through the relocations */
for (i = 0; i < ehdr.e_shnum; i++) {
char *sym_strtab;
Elf_Sym *sh_symtab;
struct section *sec_applies, *sec_symtab;
int j;
struct section *sec = &secs[i];
if (sec->shdr.sh_type != SHT_REL_TYPE) {
continue;
}
sec_symtab = sec->link;
sec_applies = &secs[sec->shdr.sh_info];
if (!(sec_applies->shdr.sh_flags & SHF_ALLOC)) {
continue;
}
sh_symtab = sec_symtab->symtab;
sym_strtab = sec_symtab->link->strtab;
for (j = 0; j < sec->shdr.sh_size/sizeof(Elf_Rel); j++) {
Elf_Rel *rel = &sec->reltab[j];
Elf_Sym *sym = &sh_symtab[ELF_R_SYM(rel->r_info)];
const char *symname = sym_name(sym_strtab, sym);
process(sec, rel, sym, symname);
}
}
}
该函数主要是遍历ELF文件中所有的section,提取出其中的SHT_REL_TYPE类型的section逐一进行解析,该类型的section就是我们所需要的包含符号地址信息的section。具体的解析工作由函数指针process完成,就是上文中提到的不同模式下的Linux,有不同类型的符号地址信息。针对64bit环境,代码如下。
static int do_reloc64(struct section *sec, Elf_Rel *rel, ElfW(Sym) *sym,
const char *symname)
{
unsigned r_type = ELF64_R_TYPE(rel->r_info);
ElfW(Addr) offset = rel->r_offset;
int shn_abs = (sym->st_shndx == SHN_ABS) && !is_reloc(S_REL, symname);
if (sym->st_shndx == SHN_UNDEF)
return 0;
/*
* Adjust the offset if this reloc applies to the percpu section.
*/
if (sec->shdr.sh_info == per_cpu_shndx)
offset += per_cpu_load_addr;
switch (r_type) {
case R_X86_64_NONE:
/* NONE can be ignored. */
break;
case R_X86_64_PC32:
/*
* PC relative relocations don't need to be adjusted unless
* referencing a percpu symbol.
*/
if (is_percpu_sym(sym, symname))
add_reloc(&relocs32neg, offset);
break;
case R_X86_64_32:
case R_X86_64_32S:
case R_X86_64_64:
/*
* References to the percpu area don't need to be adjusted.
*/
if (is_percpu_sym(sym, symname))
break;
if (shn_abs) {
/*
* Whitelisted absolute symbols do not require
* relocation.
*/
if (is_reloc(S_ABS, symname))
break;
die("Invalid absolute %s relocation: %s\n",
rel_type(r_type), symname);
break;
}
/*
* Relocation offsets for 64 bit kernels are output
* as 32 bits and sign extended back to 64 bits when
* the relocations are processed.
* Make sure that the offset will fit.
*/
if ((int32_t)offset != (int64_t)offset)
die("Relocation offset doesn't fit in 32 bits\n");
if (r_type == R_X86_64_64)
add_reloc(&relocs64, offset);
else
add_reloc(&relocs32, offset);
break;
default:
die("Unsupported relocation type: %s (%d)\n",
rel_type(r_type), r_type);
break;
}
return 0;
}
上述代码主要作用是根据符号类型往动态分配的内存中写入符号地址信息。特别注意的是如果是64bit的符号地址,将会截断成32bit,具体说明将会在下一章说到。得到所有的符号地址信息后,按照类型对符号地址从小到大进行排序,我并不清楚这一步的具体意义是什么,或许是为了Linux进行动态的relocation的时候寻址更加快捷有序,或许就是有强迫症,看上去更加美观,希望知道的大神能够解惑。最后一步就是把所有符号地址信息输出到vmlinux.relocs中,通过重定向stdout实现,其中不同类型之间还需要填0来保证了Linux做relocation时对不同类型的符号的识别。
得到vmlinux.relocs后,它将会跟处理了符号信息后的vmlinux一起进行压缩,并且压缩在vmlinux image的后面,下一章将会进行说明,如下图所示。
压缩之后的处理过程是通过工具mkpiggy实现的,如下图所示,具体过程请参考上面所说的书籍,不在重复了。
总结一下,这一篇主要说了一些Linux需要relocation的前提准备工作,这是静态的步骤,主要涉及到Linux的系统构建,Makefile,链接脚本以及ELF文件相关的知识,具体可参照一些其他书籍来学习。当然还有一些具体相关的细节并没有进行深入的探索,比如per_cpu相关的符号的处理。在下一章中我将会详细介绍Linux在启动过程的解压并且如何实现物理地址和虚拟地址随机化的。