如何将目标文件“链接”到可执行/编译的二进制文件?

13

问题

我希望将一个目标文件注入到现有的二进制文件中。举个具体的例子,考虑一个源码文件 Hello.c

#include <stdlib.h>

int main(void)
{
    return EXIT_SUCCESS;
}

这段代码可以通过 gcc -std=gnu99 -Wall Hello.c -o Hello 编译成一个可执行文件,文件名为 Hello。此外,现在考虑一下 Embed.c

func1(void)
{
}

一个目标文件Embed.o可以通过gcc -c Embed.c创建。我的问题是如何将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),因此这个问题肯定是可以解决的。


快速阅读问题后,会留下一个感觉,即运行时链接器和普通链接器之间的概念并未被理解。运行时链接器/程序加载器仅对易于快速修复的格式进行操作。.o不是其中之一 :-) 如果它具有最小的依赖关系,例如编解码器,则使用最少的代码进行链接,使其成为.so似乎是逻辑路线。 - Marco van de Voort
@MarcovandeVoort:谢谢您的评论 :) 我使用“链接”这个术语比较宽泛,就像一个人可能会使用“注入”,这就是为什么我将其放在引号中的原因之一。 我不能将它制作为.so文件的原因之一是,诸如LD_PRELOAD之类的注入技巧可能会被应用程序破解。不仅如此,它还需要分发一个额外的库,以形成新环境。静态拦截具有各种其他优点(特别是对于这个项目的目的),但正如我在问题和回答评论中已经说过的那样,这不是我可以更改的设计决策 :) - Mike Kwan
你是否正在尝试做类似于AIX上的ld能力(我不知道其他地方是否有)来重新链接一个可执行文件,只有一个目标文件已更改的事情? - evil otto
@evilotto:我想添加一个以前从未存在过的新对象文件。 - Mike Kwan
你介意简要地分享一下 Rationale 下的 #2 是如何可能的吗?如果你现在知道 OP 的答案,我也非常好奇。 - Praxeolitic
显示剩余3条评论
6个回答

7

如果是我,我会将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!
}

是的,这是实现Detouring的一种替代方法。关于如何实现修补程序的问题,可以使用__attribute__((constructor)) GCC属性来完成,该属性允许在库被加载时调用方法。可执行文件也可以被欺骗以认为共享对象是一个依赖项。这是现有工具LEEL所采用的方法。 - Mike Kwan
很遗憾,运行时/动态重定向不会是一个可接受的解决方案。这是项目开始时明确声明的要求。 - Mike Kwan

4
假设第一个可执行文件的源代码可用,并且已经编译并链接了后续对象文件所需的空间,那么有一个相对简单的解决方案。由于我目前正在开发 ARM 项目,以下示例是使用 GNU ARM 交叉编译器编译的。
主要的源代码文件为 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

我收到了“hello: unsupported ELF file type 2”错误信息...(使用arm-oe-linux-gnueabi/4.9.2编译) - IvanDi
1
你尝试过使用arm-none-eabi-*工具吗?像https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads这样的工具链。 - fsheikh
1
抱歉这么不要脸地问,但如果您觉得答案对您有用的话,能否也给它点个赞呢? :D - fsheikh
当然。它很有用和富有教育性,尽管在我的情况下它无法工作(针对oelinux目标的gcc交叉编译器)。 - IvanDi
我测试了这个解决方案,它导致程序崩溃。没有执行,没有gdb...总是出现分段错误。 - husin alhaj ahmade

2
问题在于.o文件尚未完全链接,大多数引用仍然是符号引用。二进制文件(共享库和可执行文件)更接近于最终链接的代码。
将链接步骤应用于共享库,并不意味着您必须通过动态库加载器来加载它。建议更多的是为二进制文件或共享库提供自己的加载器,这可能比.o文件更简单。
另一种可能性是自定义链接过程并调用链接器将其链接到某个固定地址以进行加载。您还可以查看例如引导加载程序的准备过程,其中还涉及基本链接步骤以恰好达到此目的(将代码片段固定到已知加载地址)。
如果不链接到固定地址,并且要在运行时重定位,则必须编写一个基本链接器,该链接器接受对象文件并通过执行适当的修补来将其重定位到目标地址。
我假设您已经掌握了这些技术,因为这是您的硕士论文,但是这本书:http://www.iecc.com/linker/ 是关于这方面的标准介绍。

我实际上也考虑过定制链接过程,这也是我在这里提出问题的原因:https://dev59.com/cGHVa4cB1Zd3GeqPiwZO。如果我能够将某些部分链接到特定地址,我认为我就可以使用`libbfd`将它们复制到可执行文件中。您是否知道任何工具或链接选项可以允许您建议的内容(将部分链接 - 而不是符号 - 到固定地址)? - Mike Kwan
如在其他问题中已经提到的那样:链接器资源文件是正确的选择。 - Marco van de Voort

1

为了让可重定位代码适合于可执行文件中,您必须扩展可执行文件的文本段,就像病毒感染一样。然后,在将可重定位代码写入该空间后,通过为可重定位对象中的任何内容添加符号来更新符号表,然后应用必要的重定位计算。我已经编写了针对32位ELF文件的很好的代码来完成这个任务。


欢迎来到 Stack Overflow。请展示一些你编写的代码来解决这个问题 - 告诉我们你有它是很好,但现在并没有帮助。 - michaelb958--GoFundMonica

0
你有没有看过DyninstAPI?最近似乎添加了将.o文件链接到静态可执行文件的支持。
从发布网站上可以看到:

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


谢谢提供这个链接。我之前见过Dyninst,但不知道它也可以进行静态二进制重写。我会看一下并稍后更新。 - Mike Kwan

0

你无法以任何实际的方式完成这个任务。预期的解决方案是将该对象制作成共享库,然后调用dlopen。


谢谢你的答复。请查看我对Dan Fego的评论。具体来说,这是一个我无法更改的要求。我不确定它不能以“实用的方式”完成,因为现有的EEL工具可以做到这一点。 - Mike Kwan
我不知道是哪个疯子定义了你的需求,但坚持使用.o文件而不是包含它的.so文件符合我的“疯子”定义。我的“实用”定义是“具有适当程度的努力”。如果你的管理层想让你花费大量时间来实现这一点,我很同情你。 - bmargulies
我很同情你。你的教授似乎在将有趣的研究问题与无聊的基础设施区分开方面存在问题。 - bmargulies

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接