从core映像文件中重新构造ELF可执行文件 整理:e4gle(大鹰) 来源:http://e4gle.org 从core映像文件中重新构造ELF可执行文件 ------------------------------------------------ - Silvio Cesare - December 1999 - http://www.big.net.au/~silvio - http://virus.beergrave.net/ 整理:e4gle from e4gle.org 目录 ----------------- 2.0到2.2内核的改变 绪论 进程映像 core映像 重建可执行文件 重建失败的一些例子 实现 2.0到2.2内核的改变 ------------------------------ 本文主要是针对linux的2.0.x内核,但是这些代码应该也可以在2.2.x执行.2.0.x内核和2.2.x内 核的内存映像是有区别的,包括ELF的core dump的映像我想也有所改变.译者注:我尽力调试此文档 使它可以适合2.2.x内核. 绪论 ------------ 这篇文档实践并讲述了在给定一个core dump或者进程映像的快照文件下重新构造ELF可格式的二进制 可执行文件的技术.对于本文的读者,ELF格式的相关知识是必要的. 进程映像 ----------------- 简单来说,一个core映像就是进程映像发生dump的那个时候的快照.进程映像包括了许多可加载的程序段 或虚拟内存区.这在一个ELF格式的二进制文件里涉及程序头,在linux内核里涉及到vm_area_struct 结构.一个core dump就是vm_area_struct的dump,而相应的可执行程序头和共享库用来创建进程 映像.在linux里,一组vm_area_struct为proc伪文件系统提供了内存映像.我们看一下下面这个例子, 这是一个拥了libc的典型的映像: [e4gle@linux]# cat /proc/31189/maps 08048000-0804d000 r-xp 00000000 03:08 243714 /bin/login 0804d000-0804e000 rw-p 00004000 03:08 243714 /bin/login 0804e000-0805a000 rwxp 00000000 00:00 0 40000000-40013000 r-xp 00000000 03:08 304059 /lib/ld-2.1.3.so 40013000-40014000 rw-p 00012000 03:08 304059 /lib/ld-2.1.3.so 40014000-40016000 rw-p 00000000 00:00 0 40016000-40018000 r-xp 00000000 03:08 96347 /lib/security/pam_securetty.so 40018000-40019000 rw-p 00001000 03:08 96347 /lib/security/pam_securetty.so 40019000-4001a000 r-xp 00000000 03:08 96341 /lib/security/pam_nologin.so 4001a000-4001b000 rw-p 00000000 03:08 96341 /lib/security/pam_nologin.so 4001c000-40021000 r-xp 00000000 03:08 304068 /lib/libcrypt-2.1.3.so 40021000-40022000 rw-p 00004000 03:08 304068 /lib/libcrypt-2.1.3.so 40022000-40049000 rw-p 00000000 00:00 0 40049000-40050000 r-xp 00000000 03:08 304304 /lib/libpam.so.0.72 40050000-40051000 rw-p 00006000 03:08 304304 /lib/libpam.so.0.72 40051000-40053000 r-xp 00000000 03:08 304075 /lib/libdl-2.1.3.so 40053000-40055000 rw-p 00001000 03:08 304075 /lib/libdl-2.1.3.so 40055000-40057000 r-xp 00000000 03:08 304307 /lib/libpam_misc.so.0.72 40057000-40058000 rw-p 00001000 03:08 304307 /lib/libpam_misc.so.0.72 40058000-40059000 rw-p 00000000 00:00 0 40059000-40146000 r-xp 00000000 03:08 304066 /lib/libc-2.1.3.so 40146000-4014a000 rw-p 000ec000 03:08 304066 /lib/libc-2.1.3.so bfff9000-c0000000 rwxp ffffa000 00:00 0 从上面可以看到,我举了一个login程序的例子,首先的两块内存区域用虚拟地址08048000-0804d000 和0804d000-0804e000分别对应了文本段和数据段.注意一下也是有权限设置的.同时内存区域仅仅 由页边界来决定.所有的core dump或映像内存区域都取决于页边界.意思是最小的内存区域就是一个 页的长度.需要注意的是由ELF格式的程序头表现的程序段是和页边界无关的,所以程序段不会在虚拟 内存区域产生映像.后面几个区域是动态链接相关的库的加载,最后一行是栈. CORE映像 -------------- core映像就是进程dump出来的映像,具有一些额外寄存器的节和一些有用的信息.在一个ELF的core 映像里,进程映像的的内存区域相对应程序段,所以一个core文件拥有一个针对每个虚拟内存空间的 程序头列表.关于寄存器的信息存储在ELF二进制格式的notes节里.从一个core dump或者进程映像 里来重建可执行文件,我们可以忽略寄存器且把精力仅仅集中在内存区域上. 重建可执行文件 -------------------------- 从一个core dump的文件里重建可执行文件我们必须从core映像中提取ELF可执行所需的文本段和 数据段对应的内存区域.当在加载代码段的时候,ELF头和程序头也同时加载进内存了(这样可以提高 效率)所以我们可以利用这些来创建可执行映像.可执行的ELF头包括一些象真实的代码段和数据段的 起始地址和大小这样的信息(记住,内存区域取决于页边界). 现在,假如我们只在我们重建的文件中用到代码段和数据段,导致的结果就使我们的可执行程序只可 以工作在被创建这个程序的系统上.因为PLT可能拥有一个共享库函数指向它的加载值.移动二进制 程序会使库函数不同的位置,或者使函数改变位置.所以,只能在重建的系统上运行,要使可以运行在 系统就必须使整个映像(栈除外)包括在重建的可执行程序里,这在下面的程序可以反应出来. 重建失败的一些例子 -------------------------- 重建的一些问题,进程映像的快照是实时运行的,并不是起始时间,所以数据段的值可能会被改掉,数据 段是可写的.看看下面的代码 static int i = 0; int main() { if (i++) exit(0); printf("Hi\n"); } 在这个例子中,重建映像会导致程序立即退出,因为它依靠全局变量i的初始值来判定程序的流程. 实现 ---------------------- 其实重建可执行映像没用到很高深的理论,它只是把一个只有执行权限的可执行程序复制出来而已. 创建一个core dump不难,只需要给进程发送一个SIGSEGV信号,core映像就从进程映像中被拷贝 到了proc文件系统里了. -- [e4gle@linux]$ cat test_harness.c int main() { for (;;) printf("Hi\n"); } [e4gle@linux]$ gcc test_harness.c -o test_harness [e4gle@linux]$ ./test_harness <-验证该程序的输出(e4gle:好像杀不掉了,所以为了便于测试 我采用后台运行它,再给它发送一个SIGSEGV信号) Hi Hi Hi . . . [e4gle@linux]$ ./test_harness >/dev/null & [1] 15254 [e4gle@linux]# ps -eaf|grep test_harness root 15254 15229 99 17:16 pts/3 00:00:19 ./test_harness root 15256 15229 0 17:17 pts/3 00:00:00 grep test_harness [e4gle@linux]# kill -SIGSEGV 15254 <-使它core dump [e4gle@linux]$ gcc -o core_reconstruct core_reconstruct.c [e4gle@linux]$ ./core_reconstruct <-我们写的提取例程来从core中提出可执行映像 [e4gle@linux]$ ./a.out <-测试我们提取出来的可执行文件 Hi Hi Hi . . . 以下是提取core文件到可执行程序的例程.(e4gle:这个程序还是很容易理解的:) --------------------------------- CUT --------------------------------------- #include #include #include #include #include #include #include void die(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); exit(1); } #define PAGE_SIZE 4096 static char shstr[] = "\0" ".symtab\0" ".strtab\0" ".shstrtab\0" ".interp\0" ".hash\0" ".dynsym\0" ".dynstr\0" ".rel.got\0" ".rel.bss\0" ".rel.plt\0" ".init\0" ".plt\0" ".text\0" ".fini\0" ".rodata\0" ".data\0" ".ctors\0" ".dtors\0" ".got\0" ".dynamic\0" ".bss\0" ".comment\0" ".note" ; char *xget(int fd, int off, int sz) { char *buf; if (lseek(fd, off, SEEK_SET) < 0) die("Seek error"); buf = (char *)malloc(sz); if (buf == NULL) die("No memory"); if (read(fd, buf, sz) != sz) die("Read error"); return buf; } void do_elf_checks(Elf32_Ehdr *ehdr) { if (strncmp(ehdr->e_ident, ELFMAG, SELFMAG)) die("File not ELF"); if (ehdr->e_type != ET_CORE) die("ELF type not ET_CORE"); if (ehdr->e_machine != EM_386 && ehdr->e_machine != EM_486) die("ELF machine type not EM_386 or EM_486"); if (ehdr->e_version != EV_CURRENT) die("ELF version not current"); } int main(int argc, char *argv[]) { Elf32_Ehdr ehdr, *core_ehdr; Elf32_Phdr *phdr, *core_phdr, *tmpphdr; Elf32_Shdr shdr; char *core; char *data[2], *core_data[3]; int prog[2], core_prog[3]; int in, out; int i, p; int plen; if (argc > 2) die("usage: %s [core-file]"); if (argc == 2) core = argv[1]; else core = "core"; in = open(core, O_RDONLY); if (in < 0) die("Coudln't open file: %s", core); if (read(in, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) die("Read error"); do_elf_checks(&ehdr); if (lseek(in, ehdr.e_phoff, SEEK_SET) < 0) die("Seek error"); phdr = (Elf32_Phdr *)malloc(plen = sizeof(Elf32_Phdr)*ehdr.e_phnum); if (read(in, phdr, plen) != plen) die("Read error"); for (i = 0; i < ehdr.e_phnum; i++) printf("0x%x - 0x%x (%i)\n", phdr[i].p_vaddr, phdr[i].p_vaddr + phdr[i].p_memsz, phdr[i].p_memsz); /* copy segments (in memory) prog/data[0] ... text prog/data[1] ... data prog/data[2] ... dynamic */ for (i = 0, p = 0; i < ehdr.e_phnum; i++) { if ( phdr[i].p_vaddr >= 0x8000000 && phdr[i].p_type == PT_LOAD ) { prog[p] = i; if (p == 1) break; ++p; } } if (i == ehdr.e_phnum) die("Couldnt find TEXT/DATA"); for (i = 0; i < 2; i++) data[i] = xget( in, phdr[prog[i]].p_offset, (phdr[prog[i]].p_memsz + 4095) & 4095 ); core_ehdr = (Elf32_Ehdr *)&data[0][0]; core_phdr = (Elf32_Phdr *)&data[0][core_ehdr->e_phoff]; for (i = 0, p = 0; i < core_ehdr->e_phnum; i++) { if (core_phdr[i].p_type == PT_LOAD) { core_prog[p] = i; if (p == 0) { core_data[0] = &data[0][0]; } else { core_data[1] = &data[1][ (core_phdr[i].p_vaddr & 4095) ]; break; } ++p; } } if (i == core_ehdr->e_phnum) die("No TEXT and DATA segment"); for (i = 0; i < core_ehdr->e_phnum; i++) { if (core_phdr[i].p_type == PT_DYNAMIC) { core_prog[2] = i; core_data[2] = &data[1][64]; break; } } if (i == core_ehdr->e_phnum) die("No DYNAMIC segment"); out = open("a.out", O_WRONLY | O_CREAT | O_TRUNC); if (out < 0) die("Coudln't open file: %s", "a.out"); core_ehdr->e_shoff = core_phdr[core_prog[2]].p_offset + core_phdr[core_prog[2]].p_filesz + sizeof(shstr); /* text data bss dynamic shstrtab */ core_ehdr->e_shnum = 6; core_ehdr->e_shstrndx = 5; for (i = 0; i < 2; i++) { Elf32_Phdr *p = &core_phdr[core_prog[i]]; int sz = p->p_filesz; if (lseek(out, p->p_offset, SEEK_SET) < 0) goto cleanup; if (write(out, core_data[i], sz) != sz) goto cleanup; } if (write(out, shstr, sizeof(shstr)) != sizeof(shstr)) goto cleanup; memset(&shdr, 0, sizeof(shdr)); if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup; /* text section */ tmpphdr = &core_phdr[core_prog[0]]; shdr.sh_name = 95; shdr.sh_type = SHT_PROGBITS; shdr.sh_addr = tmpphdr->p_vaddr; shdr.sh_offset = 0; shdr.sh_size = tmpphdr->p_filesz; shdr.sh_flags = SHF_ALLOC | SHF_EXECINSTR; shdr.sh_link = 0; shdr.sh_info = 0; shdr.sh_addralign = 16; shdr.sh_entsize = 0; if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup; /* data section */ tmpphdr = &core_phdr[core_prog[1]]; shdr.sh_name = 115; shdr.sh_type = SHT_PROGBITS; shdr.sh_addr = tmpphdr->p_vaddr; shdr.sh_offset = tmpphdr->p_offset; shdr.sh_size = tmpphdr->p_filesz; shdr.sh_flags = SHF_ALLOC | SHF_WRITE; shdr.sh_link = 0; shdr.sh_info = 0; shdr.sh_addralign = 4; shdr.sh_entsize = 0; if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup; /* dynamic section */ for (i = 0; i < core_ehdr->e_phnum; i++) { if (core_phdr[i].p_type == PT_DYNAMIC) { tmpphdr = &core_phdr[i]; break; } } shdr.sh_name = 140; shdr.sh_type = SHT_PROGBITS; shdr.sh_addr = tmpphdr->p_vaddr; shdr.sh_offset = tmpphdr->p_offset; shdr.sh_size = tmpphdr->p_memsz; shdr.sh_flags = SHF_ALLOC; shdr.sh_link = 0; shdr.sh_info = 0; shdr.sh_addralign = 4; shdr.sh_entsize = 8; if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup; /* bss section */ shdr.sh_name = 149; shdr.sh_type = SHT_PROGBITS; shdr.sh_addr = tmpphdr->p_vaddr + tmpphdr->p_filesz; shdr.sh_offset = tmpphdr->p_offset + tmpphdr->p_filesz; shdr.sh_size = tmpphdr->p_memsz - tmpphdr->p_filesz; shdr.sh_flags = SHF_ALLOC; shdr.sh_link = 0; shdr.sh_info = 0; shdr.sh_addralign = 1; shdr.sh_entsize = 0; if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup; /* shstrtab */ shdr.sh_name = 17; shdr.sh_type = SHT_STRTAB; shdr.sh_addr = 0; shdr.sh_offset = core_ehdr->e_shoff - sizeof(shstr); shdr.sh_size = sizeof(shstr); shdr.sh_flags = 0; shdr.sh_link = 0; shdr.sh_info = 0; shdr.sh_addralign = 1; shdr.sh_entsize = 0; if (write(out, &shdr, sizeof(shdr)) != sizeof(shdr)) goto cleanup; return 0; cleanup: unlink("a.out"); die("Error writing file: %s", "a.out"); return 1; /* not reached */ }