ELF文件分析(三) 符号解析
0x00 资源
关于ELF文件格式规范中的一些宏,可以参看elf.h这个头文件。下面的实例分析中会用到其中的一部分。对于最新最全的ELF相关的内容可以去参考elfutils这个软件源码中相关的。
0x01 ELF符号
符号(symbol)是对事物的引用,比如说我们需要说话的时候需要指代一个叫人,我们就会叫他的名字,而这个名字就是一种简单的符号,人的客体存在性不依赖于符号,但是为了满足交流方便和统一指代的原因,这个符号又是非常重要的部分。ELF符号(下面简称符号)是对某些类型的数据或者代码(如全局变量或函数)的符号引用。
ELF文件中的符号怎么存放呢?难道要直接存字符串吗?如果真这样做,显然这是非常失败的设计。在linux下是通过符号项来存储的,存放的是字符串的偏移,然后将真正的字符串符号放在字符串表中进行关联。
下面是符号项的格式:
1 | typedef struct { |
ELF符号存在于符号表中,符号表实质就是一个关于符号项的数组构成的节。其中.dynsym节保存着来自外部文件符号的全局符号,如printf这样的库函数。而.symtab节则不但保存了外部文件符号的全局符号,还包含着可执行文件的本地符号,如全局变量,本地函数。
info
这两个符号表有什么关联呢,很明显.symtab节明明包含.dynsym节中的符号作为子集,那后者有什么存在的必要呢。 实际上两者的节属性不一样,主要区分在sh_flag字段,对于.dynsym,被标记了ALLOC,而.symtab没有被标记,这意味着前者会在运行时装载进内存,而后者并不会。前者是执行过程中所必要的信息,而后者不是,可以通过strip去除。
0x02 符号类型和符号绑定
这部分主要讲关于st_info的含义,这是一个8位的整数,低4位用来标记符号类型,高4位用来标记符号绑定。
1. 符号类型
- STT_NOTYPE 与类型未定义关联
- STT_FUNC 与函数或者可执行代码关联
- STT_OBJECT 与数据对象关联
- STT_SECTION 与节关联
- STT_FILE 与文件关联
2. 符号绑定
- STB_LOCAL 本地符号(如static的函数)
- STB_GLOBAL 全局符号,对于其他目标文件可见
- STB_WEAK 弱全局符号,类似与全局符号,但是可能被同名的STB_GLOBAL或者其他STB_WEAK覆盖
note
如何解析符号类型和符号绑定
- Type: st_info & 0x0F
- Binding: st_info >> 4
0x03 符号表和字符串表解析
1 | # readelf解析 -s 符号 |
实例
下面我们来手动进行符号的解析,只使用imhex且不利用模板文件来展示整个过程。也算是对前两次的复习吧。
note
下面涉及到的几个重要概念:符号表,字符串表,字符串表在节头表中对应的表项。
- 符号表是一个大数组,表项指明了符号的基本情况,可以通过其中的信息去相应的字符串表中查找。符号表是一个节。
-
字符串表不是一个数组,而是每个表项不等的字符串构成,第一个字符串为空(或者认为第一个字节为
00)。字符串表是一个节。 - 字符串表在节头表中对应的表项,这个部分存在于节头表,指明了如何去寻找字符串表这个节。
1. 文件头分析
首先imhex打开程序,观察首先确定当前文件的格式和文件属性。
1
2
3
4
5
6Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF............
00000010 03 00 3E 00 01 00 00 00 20 11 00 00 00 00 00 00 ..>..... .......
00000020 40 00 00 00 00 00 00 00 58 38 00 00 00 00 00 00 @.......X8......
00000030 00 00 00 00 40 00 38 00 0D 00 40 00 1F 00 1E 00 ....@.8...@.....
这里是文件头部分,按照之前介绍的进行解析。
前4字节是魔数7F 45 4C 46,然后下一字节02,显示出是64位的程序。接着的第六字节01,说明是小端序程序。然后我们现在可以确定整个ELF头的格式了,用64位的模板进行解析,可以获得下面的关键数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Elf64_Ehdr elf_header = {
.e_ident = { 0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
.e_type = 0x0003, // ET_DYN 动态链接文件
.e_machine = 0x003E, // EM_X86_64
.e_version = 0x00000001,
.e_entry = 0x0000000000001120ULL, // 入口点地址0x1120
.e_phoff = 0x0000000000000040ULL, // 程序头表偏移0x40
.e_shoff = 0x0000000000003858ULL, // 节头表偏移0x3858
.e_flags = 0x00000000, // 处理器标志
.e_ehsize = 0x0040, // elf文件头大小 64字节
.e_phentsize = 0x0038, // elf程序头表项大小 56字节
.e_phnum = 0x000D, // elf程序头表项目数量 13个
.e_shentsize = 0x0040, // elf节头表项大小 64字节
.e_shnum = 0x001F, // elf节头表项数量 31个
.e_shstrndx = 0x001E // 节头字符串表在节头的索引 [30]
};
2. 节头表分析
好的,现在我们可以继续定位节头字符串表的位置,首先需要简单计算一下,参考公式如下: 节头字符串表的文件偏移 = 节头表开始的文件偏移 + 节头字符串表的索引 * 节头表项大小
在本例中即是
$$ \text{sh_str_addr} \begin{aligned}[t] &= \text{e_shoff} + \text{e_shstrndx} \times \text{e_shentsize} \\ &= \text{0x}\text{3858} + \text{0x}\text{1E} \times \text{0x}\text{40} \\ &= \text{0x}\text{3FD8} \end{aligned} $$ 我们查看这部分的内容
1
2
3
4
5
6
7Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00003FD0 11 00 00 00 03 00 00 00 ................
00003FE0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00003FF0 3B 37 00 00 00 00 00 00 1A 01 00 00 00 00 00 00 ;7..............
00004000 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................
00004010 00 00 00 00 00 00 00 00 ........
然后利用节头表的结构进行解析
1
2
3
4
5
6
7
8
9
10
11
12struct Elf64_Shdr shstr_entry = {
.sh_name = 0x00000011, // 节区名称的偏移量,.shstrtab中的0x11开始的字符串
.sh_type = 0x00000003, // 节区类型 SHT_STRTAB
.sh_flags = 0x0000000000000000, // 节区标志位
.sh_addr = 0x0000000000000000, // 虚拟地址,0代表不存在与内存中
.sh_offset = 0x000000000000373B, // 文件偏移量,文件偏移373B
.sh_size = 0x000000000000011A, // 字符串表字节数
.sh_link = 0x00000000, // 链接信息,未使用
.sh_info = 0x00000000, // 额外信息,未使用
.sh_addralign = 0x0000000000000001, // 对齐
.sh_entsize = 0x0000000000000000 // 条目大小,这个不是数组类的(如符号表),没有这个值
};
接着我们到该部分看看情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00003730 00 2E 73 79 6D ..sym
00003740 74 61 62 00 2E 73 74 72 74 61 62 00 2E 73 68 73 tab..strtab..shs
00003750 74 72 74 61 62 00 2E 69 6E 74 65 72 70 00 2E 6E trtab..interp..n
00003760 6F 74 65 2E 67 6E 75 2E 70 72 6F 70 65 72 74 79 ote.gnu.property
00003770 00 2E 6E 6F 74 65 2E 67 6E 75 2E 62 75 69 6C 64 ..note.gnu.build
00003780 2D 69 64 00 2E 6E 6F 74 65 2E 41 42 49 2D 74 61 -id..note.ABI-ta
00003790 67 00 2E 67 6E 75 2E 68 61 73 68 00 2E 64 79 6E g..gnu.hash..dyn
000037A0 73 79 6D 00 2E 64 79 6E 73 74 72 00 2E 67 6E 75 sym..dynstr..gnu
000037B0 2E 76 65 72 73 69 6F 6E 00 2E 67 6E 75 2E 76 65 .version..gnu.ve
000037C0 72 73 69 6F 6E 5F 72 00 2E 72 65 6C 61 2E 64 79 rsion_r..rela.dy
000037D0 6E 00 2E 72 65 6C 61 2E 70 6C 74 00 2E 69 6E 69 n..rela.plt..ini
000037E0 74 00 2E 70 6C 74 2E 67 6F 74 00 2E 70 6C 74 2E t..plt.got..plt.
000037F0 73 65 63 00 2E 74 65 78 74 00 2E 66 69 6E 69 00 sec..text..fini.
00003800 2E 72 6F 64 61 74 61 00 2E 65 68 5F 66 72 61 6D .rodata..eh_fram
00003810 65 5F 68 64 72 00 2E 65 68 5F 66 72 61 6D 65 00 e_hdr..eh_frame.
00003820 2E 69 6E 69 74 5F 61 72 72 61 79 00 2E 66 69 6E .init_array..fin
00003830 69 5F 61 72 72 61 79 00 2E 64 79 6E 61 6D 69 63 i_array..dynamic
00003840 00 2E 64 61 74 61 00 2E 62 73 73 00 2E 63 6F 6D ..data..bss..com
00003850 6D 65 6E 74 00 ment.
这就是关于节区名字符串表的情况,我们可以从中定位到.symtab和.dynsym的st_name的值(偏移量),此外你也能注意到这两个符号表对应的字符串表的名字strtab和dynstr。
还记得我们刚才的.shstrtab的节的sh_name吗,是0x11,数一数从上面开头往后的第17个字节的字符串是什么?是.shstrtab对吧。现在我们试试找到手动找到.symtab和.dynsym的st_name的值。
3. 寻找和解析.symtab和.strtab
为了找到 .symtab
节的节头,我们需要在节头表中进行搜索。我们的目标是找到一个类型为
SHT_SYMTAB (2),并且其 sh_name
字段指向节头字符串表 (.shstrtab) 中 ".symtab"
字符串的表项。通过观察 .shstrtab,我们发现
".symtab" 字符串的偏移量为1。
因此,我们寻找的表项特征是 sh_name = 1 且
sh_type = 2。我们从0x3858开始寻找,每64字节是一个表项,预期的前8字节是01 00 00 00 02 00 00 00,不难找到其位置。
1
2
3
4
5
6
7Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00003F50 01 00 00 00 02 00 00 00 ........
00003F60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00003F70 40 30 00 00 00 00 00 00 68 04 00 00 00 00 00 00 @0......h.......
00003F80 1D 00 00 00 12 00 00 00 08 00 00 00 00 00 00 00 ................
00003F90 18 00 00 00 00 00 00 00 ........
依然可以类似的解析,过程同上,主要提取重要的信息字段,关注的字段有:
- sh_offset = 0x3040
- sh_size = 0x0468 (1128)
- sh_link = 0x1D (29)
- sh_entsize = 0x18 (24)
这些字段告诉我们要找的.symtab表在文件偏移为0x3040开始,一直有1128字节的大小,每个符号项大小为24字节,一共有47个(计算1128 / 24)。而sh_link则指出字符串表的表头项在节头表中的索引是[29]。
1
2
3
4
5
6
7
8
9
10
11Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00003040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00003050 00 00 00 00 00 00 00 00 01 00 00 00 04 00 F1 FF ................
00003060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00003070 09 00 00 00 01 00 04 00 8C 03 00 00 00 00 00 00 ................
00003080 20 00 00 00 00 00 00 00 13 00 00 00 04 00 F1 FF ...............
00003090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000030A0 1E 00 00 00 02 00 10 00 50 11 00 00 00 00 00 00 ........P.......
000030B0 00 00 00 00 00 00 00 00 20 00 00 00 02 00 10 00 ........ .......
000030C0 80 11 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ........
这个节比较大,我们就只展示和分析开头的一部分。首先我们来解析第一个非0表项。
1
2
3
4Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00003050 01 00 00 00 04 00 F1 FF ........
00003060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
按照结构体解析如下:
1
2
3
4
5
6
7
8struct Elf64_Sym sym_entry1 = {
.st_name = 0x00000001, // 指向.strtab的偏移为1的字节开始的字符串
.st_info = 0x04, // 绑定: STB_LOCAL (0), 类型: STT_FILE (4)
.st_other = 0x00, // 可见性,STV_DEFAULT
.st_shndx = 0xFFF1 // 节区索引 SHN_ABS
.st_value = 0x0000000000000000, // 值 0
.st_size = 0x0000000000000000 // 内存大小 0
};
如果我们想要知道这个符号的名称,我们得在.strtab中寻找答案,利用刚才的sh_link = 0x1D这个关键信息可以很容易找到字符串表在节头表的信息。如下所示:
1
2
3
4
5
6
7Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00003F90 09 00 00 00 03 00 00 00 ........
00003FA0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00003FB0 A8 34 00 00 00 00 00 00 93 02 00 00 00 00 00 00 .4..............
00003FC0 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................
00003FD0 00 00 00 00 00 00 00 00 ........
然后同样进行解析即可,可以找到字符串表的文件偏移是0x34A8。
1
2
3
4
5
6
7
8
9
10
11Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
000034A0 00 53 63 72 74 31 2E 6F .Scrt1.o
000034B0 00 5F 5F 61 62 69 5F 74 61 67 00 63 72 74 73 74 .__abi_tag.crtst
000034C0 75 66 66 2E 63 00 64 65 72 65 67 69 73 74 65 72 uff.c.deregister
000034D0 5F 74 6D 5F 63 6C 6F 6E 65 73 00 5F 5F 64 6F 5F _tm_clones.__do_
000034E0 67 6C 6F 62 61 6C 5F 64 74 6F 72 73 5F 61 75 78 global_dtors_aux
000034F0 00 63 6F 6D 70 6C 65 74 65 64 2E 30 00 5F 5F 64 .completed.0.__d
00003500 6F 5F 67 6C 6F 62 61 6C 5F 64 74 6F 72 73 5F 61 o_global_dtors_a
00003510 75 78 5F 66 69 6E 69 5F 61 72 72 61 79 5F 65 6E ux_fini_array_en
00003520 74 72 79 00 66 72 61 6D 65 5F 64 75 6D 6D 79 00 try.frame_dummy.
可以看到第一个符号是Scrt1.o这个文件,按照这个方法不断进行,我们就可以将其他的符号全部解析出来。
info
Scrt1.o是C运行时启动文件(C Runtime Startup File)在链接到程序时的表现。它的主要作用是作为程序开始执行的实际入口点。当操作系统加载可执行文件时,它不会直接跳转到C语言代码中的main函数。相反,它会跳转到一个由链接器确定的地址(一般是_start),这个地址通常位于Scrt1.o提供的代码中。
0x04 小问题
关于符号表:
- 本地符号只有当前文件可见,那保留它的用途是什么呢?
- 查询sh_info的作用。分析.symtab的节头表项信息,你会发现sh_info=0x12, 表示第一个非本地符号的索引。为什么要保留这个信息?
- st_value到底代表什么?链接器如何知道这个值的用途?
关于字符串表:
- 为什么 ELF
规范要求字符串表的第一个字节是
00? - 为什么需要多个字符串表,而不能合并在一个中?