ELF文件分析(二) 段节比较和节头表

0x01 段vs节 (Segment vs Section)

名称 段(Segment) 节(Section)
描述来源 程序头表 节头表
面向对象 操作系统加载器 链接器和调试器
核心概念 运行时视图 链接时视图
描述是否为运行必需 是,必须要有程序头表 否,节头表可选
目的 加载和执行 链接和调试

Important

段(Segment)和节(Section)是理解ELF文件的两种不同但相关的视角,分别服务于程序生命周期的不同阶段。

  • 节 (Section) 是链接器眼中的链接视图(Linking View)。它将文件内容划分为具有特定类型和属性的逻辑单元,如代码(.text)、只读数据(.rodata)和可读写数据(.data)。这些信息对于链接器拼接目标文件和调试器定位符号至关重要。
  • 段 (Segment) 则是加载器眼中的运行时视图(Execution View)。关键在于,段是链接器为了优化加载过程,而对节进行组织和合并的结果。 链接器会将权限相似的节(如所有只读+可执行的 .text、.plt)合并成一个大的、连续的段。这样,加载器只需进行一次内存映射,而不是对十几个小节分别操作,这极大地提升了程序启动效率。

    因此,它们的存在与否取决于ELF文件的类型(e_type)和用途
  • 对于可执行文件 (ET_EXEC, ET_DYN),程序头表是必须的,因为它定义了如何将程序加载到内存执行。而节头表是可选的,可以使用strip命令去除以减小文件大小。
  • 对于可重定位文件/目标文件 (ET_REL),节头表是必须的,因为它包含了链接器进行符号解析和重定位所需的全部信息。而这类文件通常没有程序头表,因为它们还不能被直接执行。

0x02 ELF节头

1
2
3
4
5
# readelf解析 -l 输出的后半部分显示了节在段中的情况
readelf -l ELF-file
# readelf解析 -S 节头表
readelf -S ELF-file
readelf -WS ELF-file

节头的内部结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct {
uint32_t sh_name; // 在节区字符串表中的偏移
uint32_t sh_type; // 节的类型
uint32_t sh_flags; // 节的标志,如 SHT_WRITE | SHT_ALLOC
Elf32_Addr sh_addr; // 节的起始地址
Elf32_Off sh_offset; // 节的文件偏移
uint32_t sh_size; // 节在磁盘中的大小
uint32_t sh_link; // 指向另一个节的索引号
uint32_t sh_info; // 与节相关的信息
uint32_t sh_addralign; // 节的内存对齐
uint32_t sh_entsize; // 节的每个条目的大小
} Elf32_Shdr;

typedef struct {
uint32_t sh_name; // 同上,部分长度不同而已
uint32_t sh_type;
uint64_t sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
} Elf64_Shdr;

0x03 各种各样的节

下面介绍一些比较常见的节,其用途、数据形式、节类型、一般情况的所属段。

大概稍微看看留意一下就好了,后面会进一步解析,因此此处记忆完全没有任何必要。需要说明的是节标志中A代表alloc(装载),X代表execute(可执行),W代表write(可写),I代表info(信息),如果没填代表没有这些标志。

节名称 用途 数据形式 节类型 节标志
.text 存放程序的主要可执行代码(机器指令) 机器指令 SHT_PROGBITS AX
.rodata 存放只读数据,如字符串常量、const 变量 程序中定义的常量数据 SHT_PROGBITS A
.plt 过程链接表, 用于动态链接中函数的“懒”解析 可执行的跳转指令 SHT_PROGBITS AX
.data 存放已初始化的全局变量和静态变量 程序中定义的变量初始值 SHT_PROGBITS WA
.bss 存放未初始化的全局变量和静态变量。加载时由系统清零 在文件中不占空间 SHT_NOBITS WA
.got.plt 全局偏移量表.plt 配合,存储动态链接函数的真实地址 函数地址指针的数组 SHT_PROGBITS WA
.dynsym 动态符号表,仅包含动态链接所需的符号信息 ElfN_Sym结构体数组 SHT_DYNSYM A
.dynstr 动态字符串表,存放 .dynsym 中符号的名字字符串 以null结尾的字符串 SHT_STRTAB A
.rel.* 重定位表,包含需要链接器或加载器在运行时修正地址的信息 ElfN_Rel/ ElfN_Rela 结构体 SHT_REL / SHT_RELA A/AI
.hash 查找符号的散列表 哈希表数据结构 SHT_HASH / SHT_GNU_HASH A
.symtab 完整符号表,包含程序所有符号,包括静态函数、局部变量等,主要用于调试和链接。 ElfN_Sym 结构体数组 SHT_SYMTAB
.strtab 字符串表 ,存放 .symtab 中符号的名字字符串 以null结尾的字符串 SHT_STRTAB
.shstrtab 节头字符串表 ,存放节头的名字字符串 以null结尾的字符串 SHT_STRTAB

