相关结构

.plt&&.plt.got&&.plt.sec

所在segment:代码段

plt表说是表但其实其中的代码都是用来运行的

PLT : 程序链接表(PLT,Procedure Link Table)

调用链接器来解析某个外部函数的地址, 并填充到GOT表中, 然后跳转到该函数; 或者直接在GOT中查找并跳转到对应外部函数(如果非首次调用).

PLT表可能四种情况:

  1. 只有.plt
  2. `.plt和.plt.got
  3. .plt和.plt,sec
  4. .plt和.plt,sec和.plt.got

在延迟绑定环境下,每一个函数对应的PLT表项有三个字段

  1. code:跳转到对应的got表项中的地址
  2. code:压栈,该参数是对应函数在.rel.plt上的偏移,是写定的
  3. code:跳转到公共项plt[0]

公共项plt[0]处的代码push GOT[1]然后jmp GOT[2]

在使用了intel cet技术后,在code1和code2字段前会各加一个endbr64指令

且只要存在.plt.sec节,那么code1一般都位于其中

code2和code3以及公共项都位于.plt

.got && .got.plt

所在segment:数据段

got表就真的只是表了,只存储内容非运行代码

GOT : 全局偏移表(GOT, Global Offset Table)

包括了.got.got.plt.

有时.got.got.plt同时存在,有时只有.got,与relro模式相关

got表相当于plt的GOT全局偏移表, 其内容有两种情况:

  1. 如果在之前查找过该符号, 内容为外部函数的具体地址.
  2. 如果没查找过, 则内容为对应函数PLT表第二个表项的地址.

在x86架构下, 除了每个函数占用一个GOT表项外,GOT表项还保留了 3个公共表项, 保存在前三个位置, 分别是:

  • got[0]: 本ELF动态段(.dynamic段)的装载地址,初始不为空,就位于elf中
  • got1: 本ELF的link_map数据结构描述符地址,初始为空,程序开始前初始化
  • got2: _dl_runtime_resolve函数的地址,初始为空,程序开始前初始化

如果.got.got.plt同时存在,这三个表项位于.got.plt,否则位于.got

FULL RELRO下,got[1]和got[2]不初始化

Partial RELRONo RELRO则在程序正式开始前由_dl_start_user完成初始化,

其中, link_map数据结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct link_map 
{
/* Shared library's load address. */
ElfW(Addr) l_addr;
/* Pointer to library's name in the string table. */ char *l_name;
/* Dynamic section of the shared object.
Includes dynamic linking info etc.
Not interesting to us.
*/
ElfW(Dyn) *l_ld;
/* Pointer to previous and next link_map node. */
struct link_map *l_next, *l_prev;
};

imapct of RELRO

这篇文章讲的动态链接延迟绑定

是指在RELRO保护为Partial RELRO和No RELRO的情况下部分函数进行的延迟绑定


当RELRO保护为FULL RELRO的情况下
函数的动态链接会在程序进入main之前(start函数中)便完成,所有函数第一次调用时GOT表皆已指向真实函数地址

Parital RELRO和No RELRO部分函数也是这样

栈回溯如下

backtrace
1
2
3
4
5
6
7
8
►  f 0   0x7ffff7fdd80d _dl_relocate_object+3613
f 1 0x7ffff7fdd80d _dl_relocate_object+3613
f 2 0x7ffff7fd353a dl_main+8026
f 3 0x7ffff7febc4b _dl_sysdep_start+1355
f 4 0x7ffff7fd104c _dl_start+604
f 5 0x7ffff7fd104c _dl_start+604
f 6 0x7ffff7fd0108 _dl_start_user
f 7 0x1

FULL RELRO

FULL RELRO情况下,一般只会有一个.got节(.got.plt并入其中)

程序正式进行之前,所有函数的动态链接完成后

会使用mprotect将.got表所在页的写权限禁止

Partial RELRO

Parital RELRO情况下,会有.got节和.got.plt

将函数分为两个部分

.got中的函数,在main函数之前完成动态链接,并由mprotect禁止.got所在页写权限

.got.plt中的函数,则是按照正常的延迟绑定流程进行,并且一直拥有写权限

No RELRO

No RELRO和Parital一样会有.got节和.got.plt

函数同样分为两个部分

.got中的函数,同样在main函数之前完成动态链接

.got.plt中的函数,同样按照正常的延迟绑定流程进行

不同的是,No RELRO不禁止GOT表的任何写权限

.dynamic

包含了很多动态链接所需的关键信息,在动态链接中我们主要关注DT_STRTAB, DT_SYMTAB, DT_JMPREL这三项,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针

结构体成员的定义如下

1
2
3
4
5
6
00000000 Elf64_Dyn struc ; (sizeof=0x10, align=0x8, copyof_3)
00000000
00000000
00000000 d_tag dq ? //在link_map成员l_info中的下标
00000008 d_un dq ?
00000010 Elf64_Dyn ends

其中的d_un是个联合体

1
2
3
4
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;

除了上述提及的三个成员,.dynamic中还存在不少符号信息(init,fini等等),其中还有一个可能要用到的是DT_DEBUG成员,它指向_r_debug全局结构体,在其中能够找到link_map地址(一般是其第二个成员)

.dynstr

一个字符串表,包含着动态链接所需要的符号,表项是字符串以0结尾,当要引用某个字符串时,用的时相对这个secticon头的偏移

.dynsym

这个节是一个符号表(结构体数组),里面记录了各种符号的信息,每个表项是一个结构体每个结构体对应一个符号。

64位和32位中改结构体有些差异,以64位为例

1
2
3
4
5
6
7
8
9
10
00000000 Elf64_Sym struc ; (sizeof=0x18, align=0x8, mappedto_1)
00000000
00000000
00000000 st_name dd ? //函数名字符串在.dynstr中的偏移
00000004 st_info db ? //对导入函数而言,为固定的0x12
00000005 st_other db ? //对导入函数而言,剩下的都为0
00000006 st_shndx dw ?
00000008 st_value dq ?
00000010 st_size dq ?
00000018 Elf64_Sym ends

.rel.plt

它是重定位表,也是一个结构体数组,每个项对应一个导入函数。

64位和32位中改结构体有些差异,以64位为例,结构体定义如下:

1
2
3
4
5
6
7
00000000 Elf64_Rela struc ; (sizeof=0x18, align=0x8, copyof_2)
00000000
00000000
00000000 r_offset dq ? //存储导入函数的got表地址
00000008 r_info dq ? //对导入函数而言,该值等于[函数序号<<32]+7 32位下<<8
00000010 r_addend dq ? //对导入函数而言为0
00000018 Elf64_Rela ends

对整个elf生命周期都非常重要的一个结构体,结构体非常大,声明有几百行

详见glibc/include/link.h

简单看一下常用的几个成员

1
2
3
4
5
6
7
8
9
10
11
12
struct link_map {
Elf64_Addr l_addr;
char *l_name;
Elf64_Dyn *l_ld;
struct link_map *l_next;
struct link_map *l_prev;
struct link_map *l_real;
Lmid_t l_ns;
struct libname_list *l_libname;
Elf64_Dyn *l_info[77];
......
}

一个普通的程序启动后,会有四个link_map结构

通过l_next和l_prev链接,以l_next为正序的话,四个link_map分别对应

running elf -> vdso -> libc -> ld

link_map用到较多的是l_info,其是一个指针线性表,程序开始时便会用.dynmaic段的表项地址去初始化它,在resolve时会利用这这些指针去访问.dynamic段中的各个表项

链接过程

整个过程可以概述为

  1. link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针,初始化时就将.dynamic中的各表项地址用于初始化link_map
  2. .rel.plt + 第二个参数求出当前函数的重定位表项Elf_Rel的指针,记作rel
  3. rel->r_info >> 固定数作为.dynsym的下标,求出当前函数的符号表项Elf_Sym的指针,记作sym
  4. .dynstr + sym->st_name得出符号名字符串指针
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
  6. 调用这个函数

_dl_runtime_resolve直接由汇编写成,见glibc/sysdeps/x86_64/dl-trampoline.h

不过代码有点难读直接放实际运行时dump下来的

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
Dump of assembler code for function _dl_runtime_resolve_xsavec:
0x00007ffff7fe7bc0 <+0>: endbr64
0x00007ffff7fe7bc4 <+4>: push rbx
0x00007ffff7fe7bc5 <+5>: mov rbx,rsp
0x00007ffff7fe7bc8 <+8>: and rsp,0xffffffffffffffc0
0x00007ffff7fe7bcc <+12>: sub rsp,QWORD PTR [rip+0x14b35] # 0x7ffff7ffc708 <_rtld_global_ro+232>
0x00007ffff7fe7bd3 <+19>: mov QWORD PTR [rsp],rax
0x00007ffff7fe7bd7 <+23>: mov QWORD PTR [rsp+0x8],rcx
0x00007ffff7fe7bdc <+28>: mov QWORD PTR [rsp+0x10],rdx
0x00007ffff7fe7be1 <+33>: mov QWORD PTR [rsp+0x18],rsi
0x00007ffff7fe7be6 <+38>: mov QWORD PTR [rsp+0x20],rdi
0x00007ffff7fe7beb <+43>: mov QWORD PTR [rsp+0x28],r8
0x00007ffff7fe7bf0 <+48>: mov QWORD PTR [rsp+0x30],r9
0x00007ffff7fe7bf5 <+53>: mov eax,0xee
0x00007ffff7fe7bfa <+58>: xor edx,edx
0x00007ffff7fe7bfc <+60>: mov QWORD PTR [rsp+0x250],rdx
0x00007ffff7fe7c04 <+68>: mov QWORD PTR [rsp+0x258],rdx
0x00007ffff7fe7c0c <+76>: mov QWORD PTR [rsp+0x260],rdx
0x00007ffff7fe7c14 <+84>: mov QWORD PTR [rsp+0x268],rdx
0x00007ffff7fe7c1c <+92>: mov QWORD PTR [rsp+0x270],rdx
0x00007ffff7fe7c24 <+100>: mov QWORD PTR [rsp+0x278],rdx
0x00007ffff7fe7c2c <+108>: xsavec [rsp+0x40]
0x00007ffff7fe7c31 <+113>: mov rsi,QWORD PTR [rbx+0x10]
0x00007ffff7fe7c35 <+117>: mov rdi,QWORD PTR [rbx+0x8]
0x00007ffff7fe7c39 <+121>: call 0x7ffff7fe00c0 <_dl_fixup>
0x00007ffff7fe7c3e <+126>: mov r11,rax
0x00007ffff7fe7c41 <+129>: mov eax,0xee
0x00007ffff7fe7c46 <+134>: xor edx,edx
0x00007ffff7fe7c48 <+136>: xrstor [rsp+0x40]
0x00007ffff7fe7c4d <+141>: mov r9,QWORD PTR [rsp+0x30]
0x00007ffff7fe7c52 <+146>: mov r8,QWORD PTR [rsp+0x28]
0x00007ffff7fe7c57 <+151>: mov rdi,QWORD PTR [rsp+0x20]
0x00007ffff7fe7c5c <+156>: mov rsi,QWORD PTR [rsp+0x18]
0x00007ffff7fe7c61 <+161>: mov rdx,QWORD PTR [rsp+0x10]
0x00007ffff7fe7c66 <+166>: mov rcx,QWORD PTR [rsp+0x8]
0x00007ffff7fe7c6b <+171>: mov rax,QWORD PTR [rsp]
0x00007ffff7fe7c6f <+175>: mov rsp,rbx
0x00007ffff7fe7c72 <+178>: mov rbx,QWORD PTR [rsp]
0x00007ffff7fe7c76 <+182>: add rsp,0x18
0x00007ffff7fe7c7a <+186>: bnd jmp r11

_dl_fixup

_dl_runtime_resolve的主体其实是调用_dl_fixup

当然_dl_fixup也调用了不少函数,暂时不深入,重点关注_dl_fixup

_dl_fixup是在glibc/elf/dl-runtime.c实现的

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) DL_ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

const uintptr_t pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]);

const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL])
+ reloc_offset (pltgot, reloc_arg));
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}

#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}

/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);

if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

#ifdef SHARED
/* Auditing checkpoint: we have a new binding. Provide the auditing
libraries the possibility to change the value and tell us whether further
auditing is wanted.
The l_reloc_result is only allocated if there is an audit module which
provides a la_symbind. */
if (l->l_reloc_result != NULL)
{
/* This is the address in the array where we store the result of previous
relocations. */
struct reloc_result *reloc_result
= &l->l_reloc_result[reloc_index (pltgot, reloc_arg, sizeof (PLTREL))];
unsigned int init = atomic_load_acquire (&reloc_result->init);
if (init == 0)
{
_dl_audit_symbind (l, reloc_result, reloc, sym, &value, result, true);

/* Store the result for later runs. */
if (__glibc_likely (! GLRO(dl_bind_not)))
{
reloc_result->addr = value;
/* Guarantee all previous writes complete before init is
updated. See CONCURRENCY NOTES below. */
atomic_store_release (&reloc_result->init, 1);
}
}
else
value = reloc_result->addr;
}
#endif

/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}