返回

ELF 文件 SHT_DYNAMIC 与 SHT_DYNSYM 解析难题及解决

Linux

SHT_DYNAMIC 与 SHT_DYNSYM:理解与解决

ELF(可执行和链接格式)文件是一种广泛使用的二进制文件格式,了解其结构对于程序分析和调试至关重要。 其中,节头部(Section Header)包含着重要信息。SHT_DYNAMIC类型的节存放动态链接的信息,其sh_link字段根据ELF规范本应指向字符串表,而非符号表,可一些二进制文件,比如ARM/Android上的OAT文件,该字段却指向SHT_DYNSYM类型的节,这就引起了一个问题:为什么?以及该如何处理?

问题分析

按理说,动态链接表(.dynamic)中的条目,像依赖的库的名称等,应引用一个字符串表。而规范明确表示,SHT_DYNAMIC节的sh_link字段需要指向存储这些字符串的节的索引,通常这个节是SHT_STRTAB类型的。可是,在一些情况下,sh_link值会指向SHT_DYNSYM类型的动态符号表节,这是一种明显的偏离。

一个关键的原因在于动态符号表SHT_DYNSYM,它与一个独立的动态字符串表SHT_DYNSTR关联,两者协同工作,即 SHT_DYNSYM 中的符号引用字符串表中对应的字符串,进而解析符号的名字和地址。 SHT_DYNSYM 不直接存储字符串数据, 而是存储指向 SHT_DYNSTR 中对应字符串的偏移量。这使得节之间构成间接的链接关系。

因此,将sh_link字段指向SHT_DYNSYM虽然违反了规范的“直接指向”的规则,但是符合加载器间接寻址的要求,在加载器逻辑上是可行的。因为通过SHT_DYNSYM,加载器可以间接地获取SHT_DYNSTR的地址,找到需要的动态链接字符串。

解决方案

这种现象虽然不是理想情况,但考虑到系统加载器依赖此种关联,直接更改sh_link字段的值可能会导致文件加载失败。这里提供两种主要的处理策略,需要根据不同场景选择:

1. 保留现状,通过代码或脚本实现正确解析

大部分场景中,加载器依赖此种关联方式。这意味着,如果我们处理这类ELF文件,必须清楚这一点,并通过代码逻辑适配。解析时,应当先获取SHT_DYNAMIC节的sh_link值,检查该值所指向的节类型,若是SHT_DYNSYM,那么应进一步寻找此SHT_DYNSYM关联的字符串表节 。具体实现上:

  1. 读取SHT_DYNAMIC节头的sh_link字段值,此值代表一个节索引dynsym_index
  2. 获取节索引 dynsym_index 对应的节头部信息 dynsym_shdr
  3. 如果 dynsym_shdr 的节类型是SHT_DYNSYM,那么应该继续获取 dynsym_shdrsh_link值。这个新值代表了字符串表(SHT_DYNSTR类型)的节索引。

以下示例代码(Python),用于解析此结构:

import lief
from enum import IntEnum

class ElfSectionType(IntEnum):
    SHT_NULL    = 0
    SHT_PROGBITS = 1
    SHT_SYMTAB    = 2
    SHT_STRTAB  = 3
    SHT_RELA  = 4
    SHT_HASH  = 5
    SHT_DYNAMIC    = 6
    SHT_NOTE  = 7
    SHT_NOBITS   = 8
    SHT_REL   = 9
    SHT_SHLIB  = 10
    SHT_DYNSYM    = 11


def get_dyn_str_table(elf_file):
    for section in elf_file.sections:
      if section.type == ElfSectionType.SHT_DYNAMIC:
          dynsym_index = section.link
          dynsym_section = elf_file.sections[dynsym_index]
          if dynsym_section.type == ElfSectionType.SHT_DYNSYM:
            strtab_index = dynsym_section.link
            strtab_section = elf_file.sections[strtab_index]
            if strtab_section.type == ElfSectionType.SHT_STRTAB:
              return strtab_section

    return None

elf_file_path = "your_elf_file"  # 指定目标 ELF 文件路径
try:
  binary = lief.ELF.parse(elf_file_path)
  if binary:
    str_tab = get_dyn_str_table(binary)
    if str_tab:
      print("Found the dyn str tab:", str_tab.name, hex(str_tab.virtual_address))
    else:
        print("Can't locate dynamic string table by above logic.")
except lief.bad_file as e:
    print("Error opening file")
    exit()

上述代码使用 lief 库处理ELF文件。这段代码首先找到SHT_DYNAMIC节,之后读取其sh_link。若所指为SHT_DYNSYM,则接着读取SHT_DYNSYM节的sh_link,并将其所指节识别为SHT_STRTAB,即可获取真正的动态链接字符串表。
安全提示: 在解析ELF结构时,注意越界访问检查,避免程序崩溃或出现其他安全问题。确保使用经过验证的库进行解析,避免自定义解析造成的潜在漏洞。

2. (慎用) 修改sh_link使其符合规范

仅在极少数、非常特殊的情况下,可能需要尝试直接修改sh_link的值。修改必须在深入了解系统加载器以及依赖的细节后谨慎进行。这个做法极不推荐,任何不充分的修改会导致不可预见的崩溃,只建议作为最后手段,并且在修改之前必须备份原始文件 。步骤大致为:

  1. 定位 SHT_DYNAMICsh_link 指向的节 SHT_DYNSYM
  2. 定位 SHT_DYNSYMsh_link 指向的 SHT_DYNSTR
  3. 修改 SHT_DYNAMICsh_link 指向 SHT_DYNSTR,确保结果的ELF文件的结构更接近规范。

利用工具 patchelf 或类似的ELF文件修改工具可以实现以上操作。

示例命令(patchelf):

# 注意,此命令假设你已知目标 SHT_DYNAMIC 对应的 index 是 $dynamic_index,SHT_DYNSTR 的索引为$dynstr_index
patchelf --section-set-sh-link $dynamic_index $dynstr_index your_elf_file

安全提示: 修改二进制文件非常危险。需要充分测试修改后的文件是否能够正确运行,确保备份。同时也要考虑系统加载器的兼容性问题。务必避免在生产环境上执行未经充分测试的操作。

结论

虽然 SHT_DYNAMICsh_link 指向 SHT_DYNSYM 并不完全符合 ELF 规范的定义,但在一些实践案例中,这是一种间接关联方式。这种结构依赖于动态符号表和动态字符串表之间的关系。开发者必须意识到这种潜在偏差,通过代码或脚本正确处理,在特殊情况下进行非常谨慎的修改。避免随意修改节头信息,并进行充分测试验证。 理解并掌握这一细节,能够帮助开发人员更好的进行系统级分析和逆向工程。