泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

泰晓Linux知识星球:1300+知识点,520+用户
请稍侯

ELF转二进制(4/4):动态计算并修改数据加载地址

Wu Zhangjin 创作于 2020/03/16

By Falcon of TinyLab.org Dec 09, 2019

1 背景简介

有一天,某位同学在讨论群聊起来:

除了直接把 C 语言程序编译成 ELF 运行以外,是否可以转成二进制,然后通过第三方程序加载到内存后再运行。

带着这样的问题,我们写了四篇文章,这是其四。

前面几篇把汇编、静态链接、静态加载、动态加载都讲了,还差一个缺憾,那就是动态链接。

每次得加载到一个特定地址其实不是那么灵活,所以,在不修改代码的前提下,如果能保证加载到任意地址,而且还能访问数据的话,就需要“动态链接”。

2 动态计算数据地址

就是数据的地址可以动态分配,本文模拟一下这个过程。

这里用到的例子是最原始的 hello.s,未经 hack 的:

  1. # hello.s
  2. #
  3. # as --32 -o hello.o hello.s
  4. # ld -melf_i386 -o hello hello.o
  5. # objcopy -O binary hello hello.bin
  6. #
  7. .text
  8. .global _start
  9. _start:
  10. xorl %eax, %eax
  11. movb $4, %al # eax = 4, sys_write(fd, addr, len)
  12. xorl %ebx, %ebx
  13. incl %ebx # ebx = 1, standard output
  14. movl $.LC0, %ecx # ecx = $.LC0, the addr of string
  15. xorl %edx, %edx
  16. movb $13, %dl # edx = 13, the length of .string
  17. int $0x80
  18. xorl %eax, %eax
  19. movl %eax, %ebx # ebx = 0
  20. incl %eax # eax = 1, sys_exit
  21. int $0x80
  22. .section .rodata
  23. .LC0:
  24. .string "Hello World\xa\x0"

汇编后看看:

  1. $ objdump -dr hello.o
  2. hello.o: file format elf32-i386
  3. Disassembly of section .text:
  4. 00000000 <_start>:
  5. 0: 31 c0 xor %eax,%eax
  6. 2: b0 04 mov $0x4,%al
  7. 4: 31 db xor %ebx,%ebx
  8. 6: 43 inc %ebx
  9. 7: b9 00 00 00 00 mov $0x0,%ecx
  10. 8: R_386_32 .rodata
  11. c: 31 d2 xor %edx,%edx
  12. e: b2 0d mov $0xd,%dl
  13. 10: cd 80 int $0x80
  14. 12: 31 c0 xor %eax,%eax
  15. 14: 89 c3 mov %eax,%ebx
  16. 16: 40 inc %eax
  17. 17: cd 80 int $0x80

我们希望能够动态链接,也就是在运行时根据 Binary 文件加载的位置,动态计算出数据地址(不用修改代码本身)。也就是说确认 R_386_32 这个类型的地址。

这个地址可以用 Binary 文件的加载地址加上 .rodata 相对 Binary 文件开头(即 .text 起始位置)的偏移算出来。

