本篇文章将会尝试解释重定位,PLT,GOT以及延迟绑定,并且通过一个简单的例子动态调试追踪延迟绑定的过程。
Relocation is the process of connecting symbolic references with symbolic definitions. Relocatable files must have information that describes how to modify their section contents, thus allowing executable and shared object files to hold the right information for a process's program image. Relocation entries are these data.
即:
以32位系统为例(为了便于理解,本文后续内容均会以32位系统场景描述),其关键的数据结构如下:
在程序加载时,会通过自己的.rel
section,告诉连接器需要重定位的位置,在后续会详细的展示一个32位程序重定位的过程。
像上面那种在程序加载时,通过.rel
section,让编译器基于重定位信息计算出调用函数在程序中的实际位置的加载方式,一般被称为静态链接 ,如果程序使用了外部的库函数时,整个库函数都会被直接编译到程序中。
可以思考一下它的缺点,以及对应的改正方法:
动态链接 技术的提出就是为了解决这个问题,在程序运行时,将共享库和程序本身进行链接,同时,内存里的程序可以共享同一个库文件,这样既节省了硬盘存储空间,同样节省了内存空间。
静态链接与动态链接主要区别如下图所示。(本图参照《CTF竞赛权威指南》所画)
为了做到动态编译,首先需要生成位置无关代码(Posistion-Independent Code,PIC) ,通过PIC
一个共享库可以被多个进程共享。
同时想要完成动态链接在源程序中还需要有:
因为数据段和代码段之间的距离是一个运行时常量,他们之间的偏移是固定的,于是这里就有了全局偏移表(GOT,Global Offset Table),它位于数据段的开始,用于保存全局变量以及库函数(外部函数)的引用,每一条8个字节,在程序加载时会完成重定位,并填入符号的绝对地址。GOT一般被拆成了两个section,不需要延迟绑定,用于存储全局变量,加载到内存中只需要被读取的.got
,以及为了存储库函数需要延迟绑定写入的.got.plt
。
而同时为了完成延迟绑定还需要将外部函数的值在运行时写入.got.plt
,因此又引入了过程链接表(PLT,Procedure Liknage Table)。PLT是由代码片段组成,用于将地址无关函数转移到绝对地址。每一个被调用的库函数,都会映射到一组PLT 和 GOT。如下图所示:
Lazy Binding ,即延迟绑定,指的是只有当函数被调用的时候才进行函数绑定,这种方式加快的程序的启动速度。
为了完成延迟绑定的过程,PLT和GOT需要配合完成一些事情。
假设程序中存在一个puts函数被调用,在PLT 中的表项为puts@plt
,在GOT中表项为`puts@got.plt。为了完成延迟绑定,在第一次执行的时候会完成以下事情:
在后续调用,就会直接到GOT表项取得puts
函数的地址。
整理一下,PLT和GOT中存在着通用的指令和后续的指令如下所示:
整个过程草图如下所示
环境:
源代码:
编译:
通过objdump查看目标文件汇编代码:
print_hello
函数如下:
main
函数如下:
通过objdump查看最终汇编代码:
print_hello
函数如下:
main
函数如下:
对比目标代码和最终代码的反汇编代码,可以看到,在print_hello
函数中,调用call函数时对应内容是有经过重定向的,如下图所示。
在目标代码中:
查看对应重定位表,可以看到:
offset 为$5$ 的符号是 __x86.get_pc_thunk.bx
, offset为 $17$的符号是puts
。
而在最终代码中:
查看编译之后程序的符号:
对于 __x86.get_pc_thunk.bx
, 采用的是S + A - P
的方式计算偏移:
那么还剩下一个问题,puts
函数此时在符号表中的值是0,又应该怎么计算呢?
前面有讲到,Linux下符号动态链接默认采用的是延迟绑定的方式,也就是说,在程序运行时用到改符号的时候才会去解析它的地址(值)。
开始动态调试,启动后在puts@plt
下断点:
可以看到PLT表的内容和上述所说的基本吻合,首先是一个jump
指令,此时根据上面信息可以得知ebx
寄存器存储的是GOT
表地址,所以这条jump
指令就跳转到GOT
表中。第二条push
序号入栈,第三条jump
指令调转到的位置就是PLT[0]
。
查看jump地址的信息:
跳入的是.got.plt
且表现值存储的值是0x56556056
即刚才的push
指令。
继续查看0x56556030
处的内容,ebx
存储的仍然是GOT表的地址,因此此处指令仍然是上面所描述的push GOT[1] jmp GOT[2]
。
继续调试,发现他进入的即是_dl_runtime_resolve()
函数。
到这里有关PLT,GOT延迟绑定的整个过程已经基本梳理完毕,在后续有关更多PLT,GOT攻击利用的时候,再来详细讨论_dl_runtime_resolve()
函数。
好的,下面我们结合源码分析一下_dl_runtime_resolve()
函数的实现。
在GNU C库的源码中,_dl_runtime_resolve()
函数的定义位于elf/dl-runtime.c
文件中。该函数的主要作用是在程序运行期间解析动态链接函数。
函数的定义如下:
其中,ElfW(Addr)
是一个宏定义,根据编译时指定的目标架构不同而不同。例如,在32位x86架构上,它被定义为unsigned int
。
函数中的ref
参数是一个指向函数指针的指针,它指向需要被解析的动态链接函数的地址。
函数的实现过程如下:
这里的D_PTR()
宏是GNU C库中的一个宏定义,用于读取动态链接器内部数据结构中的字段。DT_PLTGOT
表示动态链接器中GOT表的地址。
然后,从ref
参数指向的地址中读取一个偏移量,该偏移量是一个相对于共享对象基地址的偏移量,表示需要解析的动态链接函数在共享对象中的位置。
这里的DT_RELA
表示动态链接器中重定位表的地址,通过它可以找到需要解析的动态链接函数的重定位项。
接着,通过解析重定位项,获取目标函数的地址。
其中,_dl_fixup()
函数是用于解析重定位项的函数,它会根据重定位项中的信息,计算出目标函数的地址,并返回给调用者。
最后,将目标函数的地址返回给调用者,完成动态链接函数的解析过程。
综上所述,_dl_runtime_resolve()
函数的实现过程中,主要涉及了动态链接器的内部数据结构和重定位表的解析。它是实现动态链接的核心函数之一,可以在程序运行期间解析动态链接函数,实现代码的灵活性和可重用性。
/
/
隐式加法重定位,重定位处的汇编一般会是 e8 fc ff ff ff
typedef struct {
Elf32_Addr r_offset;
uint32_t r_info;
} Elf32_Rel;
/
/
显式加法重定位,重定位时需要计算的加数存储在 r_addend
typedef struct {
Elf32_Addr r_offset;
uint32_t r_info;
int32_t r_addend;
} Elf32_Rela;
/
/
隐式加法重定位,重定位处的汇编一般会是 e8 fc ff ff ff
typedef struct {
Elf32_Addr r_offset;
uint32_t r_info;
} Elf32_Rel;
/
/
显式加法重定位,重定位时需要计算的加数存储在 r_addend
typedef struct {
Elf32_Addr r_offset;
uint32_t r_info;
int32_t r_addend;
} Elf32_Rela;
PLT[
0
]: push GOT[
1
]
jmp GOT[
2
]
PLT[
1
]: __libc_start_main()
PLT[
2
]: jmp GOT[
4
]
push
0
jmp PLT[
0
]
...
GOT[
0
]: .dynmic 地址
GOT[
1
]: relor
GOT[
2
]: _dl_runtime_resolve()
GOT[
3
]: sys startup
GOT[
4
]: PLT[
2
]第二条指令地址
PLT[
0
]: push GOT[
1
]
jmp GOT[
2
]
PLT[
1
]: __libc_start_main()
PLT[
2
]: jmp GOT[
4
]
push
0
jmp PLT[
0
]
...
GOT[
0
]: .dynmic 地址
GOT[
1
]: relor
GOT[
2
]: _dl_runtime_resolve()
GOT[
3
]: sys startup
GOT[
4
]: PLT[
2
]第二条指令地址
gcc (Ubuntu
11.3
.
0
-
1ubuntu1
~
22.04
)
11.3
.
0
Linux null
5.15
.
0
-
67
-
generic
gcc (Ubuntu
11.3
.
0
-
1ubuntu1
~
22.04
)
11.3
.
0
Linux null
5.15
.
0
-
67
-
generic
int
print_hello() {
printf(
"hello PLT and GOT\n"
);
}
int
main() {
print_hello();
return
0
;
}
int
print_hello() {
printf(
"hello PLT and GOT\n"
);
}
int
main() {
print_hello();
return
0
;
}
gcc main.c
-
o test
-
save
-
temps
-
m32
-
g
-
Wl,
-
z,lazy
gcc main.c
-
o test
-
save
-
temps
-
m32
-
g
-
Wl,
-
z,lazy
objdump
-
M intel
-
d main.o
objdump
-
M intel
-
d main.o
00000000
<print_hello>:
0
:
53
push ebx
1
:
83
ec
14
sub esp,
0x14
4
: e8 fc ff ff ff call
5
<print_hello
+
0x5
>
9
:
81
c3
02
00
00
00
add ebx,
0x2
f:
8d
83
00
00
00
00
lea eax,[ebx
+
0x0
]
15
:
50
push eax
16
: e8 fc ff ff ff call
17
<print_hello
+
0x17
>
1b
:
83
c4
18
add esp,
0x18
1e
:
5b
pop ebx
1f
: c3 ret
00000000
<print_hello>:
0
:
53
push ebx
1
:
83
ec
14
sub esp,
0x14
4
: e8 fc ff ff ff call
5
<print_hello
+
0x5
>
9
:
81
c3
02
00
00
00
add ebx,
0x2
f:
8d
83
00
00
00
00
lea eax,[ebx
+
0x0
]
15
:
50
push eax
16
: e8 fc ff ff ff call
17
<print_hello
+
0x17
>
1b
:
83
c4
18
add esp,
0x18
1e
:
5b
pop ebx
1f
: c3 ret
00000024
<main>:
20
:
55
push ebp
21
:
89
e5 mov ebp,esp
23
:
83
e4 f0
and
esp,
0xfffffff0
26
: e8 fc ff ff ff call
27
<main
+
0x7
>
2b
: b8
00
00
00
00
mov eax,
0x0
30
: c9 leave
31
: c3 ret
00000024
<main>:
20
:
55
push ebp
21
:
89
e5 mov ebp,esp
23
:
83
e4 f0
and
esp,
0xfffffff0
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2023-3-8 22:45
被安和桥南编辑
,原因: 添加样例
上传的附件: