![c31883a75d75cac0f3fc759f6e298c59.png](https://i-blog.csdnimg.cn/blog_migrate/86f5d96c1aaa8ac7ed354d9bb70152b1.jpeg)
注:继前段时间连载多篇 ELF 相关文章后,今次再连载 4 篇,每周 1 篇,欢迎关注并分享。分享本文到朋友圈后再加微信 tinylab 可以申请整个系列的 PDF 合集(共 15 篇,126 页)。
Linux ELF 系列文章合集(持续连载中):
- 吴章金falcon:上手9套工具,玩转二进制文件
- 吴章金falcon:为 Linux a.out 举行一个特殊的告别仪式
- 吴章金falcon:在 498 行极小 OS 上跑标准 ELF 程序
- 吴章金falcon:如何创建一个可执行的 Linux 共享库
- 吴章金falcon:深度剖析 Linux 共享库的“位置无关”实现原理
- 吴章金falcon:通过操作 Section 为 Linux ELF 程序新增数据
- 吴章金falcon:实例解析 Linux C 语言程序之变量类型
- 吴章金falcon:《360°剖析Linux ELF》视频课程新增 15 份实验材料,累计已超 70 份
本文首发于 ELF转二进制: 用 objcopy 把 ELF 转成 Binary 并运行
背景简介
有一天,某位同学在讨论群聊起来:
除了直接把 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 Worldxax0"
这段代码编译、运行后可以打印 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 开始的位置。
如果这个要在用户空间做呢?
看上去没那么容易,不过可以这么做:
- 把 hello.bin 作为一个 Section 加入到目标执行代码中,比如叫 run-bin.c
- 然后写 ld script 明确把 hello.bin 放到 Load Address 地址上
- 同时需要修改 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)))
主要做了两笔修改:
- 把 run-bin 的执行地址往前移动到了 0x08046000,避免代码覆盖
- 获取到 hello 的 _start 入口地址,并把 .bin 链接到这里,可以通过
nm hello | grep " _start"
获取
$ nm hello | grep " _start" 08048054 T _start - 把 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 种程序执行的方法。
相关课程
课程详情
![36a8e49d311dd4abc341e1d3ed03d0c9.png](https://i-blog.csdnimg.cn/blog_migrate/30904343f9eb32ed6ebb75b3fcea8ffd.jpeg)
订阅课程
- PC 报名:https://yomocode.com/courses/9
- 手机报名:https://w.url.cn/s/AMcKZ3a
- 其他合作需求请联系微信:tinylab