实例

目标文件(take.o) 节的情况:

elf-2-1

可执行文件(take) 节的情况:

elf-2-2

0x04 对节头表痛下黑手

节头表对程序的执行不是必需的,但对分析却至关重要。因此,移除或破坏节头表是恶意软件(Malware)和软件加壳(Packer)中一种常见的反静态分析、反逆向工程的手段。接触和了解这些技术,有助于我们分析被处理过的程序。

1. 完全剥离

直接删除运行无关的信息,如本地符号表

1
2
# strip 命令的主要功能是移除符号表 (.symtab) 和调试信息,以减小文件体积。
strip ELF-file

2. 丢失联系

原理是修改文件头关于节头表的信息,具体是e_shoff , e_shentsizee_shnum , 下面是一个简单的bash脚本,通过dd命令进行置0修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

# --- hide_sht.sh ---
# 功能:通过将 ELF 主头中的相关字段置零,来“隐藏”节头表。(仅适用于64位程序)
# 用法: ./hide_sht.sh <executable_file>

# 检查参数
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "用法: $0 <elf_file>"
exit 1
fi

TARGET_FILE="$1"
BACKUP_FILE="${TARGET_FILE}.bak"

echo "[*] 目标文件: ${TARGET_FILE}"

# 1. 自动备份原始文件
echo "[+] 正在备份文件到 ${BACKUP_FILE}..."
cp "${TARGET_FILE}" "${BACKUP_FILE}"

# 2. 显示修改前的节头表信息
echo -e "\n[*] 修改前的节头表信息:"
readelf -S "${TARGET_FILE}" | head -n 5 # 只显示开头几行示意

# 3. 使用 dd 命令将 e_shoff (8字节, 位于偏移0x28) 置零
# if=/dev/zero: 输入源是空字节
# of=...: 输出到目标文件
# bs=1: 块大小为1字节
# count=8: 写入8个块 (总共8字节)
# seek=40: 在输出文件中跳过40个字节 (0x28)
# conv=notrunc: 【极其重要】不要截断文件!
echo -e "\n[+] 正在将 e_shoff 置零 (偏移 0x28, 8字节)..."
dd if=/dev/zero of="${TARGET_FILE}" bs=1 count=8 seek=40 conv=notrunc &>/dev/null

# 4. 使用 dd 命令将 e_shentsize 和 e_shnum (各2字节, 位于偏移0x3A和0x3C) 置零
# 这两个字段是连续的,所以可以一次性写入4个字节
echo "[+] 正在将 e_shentsize 和 e_shnum 置零 (偏移 0x3A, 4字节)..."
dd if=/dev/zero of="${TARGET_FILE}" bs=1 count=4 seek=58 conv=notrunc &>/dev/null

echo -e "\n[+] 操作完成!"

# 5. 验证结果
echo "[*] 验证修改后的文件:"
readelf -S "${TARGET_FILE}"

echo -e "\n[*] 如需恢复,请执行: mv ${BACKUP_FILE} ${TARGET_FILE}"

3. 伪造!扭曲!变形!

通过修改文件头和节头表内容,可以实现更多在节上的操作。后续会进行细致的讨论。

0x05 小问题

关于节:

  • 目标文件和可执行文件相比少了哪些节,或者说两者在节上有哪些不同?
  • 分析以下节头表记录的第一个节,你发现它的特别之处了吗?
  • .bss是一个特别的节,节类型是SHT_NOBITS,查阅资料,了解关于其更多的信息。
  • 节头表和程序头表在文件和内存中的位置有什么特征?