浅析ELF转二进制允许把 Binary 文件加载到任意位置
背景简介
有一天,某位同学在讨论群聊起来:
除了直接把C语言程序编译成ELF运行以外,是否可以转成二进制,然后通过第三方程序加载到内存后再运行。
带着这样的问题,我们写了四篇文章,这是其二。
上篇介绍了如何把ELF文件转成二进制文件,并作为一个新的Section加入到另外一个程序中执行。
这个代码包括两个段,一个text段,一个data段,默认链接完以后,text中是通过绝对地址访问data的,ELF转成Binary后,这个地址也写死在ELF中,如果要作为新的Seciton加入到另外一个程序,那么链接时必须确保Binary文件的加载地址跟之前的ELF加载地址一致,否则数据存放的位置就偏移了,访问不到,所以上篇文章用了一个客制化的ldscript,在里头把BinarySeciton的加载地址(运行时地址)写死的。
让数据地址与加载地址无关
本篇来讨论一个有意思的话题,那就是,是否可以把这个绝对地址给去掉,只要把这个Binary插入到新程序的Text中,不关心加载地址,也能运行?
想法是这样:data应该跟text关联起来,也就是说,用相对.text的地址,因为Binary里头的.rodata是跟在.text后面,在文件中的相对位置其实是固定的,是否可以在运行时用一个偏移来访问呢?也就是在运行过程中,获取到.text中的某个位置,然后通过距离来访问这个数据?
在运行时获取eip
由于加载地址是任意的,用.text中的符号也不行,因为在链接时也一样是写死的(用动态链接又把问题复杂度提升了),所以,唯一可能的办法是eip,即程序地址计数器。
但是eip是没有办法直接通过寄存器获取的,得通过一定技巧来,下面这个函数就可以:
eip2ecx: movl(%esp),%ecx ret
这个函数能够把eip放到ecx中。
原理很简单,那就是调用它的call指令会把nexteip放到stack,并跳到eip2ecx。所以stack顶部就是eip。这里也可以直接用pop%ecx。
所以这条指令能够拿到.here的地址,并且存放在ecx中:
calleip2ecx .here: ... .section.rodata .LC0: .string"HelloWorld\xa\x0"
通过eip与数据偏移计算数据地址
然后接下来,由于汇编器能够算出.here离.LC0(数据段起始位置):.LC0-.here,对汇编器而言,这个差值就是一个立即数。如果在ecx上加上(addl)这个差值,是不是就是数据在运行时的位置?
我们在.here放上下面这条指令:
calleip2ecx .here: addl$(.LC0-.here),%ecx ... .section.rodata .LC0: .string"HelloWorld\xa\x0"
同样能够拿到数据的地址,等同于:
movl$.LC0,%ecx#ecx=$.LC0,theaddrofstring
下面几个综合一起回顾:
- addl这条指令的位置正好是运行时的nexteip(call指令的下一条)
- .here在汇编时确定,指向nexteip
- .LC0也是汇编时确定,指向数据开始位置
- .LC0-.here刚好是addl这条指令跟数据段的距离/差值
- calleip2ecx返回以后,ecx中存了eip
- addl这条指令把ecx加上差值,刚好让ecx指向了数据在内存中的位置
完整代码如下:
#hello.s # #as--32-ohello.ohello.s #ld-melf_i386-ohellohello.o #objcopy-Obinaryhellohello.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,standardoutput calleip2ecx .here: addl$(.LC0-.here),%ecx#ecx=$.LC0,theaddrofstring #equalsto:movl$.LC0,%ecx xorl%edx,%edx movb$13,%dl#edx=13,thelengthof.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"HelloWorld\xa\x0"
链接脚本简化
这个生成的hello.bin链接到run-bin,就不需要写死加载地址了,随便放,而且不需要调整run-bin本身的加载地址,所以ld.script的改动可以非常简单:
$gitdiffld.scriptld.script.new diff--gita/ld.scriptb/ld.script.new index91f8c5c..e14b586100644 ---a/ld.script +++b/ld.script.new @@-60,6+60,11@@SECTIONS /*.gnu.warningsectionsarehandledspeciallybyelf32.em.*/ *(.gnu.warning) } +.bin: +{ +bin_entry=.; +*(.bin) +} .fini: { KEEP(*(SORT_NONE(.fini)))
直接用内联汇编嵌入二进制文件
在这个基础上,可以做一个简化,直接用.pushsection和.incbin指令把hello.bin插入到run-bin即可,无需额外修改链接脚本:
$catrun-bin.c #includeasm(".pushsection.text,\"ax\"\n" ".globlbin_entry\n" "bin_entry:\n" ".incbin\"./hello.bin\"\n" ".popsection" ); externvoidbin_entry(void); intmain(intargc,char*argv[]) { bin_entry(); return0; }
这个内联汇编的效果跟上面的链接脚本完全等价。
把数据直接嵌入代码中
进一步简化汇编代码把eip2ecx函数去掉:
#hello.s # #as--32-ohello.ohello.s #ld-melf_i386-ohellohello.o #objcopy-Obinaryhellohello.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,standardoutput calleip2ecx eip2ecx: pop%ecx addl$(.LC0-eip2ecx),%ecx#ecx=$.LC0,theaddrofstring #equalsto:movl$.LC0,%ecx xorl%edx,%edx movb$13,%dl#edx=13,thelengthof.string int$0x80 xorl%eax,%eax movl%eax,%ebx#ebx=0 incl%eax#eax=1,sys_exit int$0x80 .LC0: .string"HelloWorld\xa\x0"
再进一步,直接把数据搬到nexteip所在位置:
#hello.s # #as--32-ohello.ohello.s #ld-melf_i386-ohellohello.o #objcopy-Obinaryhello.ohello # .text .global_start _start: xorl%eax,%eax movb$4,%al#eax=4,sys_write(fd,addr,len) xorl%ebx,%ebx incl%ebx#ebx=1,standardoutput callnext#pusheip;jmpnext .LC0: .string"HelloWorld\xa\x0" next: pop%ecx#ecx=$.LC0,theaddrofstring #eipisjusttheaddrofstring,`call`helpedus xorl%edx,%edx movb$13,%dl#edx=13,thelengthof.string int$0x80 xorl%eax,%eax movl%eax,%ebx#ebx=0 incl%eax#eax=1,sys_exit int$0x80
小结
本文通过eip+偏移地址实现了运行时计算数据地址,不再需要把Binary文件装载到固定的位置。
另外,也讨论到了如何用.pushsection/.popsection替代ldscript来添加新的Section,还讨论了如何把数据直接嵌入到代码中。
以上所述是小编给大家介绍的ELF转二进制允许把Binary文件加载到任意位置,希望对大家有所帮助!