![48bae7a549b3d78ca06d9b32daf089bf.png](https://i-blog.csdnimg.cn/blog_migrate/0ad24c06c96b48581b50c3743f0d9a1b.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 份
- 吴章金falcon:ELF 转二进制(1/4): 用 objcopy 把 ELF 转成 Binary 并运行
- 吴章金falcon:ELF 转二进制(2/4): 允许把 Binary 文件加载到任意位置
本文首发于 ELF转二进制:动态加载和运行
背景简介
有一天,某位同学在讨论群聊起来:
除了直接把 C 语言程序编译成 ELF 运行以外,是否可以转成二进制,然后通过第三方程序加载到内存后再运行。
带着这样的问题,我们写了四篇文章,这是其三。
前面两篇分别讨论了如何把一个转成 Binary 的 ELF 作为一个新的 Section 加入到另外一个程序中执行:
- ELF转二进制(1/4): 用 objcopy 把 ELF 转成 Binary 并运行
- ELF转二进制(2/4):允许把 Binary 文件加载到任意位置
如何实现动态加载和运行
本文继续讨论,但是方向是,在运行时加载 Binary 并运行,支持前面两篇中的两种类型的 Binary:绝对数据地址、相对数据地址。
先加载数据位置无关的 Binary 文件
先来考虑最简单的相对数据地址,动态加载后,仅需考虑把加载的 Binary 所在内存范围设置可执行即可。
把文件加载到内存中并设置内存保护属性的最佳方式是 mmap,当然也可以用 malloc/memalgin 分配内存然后用 mprotect 设置内存保护属性,但是需要额外考虑对齐。
可以直接基于 man mmap
中的例子小改一番,先拿到这个原始的例子:
$ man mmap | sed -ne "/Program source/,/SEE ALSO/p" | egrep -v "Program|SEE" | sed -e "s/^ //g" > mmap.orig.c
做完如下修改,得到一个 mmap.new.c:
$ git diff mmap.orig.c mmap.new.c
diff --git a/mmap.orig.c b/mmap.new.c
index 640bcb0..fe039c8 100644
--- a/mmap.orig.c
+++ b/mmap.new.c
@@ -49,11 +49,12 @@ main(int argc, char *argv[])
length = sb.st_size - offset;
}
- addr = mmap(NULL, length + offset - pa_offset, PROT_READ,
+ addr = mmap(NULL, length + offset - pa_offset, PROT_READ|PROT_EXEC,
MAP_PRIVATE, fd, pa_offset);
if (addr == MAP_FAILED)
handle_error("mmap");
+#if 0
s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
if (s != length) {
if (s == -1)
@@ -62,6 +63,9 @@ main(int argc, char *argv[])
fprintf(stderr, "partial write");
exit(EXIT_FAILURE);
}
+#else
+ ((void (*)(void))addr)();
+#endif
exit(EXIT_SUCCESS);
}
上面的改动很简单,一方面是调整内存保护属性为 MAP_EXEC,另外一方面是把映射完的随机地址转换为一个 void (*)void
函数,然后直接执行,也就是调用 Binary,同时把之前的打印到控制台的部分注释掉。
以 -m32 参数编译,确保可以跑 -m32 的代码(继承上面两节):
$ gcc -m32 -o mmap.new mmap.new.c
这个 mmap.new 即可运行第二篇得到的采用相对数据地址的 hello.bin:
$ ./mmap.new ./hello.bin 0
Hello World
再讨论数据位置固定的 Binary 文件
如果要带绝对地址的 hello.bin 呢?
由于数据加载地址是写死的,那意味着必须告知 mmap 映射到一个固定的地址,即 .text 的装载地址,否则数据访问就会出错。mmap 的第一个参数 addr 配合第三个参数 prot(设置为 MAP_FIXED),恰好可以做到。
只是,mmap 要求这个地址必须是对齐到页表的,这个 page size 可以通过 sysconf(_SC_PAGE_SIZE)
拿到。
可是,默认链接的时候,.text 段并不是对齐到 page size 的,对齐到 page size 的是 entry addr,还有一个 0x54 的偏移,即 elf header + program header。这个 0x54 末尾的地址不会是 page size 对齐的。
那意味者链接的时候得“做点手脚”,得强制让 .text 对齐到 page size,我们观察到 0x8046000 这个可以安全使用,因为程序都是从 0x8048000 之后的。当然,这里可用的只有不到 0x2000,8k,比这个大就把这个地址再改小吧。
怎么强制修改 .text 的装载地址呢,一个是上节提到的修改 ld.script,另外一个是直接用 ld 的 -Ttext 参数:
$ as --32 -o hello.o hello.s
$ ld -melf_i386 -o hello hello.o -Ttext=0x8046000
之后,我们再做一些修改,允许传递这个地址给 mmap,得到 mmap.any.c:
$ diff --git a/mmap.orig.c b/mmap.any.c
index 640bcb0..aa23eb2 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,16 @@ 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_EXEC,
+ MAP_PRIVATE|MAP_FIXED, fd, pa_offset);
if (addr == MAP_FAILED)
handle_error("mmap");
+#if 0
s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
if (s != length) {
if (s == -1)
@@ -62,6 +58,9 @@ main(int argc, char *argv[])
fprintf(stderr, "partial write");
exit(EXIT_FAILURE);
}
+#else
+ ((void (*)(void))addr)();
+#endif
exit(EXIT_SUCCESS);
}
这个改动把原来的参数 length 换掉,替换为 addr,允许直接通过第三个程序参数设置 .text 的装载地址(确保数据地址有效)。
重新编译 mmap.any.c 并运行:
$ gcc -m32 -o mmap.any mmap.any.c
$ ./mmap.any ./hello.bin 0 0x8046000
Hello World
需要注意的是,这个地址必须与 -Ttext 指定的地址一致。
小结
到这里,ELF转二进制 3 篇文章就完成了,分别讨论了静态嵌入、位置无关和动态加载,接下来还有一篇会讨论,如何动态修改数据地址。
欢迎订阅吴老师的 10 小时 C 语言进阶视频课:《360° 剖析 Linux ELF》,课程提供了超过 70 多份实验材料,其中 15 个例子演示了 15 种程序执行的方法。
相关课程
课程详情
![0c805be92a47e26dcdf2bc0abcc4d81e.png](https://i-blog.csdnimg.cn/blog_migrate/3bc4589080778fd13bc63abd2a37684c.jpeg)
订阅课程
- PC 报名:https://yomocode.com/courses/9
- 手机报名:https://w.url.cn/s/AMcKZ3a
- 其他合作需求请联系微信:tinylab