如上所示,.text 的大小是 0x19(0x17+2),那么 .rodata 跟 .text 的偏移刚好是 0x19,那数据地址可以通过 Binary load addr + 0x19 得出。其实这里也可以直接用 readelf -s hello.o 获得(看 Size 部分):

  1. $ readelf -S hello.o | egrep " .text|Size"
  2. [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
  3. [ 1] .text PROGBITS 00000000 000034 000019 00 AX 0 0 1

3 动态修改数据地址

另外一个是,这个地址需要编码回代码里头,即 “8: R_386_32 .rodata” 这里,也就是 .text 中的第 8 个字节开始的一个 32 位地址(the absolute 32-bit address of the symbol into the specified memory location,可参考:c - meaning of R_386_32/R_386_PC32 in .rel.text se…

上面得到了两个关键数据:

  1. 数据偏移(相对 .text 起始位置):0x19
  2. 代码偏移(相对 .text 起始位置):8,这个地方要写入上面的地址

需要做的是,用 .text addr + 0x19 算出数据地址,然后写入 .text addr + 8 这个位置。

说明一下,在实际的动态链接中,上面两个地址都是可以根据 ELF 中的相关数据,动态计算出来的,这里因为是用到了 Binary 文件,我们通过 objdump/readelf 获取到数据访问指令地址和数据偏移后来模拟动态计算。

.text addr 这个地址在运行时确定,这里由 mmap 动态获得,接下来改动代码如下,核心部分如下:

  1. /* dynamic linking: update the data address */
  2. /* offset are gotten from this command: $ objdump -dr hello.o */
  3. #define inst_offset 8
  4. #define data_offset 0x19
  5. *(unsigned int *)(addr + inst_offset) = (uint)addr + data_offset;

addr 即 mmap 加载 Binary 文件到内存时分配的地址,也是 .text 的起始地址(Binary 文件开头就是 .text,接着是 .rodata)。

完整代码如下:

  1. $ diff --git a/mmap.orig.c b/mmap.any.c
  2. index 640bcb0..87358d9 100644
  3. --- a/mmap.orig.c
  4. +++ b/mmap.any.c
  5. @@ -11,7 +11,7 @@
  6. int
  7. main(int argc, char *argv[])
  8. {
  9. - char *addr;
  10. + char *addr = NULL;
  11. int fd;
  12. struct stat sb;
  13. off_t offset, pa_offset;
  14. @@ -19,7 +19,7 @@ main(int argc, char *argv[])
  15. ssize_t s;
  16. if (argc < 3 || argc > 4) {
  17. - fprintf(stderr, "%s file offset [length]\n", argv[0]);
  18. + fprintf(stderr, "%s file offset [addr]\n", argv[0]);
  19. exit(EXIT_FAILURE);
  20. }
  21. @@ -40,20 +40,25 @@ main(int argc, char *argv[])
  22. }
  23. if (argc == 4) {
  24. - length = atoi(argv[3]);
  25. - if (offset + length > sb.st_size)
  26. - length = sb.st_size - offset;
  27. - /* Can't display bytes past end of file */
  28. -
  29. - } else { /* No length arg ==> display to end of file */
  30. - length = sb.st_size - offset;
  31. + sscanf(argv[3], "%p", &addr);
  32. }
  33. + length = sb.st_size - offset;
  34. - addr = mmap(NULL, length + offset - pa_offset, PROT_READ,
  35. - MAP_PRIVATE, fd, pa_offset);
  36. + addr = mmap((void *)addr, length + offset - pa_offset, PROT_READ|PROT_WRITE|PROT_EXEC,
  37. + MAP_PRIVATE|MAP_FIXED|MAP_POPULATE, fd, pa_offset);
  38. if (addr == MAP_FAILED)
  39. handle_error("mmap");
  40. + /* dynamic linking: update the data address */
  41. + /* offset are gotten from this command: $ objdump -dr hello.o */
  42. + #define inst_offset 8
  43. + #define data_offset 0x19
  44. + *(unsigned int *)(addr + inst_offset) = (uint)addr + data_offset;
  45. +
  46. + /* flush icache: Using the GNU Compiler Collection (GCC): Other Bui... */
  47. + __builtin___clear_cache(addr, addr + length + offset - pa_offset);
  48. +
  49. +#if 0
  50. s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
  51. if (s != length) {
  52. if (s == -1)
  53. @@ -62,6 +67,9 @@ main(int argc, char *argv[])
  54. fprintf(stderr, "partial write");
  55. exit(EXIT_FAILURE);
  56. }
  57. +#else
  58. + ((void (*)(void))addr)();
  59. +#endif
  60. exit(EXIT_SUCCESS);
  61. }

编译和运行如下:

  1. $ gcc -m32 -o mmap.any mmap.any.c
  2. $ sudo ./mmap.any ./hello.bin 0
  3. Hello World
  4. $ ./mmap.any ./hello.bin 0 0x8042000
  5. Hello World

如上演示,这里不再需要提前把 hello 链接到一个“合法”的地址,而是让 mmap 自己合理分配一个,当然,也兼容自己指定一个合法的加载地址。

上述代码还加了一个函数,用于刷新指令 Cache,确保运行结果的一致性,这个函数是 gcc 提供的:__builtin___clear_cache,详细介绍在 c - Is there a way to flush the entire CPU cache…

4 小结

未来可以考虑进一步完善 Binary 文件结构,在后面加入 inst offset 和 data offset 的数组,这样,在运行时就可以借用这个数据,不用在代码中写死。

到这里,四篇文章,比较系统地介绍了如何把一个 ELF 文件转换为 Binary 文件,并通过第三方程序加载和执行,内容涉及静态嵌入、位置无关、动态加载以及动态计算和修改数据地址。

欢迎订阅吴老师的 10 小时 C 语言进阶视频课:《360° 剖析 Linux ELF》,课程提供了超过 70 多份实验材料,其中 15 个例子演示了 15 种程序执行的方法。



Read Related:

Read Latest: