处理ELF重定位 - 了解重定位,符号,节数据以及它们如何协同工作

the*_*lix 18 reverse-engineering elf opcode relocation symbol-table

TL; DR

我试图将这个问题作为一个简短的问题,但这是一个复杂的问题所以它最终会变长.如果您可以回答这方面的任何部分或提供任何建议或提示或资源或任何内容,那将非常有帮助(即使您没有直接解决我的所有问题).我现在正撞在墙上.:)

以下是我遇到的具体问题.请阅读下面的更多信息.

  • 我正在寻找有关如何处理重定位条目和更新节数据中未解析的符号的指导.我根本不明白如何处理我从重定位和部分等中提取的所有信息.
  • 我也希望了解链接器遇到重定位时发生了什么.试图正确实现重定位方程并以正确的方式使用所有正确的值是非常具有挑战性的.
  • 当我遇到操作码,地址和符号等时,我需要了解如何处理它们.我觉得我错过了一些步骤.
  • 我觉得我没有很好地掌握符号表条目如何与重定位进行交互.我应该如何使用符号的绑定,可见性,值和大小信息?
  • 最后,当我使用已解析的数据和可执行文件使用的新重定位条目输出我的文件时,数据都是不正确的.我不确定如何遵循所有重新安置并提供所需的所有信息.可执行文件对我有什么期望?

到目前为止我的方法

我正在尝试以特定的[未记录的]专有格式创建一个重定位文件,该格式主要基于ELF.我编写了一个工具,它接受一个ELF文件和一个部分链接文件(PLF)并处理它们以输出完全解析的rel文件.此rel文件用于根据需要加载/卸载数据以节省内存.该平台是一个32位PPC.一个问题是该工具是用c#编写的,但数据是针对PPC的,因此需要注意有趣的endian问题等.

我一直试图了解在用于解析未解析的符号等时如何处理重定位.到目前为止我所做的是从PLF复制相关部分,然后对于每个相应的.rela部分,我解析条目并尝试修复部分数据并根据需要生成新的重定位条目.但这就是我的困难所在.我离开了我的元素,这种事情似乎通常是由链接器和加载器完成的,所以没有很多好的例子可以借鉴.但是我发现了一些有帮助的东西,包括这一个.

所以发生的事情是:

  1. 从PLF复制部分数据以用于rel文件.我只对.init(无数据),.text,.ctors,.dtors,.rodata,.data,.bss(无数据)和我们正在使用的另一个自定义部分感兴趣.
  2. 迭代PLF中的.rela部分并读入Elf32_Rela条目.
  3. 对于每个条目,我提取r_offset,r_info和r_addend字段,并从r_info(符号和reloc类型)中提取相关信息.
  4. 从PLF的符号表中,我可以得到symbolOffset,symbolSection和symbolValue.
  5. 从ELF,我得到symbolSection的加载地址.
  6. 我计算int localAddress =(.relaSection.Offset + r_offset).
  7. 我从r_offset的symbolSection的内容中获取了uint relocValue.
  8. 现在我拥有了我需要的所有信息,因此我对reloc类型进行了切换并处理数据.这些是我支持的类型:
    R_PPC_NONE
    R_PPC_ADDR32
    R_PPC_ADDR24
    R_PPC_ADDR16
    R_PPC_ADDR16_LO
    R_PPC_ADDR16_HI
    R_PPC_ADDR16_HA
    R_PPC_ADDR14
    R_PPC_ADDR14_BRTAKEN
    R_PPC_ADDR14_BRNTAKEN
    R_PPC_REL24
    R_PPC_REL14
    R_PPC_REL14_BRTAKEN
    R_PPC_REL14_BRNTAKEN
  9. 怎么办??我需要更新节数据并构建随播重定位条目.但我不明白该做什么以及如何做.

我这样做的全部原因是因为有一个旧的过时不支持的工具不支持使用自定义部分,这是该项目的关键要求(出于内存原因).我们有一个自定义部分,其中包含一堆我们想要在启动后卸载的初始化代码(总计大约一兆).现有工具只是忽略该部分中的所有数据.

因此,虽然制作支持自定义部分的自己的工具是理想的,但如果有任何明智的想法以实现这一目标的另一种方式,我全都耳朵!我们已经提出了将.dtor部分用于我们的数据的想法,因为它几乎是空的.但这很麻烦,如果它阻止干净关闭,可能无论如何都无法工作.


重定位加示例代码

当我处理搬迁,我工作过的公式和信息在ABI文档中发现的HERE(约4.13节,第80ish),以及一些我已经挖出了其他代码示例和博客文章.但是这一切都让人感到困惑,并没有真正拼写出来,而且我发现的所有代码都有所不同.

例如,

  • R_PPC_ADDR16_LO - > half16:#lo(S + A)
  • R_PPC_ADDR14_BRTAKEN - > low14*:( S + A)>> 2
  • 等等

所以,当我看到这种代码时,我该如何解读呢?

这是一个例子(来自这个来源)

case ELF::R_PPC64_ADDR14 : {
    assert(((Value + Addend) & 3) == 0);
    // Preserve the AA/LK bits in the branch instruction
    uint8_t aalk = *(LocalAddress+3);
    writeInt16BE(LocalAddress + 2, (aalk & 3) | ((Value + Addend) & 0xfffc));
} break;

case ELF::R_PPC64_REL24 : {
    uint64_t FinalAddress = (Section.LoadAddress + Offset);
    int32_t delta = static_cast<int32_t>(Value - FinalAddress + Addend);
    if (SignExtend32<24>(delta) != delta)
        llvm_unreachable("Relocation R_PPC64_REL24 overflow");
    // Generates a 'bl <address>' instruction
    writeInt32BE(LocalAddress, 0x48000001 | (delta & 0x03FFFFFC));
} break;
Run Code Online (Sandbox Code Playgroud)

这里有一些来自另一个例子(这里)

case R_PPC_ADDR32: /* word32 S + A */
    addr = elf_lookup(lf, symidx, 1);
    if (addr == 0)
        return -1;
    addr += addend;
    *where = addr;
    break;

case R_PPC_ADDR16_LO: /* #lo(S) */
    if (addend != 0) {
        addr = relocbase + addend;
    } else {
        addr = elf_lookup(lf, symidx, 1);
        if (addr == 0)
            return -1;
    }
    *hwhere = addr & 0xffff;
    break;

case R_PPC_ADDR16_HA: /* #ha(S) */
    if (addend != 0) {
        addr = relocbase + addend;
    } else {
        addr = elf_lookup(lf, symidx, 1);
        if (addr == 0)
            return -1;
    }
    *hwhere = ((addr >> 16) + ((addr & 0x8000) ? 1 : 0)) & 0xffff;
    break;
Run Code Online (Sandbox Code Playgroud)

另一个例子(从这里)

case R_PPC_ADDR16_HA:
    write_be16 (dso, rela->r_offset, (value + 0x8000) >> 16);
    break;
case R_PPC_ADDR24:
    write_be32 (dso, rela->r_offset, (value & 0x03fffffc) | (read_ube32 (dso, rela->r_offset) & 0xfc000003));
    break;
case R_PPC_ADDR14:
    write_be32 (dso, rela->r_offset, (value & 0xfffc) | (read_ube32 (dso, rela->r_offset) & 0xffff0003));
    break;
case R_PPC_ADDR14_BRTAKEN:
case R_PPC_ADDR14_BRNTAKEN:
    write_be32 (dso, rela->r_offset, (value & 0xfffc)
                                    | (read_ube32 (dso, rela->r_offset) & 0xffdf0003)
                                    | ((((GELF_R_TYPE (rela->r_info) == R_PPC_ADDR14_BRTAKEN) << 21)
                                    ^ (value >> 10)) & 0x00200000));
    break;
case R_PPC_REL24:
    write_be32 (dso, rela->r_offset, ((value - rela->r_offset) & 0x03fffffc) | (read_ube32 (dso, rela->r_offset) & 0xfc000003));
    break;
case R_PPC_REL32:
    write_be32 (dso, rela->r_offset, value - rela->r_offset);
    break;
Run Code Online (Sandbox Code Playgroud)

我真的很想了解这些人在这里所做的魔术,以及为什么他们的代码看起来并不总是一样.我认为一些代码假设数据已被正确屏蔽(对于分支等),而某些代码则没有.但我根本不明白这一点.


遵循符号/数据/重定位等

当我在hexeditor中查看数据时,我看到一堆"48 00 00 01".我已经发现这是一个操作码,需要使用重定位信息进行更新(这特别适用于'bl'分支和链接),但我的工具并不适用于绝大多数操作和我做的操作更新中包含错误的值(与过时工具的示例相比).很明显,我错过了这个过程的某些部分.

除了节数据外,还需要将其他重定位条目添加到rel文件的末尾.这些包括内部和外部重新安置,但我还没有完全了解这些.(这两者之间有什么区别,你何时使用其中一个?)

如果您在函数中查看此文件的末尾RuntimeDyldELF::processRelocationRef,您将看到正在创建的一些重定位条目.他们还制作存根功能.我怀疑这对我来说是缺失的环节,但它像泥浆一样清晰,我甚至没有跟踪它.

当我在每个重定位条目中输出符号时,它们每个都具有绑定/可见性[全局/弱/本地] [函数/对象]以及值,大小和节.我知道该部分是符号所在的部分,值是该部分符号的偏移量(或者是虚拟地址?).大小是符号的大小,但这很重要吗?也许全局/弱/本地对于确定它是内部还是外部重定位是有用的?

也许这个我正在谈论创建的重定位表实际上是我的rel文件的符号表?也许这个表将符号值从虚拟地址更新为部分偏移量(因为这是可重定位文件中的值,PLF中的符号表基本上是可执行文件)?


一些资源:

  1. 关于重新安置的博客:http://eli.thegreenplace.net/2011/08/25/load-time-relocation-of-shared-libraries/
  2. 最后提到了操作码:http://wiki.netbsd.org/examples/elf_executables_for_powerpc/
  3. 我的相关未回答的问题:ELF Relocation逆向工程

呼!这是一个问题的野兽.恭喜你,如果你做到这一点.:)提前感谢你能给我的任何帮助.

小智 16

我偶然发现了这个问题并认为它应该得到答案.

有elf.h方便.你可以在互联网上找到它.

如您所知,每个RELA部分都包含一个Elf32_Rela条目数组,但也与某个其他部分相关联.r_offset是其他部分的偏移量(在这种情况下 - 它对共享库的工作方式不同).您会发现节标题有一个名为sh_info的成员.这告诉你哪个部分.(它是您所期望的节头表的索引.)

您从r_info获得的"符号"实际上是驻留在另一个部分中的符号表的索引.在RELA部分的标题中查找成员sh_link.

符号表以Elf32_Sym的st_name成员的形式告诉您要查找的符号的名称.st_name是字符串部分的偏移量.您可以从符号表的节标题的sh_link成员获取哪个部分.对不起,如果这让人感到困惑.

Elf32_Shdr *sh_table = elf_image + ((Elf32_Ehdr *)elf_image)->e_shoff;
Elf32_Rela *relocs = elf_image + sh_table[relocation_section_index]->sh_offset;

unsigned section_to_modify_index = sh_table[relocation_section_index].sh_info;
char *to_modify = elf_image + sh_table[section_to_modify_index].sh_offset;

unsigned symbol_table_index = sh_table[relocation_section_index].sh_link;
Elf32_Sym *symbol_table = elf_image + sh_table[symbol_table_index].sh_offset;

unsigned string_table_index = sh_table[symbol_table].sh_link;
char *string_table = elf_image + sh_table[string_table_index].sh_offset;
Run Code Online (Sandbox Code Playgroud)

假设我们正在使用重新安置号码i.

Elf32_Rela *rel = &relocs[i];
Elf32_Sym *sym = &symbol_table[ELF32_R_SYM(rel->r_info)];
char *symbol_name = string_table + sym->st_name;
Run Code Online (Sandbox Code Playgroud)

找到该符号的地址(假设symbol_name =="printf").最终值将进入(to_modify + rel-> r_offset).

至于您链接的pdf的第79-83页上的表格,它告诉我们要在该地址放置什么,以及要写入多少字节.显然,我们刚刚获得的地址(在这种情况下是printf)是其中大部分的一部分.它对应于表达式中的S.

r_addend只是A.有时编译器需要向reloc添加一个静态常量.

B是共享对象的基址,或者是可执行程序的0,因为它们不会被移动.

所以如果ELF32_R_TYPE(rel-> r_info)== R_PPC_ADDR32我们有S + A,字大小是word32所以我们得到:

*(uint32_t *)(to_modify + rel->r_offset) = address_of_printf + rel->r_addend;
Run Code Online (Sandbox Code Playgroud)

......我们已成功完成搬迁.

当涉及到#lo,#hi等以及像low14这样的单词大小时,我无法帮助你.我对PPC一无所知,但链接的pdf似乎足够合理.

我也不知道存根函数.在链接时(通常至少动态),您通常不需要了解它们.

我不确定我是否已经回答了你的所有问题,但你应该能够看到你的示例代码现在至少做了什么.