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

ELF转二进制: 用 objcopy 把 ELF 转成 Binary 并运行

Wu Zhangjin 创作于 2020/02/27

By Falcon of TinyLab.org Dec 09, 2019

背景简介

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

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

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

这一篇先介绍如何把 ELF 文件转为二进制文件,然后把二进制文件作为一个 Section 加入到另外一个程序,然后在那个程序中访问该 Section 并运行。

准备工作

先准备一个非常简单的 X86 汇编:

# 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"

这段代码编译、运行后可以打印 Hello World。

通过 objcopy 转换为二进制文件

先来转换为二进制文件,可以用 objcopy。

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

分析转换过后的二进制代码和数据

如果要用 objcopy 做成 Binary 还能运行,怎么办呢?

首先来分析一下,转成 Binary 后的代码和数据如下:

$ hexdump -C hello.bin
00000000  31 c0 b0 04 31 db 43 b9  6d 80 04 08 31 d2 b2 0d  |1...1.C.m...1...|
00000010  cd 80 31 c0 89 c3 40 cd  80 48 65 6c 6c 6f 20 57  |..1...@..Hello W|
00000020  6f 72 6c 64 0a 00 00

可以发现,刚好只保留了代码和数据部分,其他控制相关的内容全部不见了,非常“纯正”。

再用 objdump 对照看看:

$ objdump -d -j .text hello

hello1:     file format elf32-i386


Disassembly of section .text:

08048054 <_start>:
 8048054:	31 c0                	xor    %eax,%eax
 8048056:	b0 04                	mov    $0x4,%al
 8048058:	31 db                	xor    %ebx,%ebx
 804805a:	43                   	inc    %ebx
 804805b:	b9 6d 80 04 08       	mov    $0x804806d,%ecx
 8048060:	31 d2                	xor    %edx,%edx
 8048062:	b2 0d                	mov    $0xd,%dl
 8048064:	cd 80                	int    $0x80
 8048066:	31 c0                	xor    %eax,%eax
 8048068:	89 c3                	mov    %eax,%ebx
 804806a:	40                   	inc    %eax
 804806b:	cd 80                	int    $0x80

需要注意这一行:

 804805b:	b9 6d 80 04 08       	mov    $0x804806d,%ecx

在 Binary 中,数据地址是被写死的。

所以,要让 hello.bin 能够运行,必须要把这段 Binary 装载在指定的位置,即:

$ nm hello | grep " _start"
08048054 T _start

这样取到的数据位置才是正确的。

如何运行转换过后的二进制

这个是内核压缩支持的惯用做法,先要取到 Load Address,告诉 wrapper kernel,必须把数据解压到 Load Address 开始的位置。

如果这个要在用户空间做呢?

看上去没那么容易,不过可以这么做:

  1. 把 hello.bin 作为一个 Section 加入到目标执行代码中,比如叫 run-bin.c
  2. 然后写 ld script 明确把 hello.bin 放到 Load Address 地址上
  3. 同时需要修改 ld script 中 run-bin 本身的默认加载地址,否则就覆盖了。也可以先把 hello 的 Load Address 往后搬动,这里用前者。

具体实现

先准备一个 run-bin.c,”bin_entry” 为 hello.bin 的入口,通过 ld script 定义。

#include <stdio.h>

extern void bin_entry(void);

int main(int argc, char *argv[])
{
	bin_entry();
	return 0;
}

接着,先拿到 run-bin.o:

$ gcc -c -o run-bin.o run-bin.c

把 hello.bin 作为 .bin section 加入进 run-bin.o:

$ objcopy --add-section .bin=hello.bin --set-section-flags .bin=contents,alloc,load,readonly run-bin.o

最后,修改链接脚本,先拿到一个默认的链接脚本作为 base:

$ ld -melf_i386 --verbose | sed -ne "/=======/,/=======/p" | grep -v "=======" > ld.script

修改如下,得到一个新的 ld.script.new:

$ git diff ld.script ld.script.new
diff --git a/ld.script b/ld.script.new
index 91f8c5c..7aecbbe 100644
--- a/ld.script
+++ b/ld.script.new
@@ -11,7 +11,7 @@ SEARCH_DIR("=/usr/local/lib/i386-linux-gnu"); SEARCH_DIR("=/lib/i386-linux-gnu")
 SECTIONS
 {
   /* Read-only sections, merged into text segment: */
-  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08048000)); . = SEGMENT_START("text-segment", 0x08048000) + SIZEOF_HEADERS;
+  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08046000)); . = SEGMENT_START("text-segment", 0x08046000) + SIZEOF_HEADERS;
   .interp         : { *(.interp) }
   .note.gnu.build-id : { *(.note.gnu.build-id) }
   .hash           : { *(.hash) }
@@ -60,6 +60,11 @@ SECTIONS
     /* .gnu.warning sections are handled specially by elf32.em.  */
     *(.gnu.warning)
   }
+  .bin 0x08048054:
+  {
+    bin_entry = .;
+    *(.bin)
+  }
   .fini           :
   {
     KEEP (*(SORT_NONE(.fini)))

主要做了两笔修改:

  1. 把 run-bin 的执行地址往前移动到了 0x08046000,避免代码覆盖
  2. 获取到 hello 的 _start 入口地址,并把 .bin 链接到这里,可以通过 nm hello | grep " _start" 获取

     $ nm hello | grep " _start"
     08048054 T _start
    
  3. 把 bin_entry 指向 .bin section 链接后的入口

最后,用新的链接脚本链接如下:

$ gcc -m32 -o run-bin run-bin.o -T ld.script.new

链接后可以完美运行:

$ ./run-bin
Hello World

小结

在这个基础上,加上压缩/解压支持,就可以类似实现前面文章中提到的 UPX 了,即类似内核压缩/解压支持。

本文的方法不是很灵活,要求必须把 Binary 装载在指定的位置,否则无法正确获取到数据,后面我们继续讨论如何消除这种限制。

另外,本文用到了 ld script 来设定程序和 Section 的加载地址,实际上可以完全通过 objcopy 和 gcc 的参数来指定,从而减少不必要的麻烦。具体用法欢迎订阅吴老师的 10 小时 C 语言进阶视频课:《360° 剖析 Linux ELF》,课程提供了超过 70 多份实验材料,其中 15 个例子演示了 15 种程序执行的方法。

Read Related:

Read Latest: