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

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

Wu Zhangjin 创作于 2020/03/16

By Falcon of TinyLab.org Dec 09, 2019

背景简介

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

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

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

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

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

动态计算数据地址

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

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

# hello.s
#
# as --32 -o hello.o hello.s
# ld -melf_i386 -o hello hello.o
# objcopy -O binary hello hello.bin
#

    .text
.global _start
_start:
    xorl   %eax, %eax
    movb   $4, %al                  # eax = 4, sys_write(fd, addr, len)
    xorl   %ebx, %ebx
    incl   %ebx                     # ebx = 1, standard output
    movl   $.LC0, %ecx              # ecx = $.LC0, the addr of string
    xorl   %edx, %edx
    movb   $13, %dl                 # edx = 13, the length of .string
    int    $0x80
    xorl   %eax, %eax
    movl   %eax, %ebx               # ebx = 0
    incl   %eax                     # eax = 1, sys_exit
    int    $0x80

    .section .rodata
.LC0:
    .string "Hello World\xa\x0"

汇编后看看:

$ objdump -dr hello.o
hello.o:     file format elf32-i386

Disassembly of section .text:

00000000 <_start>:
   0:	31 c0                	xor    %eax,%eax
   2:	b0 04                	mov    $0x4,%al
   4:	31 db                	xor    %ebx,%ebx
   6:	43                   	inc    %ebx
   7:	b9 00 00 00 00       	mov    $0x0,%ecx
			8: R_386_32	.rodata
   c:	31 d2                	xor    %edx,%edx
   e:	b2 0d                	mov    $0xd,%dl
  10:	cd 80                	int    $0x80
  12:	31 c0                	xor    %eax,%eax
  14:	89 c3                	mov    %eax,%ebx
  16:	40                   	inc    %eax
  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 部分):

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

动态修改数据地址

另外一个是,这个地址需要编码回代码里头,即 “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 动态获得,接下来改动代码如下,核心部分如下:

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

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

完整代码如下:

$ diff --git a/mmap.orig.c b/mmap.any.c
index 640bcb0..87358d9 100644
--- a/mmap.orig.c
+++ b/mmap.any.c
@@ -11,7 +11,7 @@
 int
 main(int argc, char *argv[])
 {
-    char *addr;
+    char *addr = NULL;
     int fd;
     struct stat sb;
     off_t offset, pa_offset;
@@ -19,7 +19,7 @@ main(int argc, char *argv[])
     ssize_t s;

     if (argc < 3 || argc > 4) {
-        fprintf(stderr, "%s file offset [length]\n", argv[0]);
+        fprintf(stderr, "%s file offset [addr]\n", argv[0]);
         exit(EXIT_FAILURE);
     }

@@ -40,20 +40,25 @@ main(int argc, char *argv[])
     }

     if (argc == 4) {
-        length = atoi(argv[3]);
-        if (offset + length > sb.st_size)
-            length = sb.st_size - offset;
-                /* Can't display bytes past end of file */
-
-    } else {    /* No length arg ==> display to end of file */
-        length = sb.st_size - offset;
+        sscanf(argv[3], "%p", &addr);
     }
+    length = sb.st_size - offset;

-    addr = mmap(NULL, length + offset - pa_offset, PROT_READ,
-                MAP_PRIVATE, fd, pa_offset);
+    addr = mmap((void *)addr, length + offset - pa_offset, PROT_READ|PROT_WRITE|PROT_EXEC,
+                MAP_PRIVATE|MAP_FIXED|MAP_POPULATE, fd, pa_offset);
     if (addr == MAP_FAILED)
         handle_error("mmap");

+    /* dynamic linking: update the data address */
+    /* offset are gotten from this command: $ objdump -dr hello.o */
+    #define inst_offset 8
+    #define data_offset 0x19
+    *(unsigned int *)(addr + inst_offset) = (uint)addr + data_offset;
+
+    /* flush icache: Using the GNU Compiler Collection (GCC): Other Bui... */
+    __builtin___clear_cache(addr, addr + length + offset - pa_offset);
+
+#if 0
     s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
     if (s != length) {
         if (s == -1)
@@ -62,6 +67,9 @@ main(int argc, char *argv[])
         fprintf(stderr, "partial write");
         exit(EXIT_FAILURE);
     }
+#else
+    ((void (*)(void))addr)();
+#endif

     exit(EXIT_SUCCESS);
 }

编译和运行如下:

$ gcc -m32 -o mmap.any mmap.any.c
$ sudo ./mmap.any ./hello.bin 0
Hello World
$ ./mmap.any ./hello.bin 0 0x8042000
Hello World

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

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

小结

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

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

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

Read Related:

Read Latest: