c语言 - 如何将目标文件'link'为可执行/编译的二进制文件?



问题

我希望将一个对象文件注入到现有的二进制文件中。作为一个具体的例子,考虑一个源Hello.c:

#include <stdlib.h>
int main(void)
{
    return EXIT_SUCCESS;
}

它可以编译为名为Hellogcc -std=gnu99 -Wall Hello.c -o Hello的可执行文件。此外,现在考虑Embed.c:

func1(void)
{
}

可以通过gcc -c Embed.c从中创建对象文件Embed.o。我的问题是,如何将Embed.o一般插入Hello,以便执行必要的重新定位,并正确修补适当的ELF内部表(如符号表、PLT等)?


假设

可以假设要嵌入的对象文件已经静态链接了其依赖项。任何动态依赖关系,例如C运行时,都可以假设也存在于目标可执行文件中。


当前尝试/想法

  • 使用libbfd将节从对象文件复制到二进制文件中。我在这方面取得的进展是,我可以用原始二进制文件的部分和对象文件的部分创建一个新对象。问题是,由于对象文件是可重定位的,如果不先执行重定位,就无法将其部分正确复制到输出中
  • 将二进制文件转换回对象文件,并使用ld重新链接。到目前为止,我尝试使用objcopy来执行转换objcopy --input elf64-x86-64 --output elf64-x86-64 Hello Hello.o。显然,这并没有像我想要的那样起作用,因为ld -o Hello2 Embed.o Hello.o将导致ld: error: Hello.o: unsupported ELF file type 2。我想这应该是意料之中的事,因为Hello不是一个对象文件
  • 找到执行这种插入的现有工具吗

基本原理(可选读取)

我正在制作一个静态可执行编辑器,其中的愿景是允许将任意用户定义的例程插入到现有的二进制文件中。这将分两个步骤进行:

  1. 将对象文件(包含用户定义的例程)注入二进制文件这是一个强制性步骤,不能通过注入共享对象等替代方法来解决
  2. 对新的二进制执行静态分析,并使用它将例程从原始代码静态绕行到新添加的代码

在大多数情况下,我已经完成了步骤2所需的工作,但在注入对象文件时遇到了问题。考虑到其他工具使用相同的对象注入方法(例如EEL),这个问题肯定是可以解决的。

如果是我,我会将Embed.c创建为一个共享对象libembed.so,如下所示:

gcc -Wall -shared -fPIC -o libembed.so Embed.c

这应该从Embed.c创建一个可重定位的共享对象。这样,您就可以通过在运行时设置环境变量LD_PRELOAD来强制目标二进制文件加载这个共享对象(请参阅此处的更多信息):

LD_PRELOAD=/path/to/libembed.so Hello

这里的"诀窍"是弄清楚如何进行插入,尤其是考虑到它是一个静态可执行文件。我帮不了你,但这是在进程内存空间中显示代码的一种方法。您可能想要在构造函数中进行某种初始化,这可以通过一个属性来完成(如果您至少使用gcc):

void __attribute__ ((constructor)) my_init()
{
    // put code here!
}

假设第一个可执行文件的源代码可用,并且使用为以后的对象文件分配空间的链接器脚本进行编译,则有一个相对简单的解决方案。由于我目前正在进行一个ARM项目,下面的例子是用GNUARM交叉编译器编译的。

主要源代码文件,hello.c

#include <stdio.h>
int main ()
{
   return 0;
}

是用一个简单的链接器脚本构建的,该脚本为稍后要嵌入的对象分配空间:

SECTIONS
{
    .text :
    {
        KEEP (*(embed)) ;
        *(.text .text*) ;
    }
}

类似:

arm-none-eabi-gcc -nostartfiles -Ttest.ld -o hello hello.c
readelf -s hello
Num:    Value  Size Type    Bind   Vis      Ndx Name
 0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
 1: 00000000     0 SECTION LOCAL  DEFAULT    1 
 2: 00000000     0 SECTION LOCAL  DEFAULT    2 
 3: 00000000     0 SECTION LOCAL  DEFAULT    3 
 4: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
 5: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 $a
 6: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
 7: 00000000    28 FUNC    GLOBAL DEFAULT    1 main

现在让我们编译要嵌入的对象,它的源代码在embed.c 中

void func1()
{
   /* Something useful here */
}

使用相同的链接器脚本重新编译,这次插入新符号:

arm-none-eabi-gcc -c embed.c
arm-none-eabi-gcc -nostartfiles -Ttest.ld -o new_hello hello embed.o

查看结果:

readelf -s new_hello
Num:    Value  Size Type    Bind   Vis      Ndx Name
 0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
 1: 00000000     0 SECTION LOCAL  DEFAULT    1 
 2: 00000000     0 SECTION LOCAL  DEFAULT    2 
 3: 00000000     0 SECTION LOCAL  DEFAULT    3 
 4: 00000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
 5: 00000000     0 NOTYPE  LOCAL  DEFAULT    1 $a
 6: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
 7: 00000000     0 FILE    LOCAL  DEFAULT  ABS embed.c
 8: 0000001c     0 NOTYPE  LOCAL  DEFAULT    1 $a
 9: 00000000     0 FILE    LOCAL  DEFAULT  ABS 
10: 0000001c    20 FUNC    GLOBAL DEFAULT    1 func1
11: 00000000    28 FUNC    GLOBAL DEFAULT    1 main

问题是.o还没有完全链接,大多数引用仍然是符号的。二进制文件(共享库和可执行文件)离最终链接的代码又近了一步。

对共享库执行链接步骤并不意味着必须通过动态库加载器加载它。建议更多的是,二进制或共享库的自己的加载程序可能比.o.更简单

另一种可能性是自己自定义链接过程,并调用链接器并将其链接到某个固定地址。您还可以查看例如bootloader的准备工作,它还包括一个基本的链接步骤来完成这一工作(将一段代码固定到已知的加载地址)。

如果您没有链接到固定地址,并且希望在运行时重新定位,则必须编写一个基本的链接器,该链接器获取对象文件,并通过执行适当的修复将其重新定位到目标地址。

我想你已经有了,因为这是你的硕士论文,但这本书:http://www.iecc.com/linker/是关于这方面的标准介绍。

您必须像病毒感染一样,通过扩展可执行文件文本段,为可重新定位的代码腾出空间,使其适合可执行文件。然后,在将可重定位代码写入该空间后,通过为该可重定位对象中的任何对象添加符号来更新符号表,然后应用必要的重定位计算。我已经用32位ELF编写了非常好的代码。

您无法以任何实际的方式做到这一点。预期的解决方案是将该对象创建为一个共享库,然后对其调用dlopen。

您看过DyninstAPI吗?最近似乎增加了对将.o链接到静态可执行文件的支持。

来自发布网站:

二进制重写器支持x86和x86_64平台上的静态链接二进制文件