浅析ELF转二进制允许把 Binary 文件加载到任意位置

背景简介

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

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

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

上篇 介绍了如何把 ELF 文件转成二进制文件,并作为一个新的 Section 加入到另外一个程序中执行。

这个代码包括两个段,一个 text 段,一个 data 段,默认链接完以后,text 中是通过绝对地址访问 data 的,ELF 转成 Binary 后,这个地址也写死在 ELF 中,如果要作为新的 Seciton 加入到另外一个程序,那么链接时必须确保 Binary 文件的加载地址跟之前的 ELF 加载地址一致,否则数据存放的位置就偏移了,访问不到,所以上篇文章用了一个客制化的 ld script,在里头把 Binary Seciton 的加载地址(运行时地址)写死的。

让数据地址与加载地址无关

本篇来讨论一个有意思的话题,那就是,是否可以把这个绝对地址给去掉,只要把这个 Binary 插入到新程序的 Text 中,不关心加载地址,也能运行?

想法是这样:data 应该跟 text 关联起来,也就是说,用相对 .text 的地址,因为 Binary 里头的 .rodata 是跟在 .text 后面,在文件中的相对位置其实是固定的,是否可以在运行时用一个偏移来访问呢?也就是在运行过程中,获取到 .text 中的某个位置,然后通过距离来访问这个数据?

在运行时获取 eip

由于加载地址是任意的,用 .text 中的符号也不行,因为在链接时也一样是写死的(用动态链接又把问题复杂度提升了),所以,唯一可能的办法是 eip,即程序地址计数器。

但是 eip 是没有办法直接通过寄存器获取的,得通过一定技巧来,下面这个函数就可以:

eip2ecx:
 movl (%esp), %ecx
 ret

这个函数能够把 eip 放到 ecx 中。

原理很简单,那就是调用它的 call 指令会把 next eip 放到 stack,并跳到 eip2ecx。所以 stack 顶部就是 eip。这里也可以直接用 pop %ecx 。

所以这条指令能够拿到 .here 的地址,并且存放在 ecx 中:

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

通过 eip 与数据偏移计算数据地址

然后接下来,由于汇编器能够算出 .here 离 .LC0(数据段起始位置): .LC0 - .here ,对汇编器而言,这个差值就是一个立即数。如果在 ecx 上加上(addl)这个差值,是不是就是数据在运行时的位置?

我们在 .here 放上下面这条指令:

call eip2ecx
.here:
 addl $(.LC0 - .here), %ecx
 ...
 .section .rodata
.LC0:
 .string "Hello World\xa\x0"

同样能够拿到数据的地址,等同于:

movl $.LC0, %ecx    # ecx = $.LC0, the addr of string

下面几个综合一起回顾:

  • addl 这条指令的位置正好是运行时的 next eip (call 指令的下一条)
  • .here 在汇编时确定,指向 next eip
  • .LC0 也是汇编时确定,指向数据开始位置
  • .LC0 - .here 刚好是 addl 这条指令跟数据段的距离/差值
  • call eip2ecx 返回以后,ecx 中存了 eip
  • addl 这条指令把 ecx 加上差值,刚好让 ecx 指向了数据在内存中的位置

完整代码如下:

# 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

 call eip2ecx
.here:
 addl $(.LC0 - .here), %ecx # ecx = $.LC0, the addr of string
         # equals to: movl $.LC0, %ecx

 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

eip2ecx:
 movl (%esp), %ecx
 ret

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

链接脚本简化

这个生成的 hello.bin 链接到 run-bin,就不需要写死加载地址了,随便放,而且不需要调整 run-bin 本身的加载地址,所以 ld.script 的改动可以非常简单:

$ git diff ld.script ld.script.new
diff --git a/ld.script b/ld.script.new
index 91f8c5c..e14b586 100644
--- a/ld.script
+++ b/ld.script.new
@@ -60,6 +60,11 @@ SECTIONS
  /* .gnu.warning sections are handled specially by elf32.em. */
  *(.gnu.warning)
 }
+ .bin   :
+ {
+ bin_entry = .;
+ *(.bin)
+ }
 .fini   :
 {
  KEEP (*(SORT_NONE(.fini)))

直接用内联汇编嵌入二进制文件

在这个基础上,可以做一个简化,直接用 .pushsection 和 .incbin 指令把 hello.bin 插入到 run-bin 即可,无需额外修改链接脚本:

$ cat run-bin.c
#include <stdio.h>

asm (".pushsection .text, \"ax\" \n"
  ".globl bin_entry \n"
  "bin_entry: \n"
  ".incbin \"./hello.bin\" \n"
  ".popsection"
);

extern void bin_entry(void);

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

这个内联汇编的效果跟上面的链接脚本完全等价。

把数据直接嵌入代码中

进一步简化汇编代码把 eip2ecx 函数去掉:

# 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

 call eip2ecx
eip2ecx:
 pop %ecx
 addl $(.LC0 - eip2ecx), %ecx # ecx = $.LC0, the addr of string
         # equals to: movl $.LC0, %ecx

 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

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

再进一步,直接把数据搬到 next eip 所在位置:

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

 .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
 call next      # push eip; jmp next
.LC0:
 .string "Hello World\xa\x0"
next:
 pop %ecx      # ecx = $.LC0, the addr of string
         # eip is just the addr of string, `call` helped us
 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

小结

本文通过 eip + 偏移地址 实现了运行时计算数据地址,不再需要把 Binary 文件装载到固定的位置。

另外,也讨论到了如何用 .pushsection/.popsection 替代 ld script 来添加新的 Section,还讨论了如何把数据直接嵌入到代码中。

以上所述是小编给大家介绍的ELF转二进制允许把 Binary 文件加载到任意位置,希望对大家有所帮助!

(0)

相关推荐

  • MySQL中Binary Log二进制日志文件的基本操作命令小结

    MySQL Binary Log也就是常说的bin-log, ,是mysql执行改动产生的二进制日志文件,其主要作用有两个: * 数据回复 * 主从数据库.用于slave端执行增删改,保持与master同步. 1.开启binary log功能 需要修改mysql的配置文件,本篇的实验环境是win7,配置文件为mysql安装目录\MySQL Server 5.1下的my.ini,添加一句log_bin = mysql_bin即可 eg: [mysqld] ...... log_bin = mysq

  • Python中的self用法详解

    在Python类中规定,函数的第一个参数是实例对象本身,并且约定俗成,把其名字写为self.其作用相当于java中的this,表示当前类的对象,可以调用当前类中的属性和方法. class是面向对象的设计思想,instance(也即是 object,对象)是根据 class 创建的. 一个类(class)应该包含数据和操作数据的方法,通俗来讲就是属性和函数(即调用方法). 类 class 中为啥用使用 self ? 在类的代码(函数)中,需要访问当前的实例中的变量和函数,即访问Instance中的

  • MYSQL的binary解决mysql数据大小写敏感问题的方法

    复制代码 代码如下: mysql> select binary 'ABCD'='abcd' COM1, 'ABCD'='abcd' COM2;+--------+-----------+| COM1 | COM2 |+--------+-----------+|      0     |      1      |+---------+-----------+1 row in set (0.00 sec) (仅仅有些而已!4.*以前)因为有的MySQL特别是4.*以前的对于中文检索会有不准确的问

  • 关于mysql字符集设置了character_set_client=binary 在gbk情况下会出现表描述是乱码的情况

    mysql链接建立之后,通过如下方式设置编码: 复制代码 代码如下: mysql_query("SET character_set_connection=" . $GLOBALS['charset'] . ",character_set_results=" . $GLOBALS['charset'] . ",character_set_client=binary", $this->link); 然而建立出来的表结构描述竟然是乱码: 复制代码

  • 对Python中class和instance以及self的用法详解

    一. Python 的类和实例 在面向对象中,最重要的概念就是类(class)和实例(instance),类是抽象的模板,而实例是根据类创建出来的一个个具体的 "对象". 就好比,学生是个较为抽象的概念,同时拥有很多属性,可以用一个 Student 类来描述,类中可定义学生的分数.身高等属性,但是没有具体的数值.而实例是类创建的一个个具体的对象, 每一个对象都从类中继承有相同的方法,但是属性值可能不同,如创建一个实例叫 hansry 的学生,其分数为 93,身高为 176,则这个实例拥

  • 浅析ELF转二进制允许把 Binary 文件加载到任意位置

    背景简介 有一天,某位同学在讨论群聊起来: 除了直接把 C 语言程序编译成 ELF 运行以外,是否可以转成二进制,然后通过第三方程序加载到内存后再运行. 带着这样的问题,我们写了四篇文章,这是其二. 上篇 介绍了如何把 ELF 文件转成二进制文件,并作为一个新的 Section 加入到另外一个程序中执行. 这个代码包括两个段,一个 text 段,一个 data 段,默认链接完以后,text 中是通过绝对地址访问 data 的,ELF 转成 Binary 后,这个地址也写死在 ELF 中,如果要作

  • python:关于文件加载及处理方式

    目录 关于文件加载及处理 1.检查python 2.对文件夹下面的文件名称进行列表排列 3.过滤不符合要求的文件 4.用于将元组转换为列表 5.打开文件codeces,open() 6.readlines() 7.strip() python文件处理(总结) 1.txt文件 2.csv文件操作 关于文件加载及处理 1.检查python 关于文件加载及处理方式文件路径是否存在,如果不存在就创建此路径. #如果不存在路径,就创建一个这样的路径     if not os.path.exists(ex

  • 学习javascript文件加载优化

    在js引擎部分,我们可以了解到,当渲染引擎解析到script标签时,会将控制权给JS引擎,如果script加载的是外部资源,则需要等待下载完后才能执行. 所以,在这里,我们可以对其进行很多优化工作. 放置在BODY底部 为了让渲染引擎能够及早的将DOM树给渲染出来,我们需要将script放在body的底部,让页面尽早脱离白屏的现象,即会提早触发DOMContentLoaded事件. 但是由于在IOS Safari, Android browser以及IOS webview里面即使你把js脚本放到

  • IE及IE6浏览器中判断JS文件加载成功失败的方法

    浏览器的文件加载实际上是有非常纠结的兼容问题的.最近看到@lifesinger做了一个具体的总结.这里比较麻烦的是IE6~8不区分加载成功或失败,都走一个回调.在网上看了一种解决方案是,在加载文件的最后置一个全局变量或改变标签的属性来区分,这样成功与否就通过这个标志位判断.但显然不太完美,还要改加载文件. 后来尝试另一种思路,先创建一个vbscript,src置成一个JS文件,如这个文件加载正常,肯定会报错否则不会有反应.这样如果window.onerror捕获到错误了,说明文件有效,再正常加载

  • javascript文件加载管理简单实现方法

    本文实例讲述了javascript文件加载管理简单实现方法.分享给大家供大家参考.具体如下: 这里介绍超级简单的进行javascript的文件(模块)的加载管理, 实现sea.js或者require.js核心功能,用一行 <script src="js/light/light.js"></script> 加载所有的js文件. //按照lightJs的顺序,让网页加载js文件: var lightJs = [ "./js/light/pre/jquery

  • 详解js异步文件加载器

    我们经常会遇到这种场景,某些页面依赖第三方的插件,而这些插件比较大,不适合打包到页面的主js里(假设我们使用的是cmd的方式,js会打包成一个文件),那么这个时候我们通常会异步获取这些插件文件,并在下载完成后完成初始化的逻辑. 以图片上传为例,我们可能会用到plupload.js这个插件,那么我们会这么写: !window.plupload ? $.getScript( "/assets/plupload/plupload.full.min.js", function() { self

  • Mysql5.7在windows7下my.ini文件加载路径及数据位置修改方法

    更新:现在上MySQL官网装个mysql installer统一对mysql软件管理配置,迁移数据也很方面.进mysql installer里面对mysql server进行reconfigure,就有数据库存储位置的改变.比下面老式的手动迁移要稳当(手动迁移后容易发生数据库启动不起来或者不能正常关闭的情况).高版本的mysql installer,在安装时自定义安装路径隐藏的很深,请参考http://www.jb51.net/article/82493.htm. 因为要将公司线上数据库传输到本

  • webpack打包并将文件加载到指定的位置方法

    使用webpack打包,最爽的事情莫过于可以直接require文件了,但是这 同时带来了一个问题,就是所有的文件整合到一起,那这一个包就太大了. 基于此:下面我们来了解下webpack的打包(主要是将如何将我们需要的内容模块,分开打包, 并且按照我们自己设定的存放路径进行存放) 首先在webpack.config.js文件中 entry入口函数出表示出哪些是需要单独打包成一个js包的: entry: { main: path.resolve(__dirname,'src/index.js'),

  • 基于js文件加载优化(详解)

    在js引擎部分,我们可以了解到,当渲染引擎解析到script标签时,会将控制权给JS引擎,如果script加载的是外部资源,则需要等待下载完后才能执行. 所以,在这里,我们可以对其进行很多优化工作. 放置在BODY底部 为了让渲染引擎能够及早的将DOM树给渲染出来,我们需要将script放在body的底部,让页面尽早脱离白屏的现象,即会提早触发DOMContentLoaded事件. 但是由于在IOS Safari, Android browser以及IOS webview里面即使你把js脚本放到

  • Django框架模板文件使用及模板文件加载顺序分析

    本文实例讲述了Django框架模板文件使用及模板文件加载顺序.分享给大家供大家参考,具体如下: 模板功能 产生html,控制页面上产生的内容.模板文件不仅仅是一个html文件. 模板文件包含两部分内容: 1.静态文件:css,js,html 2.动态内容:用于动态的去产生一些网页内容,通过模板语言产生 模板文件的使用 通常是在视图函数中使用模板产生html内容返回给客户端 a,加载模板文件 loader.get_template 获取模板文件的内容,产生一个模板对象 b,定义模板上下文 Requ

随机推荐