动态链接

计算机程序链接时分两种方法:静态链接和动态链接。
静态链接在链接时将一切目标文件中的代码、数据等Section都组装到可履行文件傍边,并将代码中运用到的外部符号(函数、变量)都进行了重定位。因而在履行时不需求依赖其他外部模块即可履行,而且能够获得更快的发动时间和履行速度。然而静态链接的方法缺陷也很明显:

  • 模块更新困难。假如依赖的外部函数有着严峻的bug,那么不得不与修复的外部模块重新链接生成新的可履行文件。
  • 对磁盘和内存的浪费十分严峻。每一个可履行文件假如都包括C语言静态库,那么对于 /usr/bin 目录下上千个可履行文件,最后造成的浪费是不可想象的。
    动态链接将程序模块彼此切割开来,而不再将它们静态的链接在一起,等到程序要运行时才链接。尽管比较于静态链接由于需求在运行时进行链接带来了一定的功能损耗,但其能够有效的节省内存以及动态更新,因而有着广阔的运用。

推迟绑定(PLT/GOT 表)

在动态链接下,程序模块之间包括大量的符号引证,所以在程序开端履行前,动态链接会消耗不少时间用于处理模块间的符号引证的查找和重定位。在一个程序中,并非一切的逻辑都会走到,或许直到程序履行完结,很多符号(全局变量或函数)都并未被履行(比如一些过错分支)。因而假如在程序履行前将一切的外部符号都进行链接,无疑是一种功能浪费,同时也极大地拖累了发动速度。所以 ELF 采用了一种推迟绑定(Lazy Binding)的做法,思维也比较简单,当外部符号第一次运用时进行绑定(符号查找、重定位等),第2次运用时直接运用第一次符号绑定的结果。

站在编译器的视点来看,由于编译器在链接阶段无法得知外部符号的地址,因而其在编译时发现有对外部符号的引证,将生成一小段代码,用于外部符号的重定位。
ELF 运用 PLT(Procedure Linkage Table 进程链接表) 和 GOT(Global Offset Table 全局偏移表) 来完结推迟绑定:

  • PLT表:编译器生成的用于获取数据段中外部符号地址的一小段代码组成的表格。它使得代码能够方便地拜访同享的符号。对于每一个引证的同享符号,PLT 表都会有一个对应的条目。这些条目用于管理和重定位动态链接的符号。
  • GOT表:寄存外部符号地址的数据段。

为什么需求 PLT/GOT 表

在不熟悉现代操作系统对于内存的拜访操控权限的情况下,咱们或许会有疑问:

  • 只经过 PLT 表无法完结推迟绑定吗?
  • PLT 表重定位拿到外部符号的地址后,再次拜访时跳转到对应的地址不行吗?
    这儿首要有两个原因。

代码段拜访权限的约束

一般来说,代码段:可读、可履行;数据段:可读、可写。PLT 表项进行重定位后,要使得下次拜访外部符号时直接跳转到重定位后的地址,需求对代码段进行修改。然而代码段是没有写权限的。已然代码段没有写权限而数据段是可写的,那么在代码段中引证的外部符号,能够在数据段中增加一个跳板:让代码段先引证数据段中的内容,然后在重定位时,把外部符号重定位的地址填写到数据段中对应的方位。这个进程正好对应 PLT/GOT 表的用处。
以下为一个根本示意图,实践的 PLT/GOT 流程更为杂乱。

+------------------+     +-------------------+     +-----------------+     +---------------------+
|                  |     |                   |     |                 |     |                     |
|  printf_func     |  +--+-> printf@plt      |     |  printf@got     | +---+--> f73835f0<printf> |
|                  |  |  |                   |     |                 | |   |                     |
|  call printf@plt +--+  |   jmp *printf@got-+-----+-> 0xf7e835f0----+-+   |                     |
|                  |     |                   |     |                 |     |                     |
+------------------+     +-------------------+     +-----------------+     +---------------------+
   可履行文件                    PLT 表                    GOT 表                  glibc中的printf

同享内存的考虑

即便能够对代码段进行修改,由于 PLT 代码片段是在一个同享目标内,由于代码段被修改了,就无法完结一切进程同享同一个同享目标。而动态库的首要优点之一是多个进程同享同一个同享目标的代码段,拥有数据段的独立副本,从而节省内存空间。为了处理这个问题,PLT/GOT 表的运用变得必要。经过在数据段中增加一个全局偏移表(GOT),在程序运行时进行动态重定位,从而完结多个进程同享同一个同享目标的代码段,而数据段仍然坚持独立副本。

PLT/GOT 表作业原理

概述

当程序要调用一个外部函数时,它会首要跳转到 PLT 表中的对应条目。当 PLT 表中的条目被调用时,它会首要检查 GOT 表中是否已经存在该函数的地址。假如存在,PLT 将直接跳转到该地址;不然 PLT 将调用动态链接器 (dynamic linker)寻觅该函数的地址,并将该地址填充到 GOT 表中。
当函数的地址被填充到 GOT 表中后,下一次调用该函数时,PLT 将直接跳转到该地址,而不需求再次调用动态链接器。这个进程中,GOT 表充当了一个缓存,能够防止重复调用动态链接器,从而提高程序的履行功率。
为了更好地了解 PLT/GOT 的作业原理,下面是一个示意图:

  第一次对外部符号进行调用            第2次对同一外部符号进行调用
┌──────────────────────┐          ┌───────────────────┐
│     External Func    │          │ External Func Addr│
└──────────────────────┘          └───────────────────┘
             │                              │
             ▼                              ▼
┌──────────────────────┐          ┌───────────────────┐
│       PLT Stub       │          │      PLT Stub     │
└──────────────────────┘          └───────────────────┘
             │                              │
             ▼                              ▼
┌──────────────────────┐          ┌───────────────────┐
│    GOT Entry Addr    │          │   GOT Entry Addr  │
└──────────────────────┘          └───────────────────┘
             │                              │
             ▼                              ▼
┌──────────────────────┐          ┌───────────────────┐
│  Dynamic Linker Call │          │ External Func Addr│
└──────────────────────┘          └───────────────────┘
             │                              │
             ▼                              ▼
┌──────────────────────┐          ┌───────────────────┐
│   Update GOT Entry   │          │      Call Func    │
└──────────────────────┘          └───────────────────┘
             │           
             ▼                              
┌──────────────────────┐ 
│  External Func Addr  │    
└──────────────────────┘          
             │                              
             ▼                              
┌──────────────────────┐ 
│       Call Func      │
└──────────────────────┘         

实践上 ELF 将 GOT 拆分成了两个表: .got 和 .got.plt 。其间:

  • .got 用来保存全局外部变量的引证地址
  • .got.plt 用来保存外部函数引证的地址。
    咱们这儿论述的默认都是指的外部函数调用的流程。

作业流程

由推迟绑定的根本思维能够知道,第2次运用同享符号时不会再次进行重定位。那么 GOT 表是怎么判别是否是第一次拜访的同享符号的呢?
一种惯例的思维便是同享符号对应的 GOT 表项设置一个特殊的初始值,由于重定位后会更新同享符号的地址,因而判别 GOT 表项中是否是这个初始值,是的话即为第一次拜访。那 ELF 文件中实践是怎么处理的呢?
对于一个 PLT 表项,其包括三条指令,格式如下:

addr xxx@plt
    jmp    *(xxx@got)
    push   offset
    jmp    *(_dl_runtime_resolve)

指令一

指令一跳转到一个地址,这个地址的值从对应的 GOT 表项中读取。这条 GOT 表项初始存储的是 PLT 表项第二条指令的地址。因而实践相当于直接顺序履行第二条指令。当重定位后 GOT 表项中存储的地址会被更新为外部符号的实践地址。因而后续拜访这个外部符号时,指令一将直接跳转到对应的外部符号地址。经过这种巧妙的方法在推迟初始化的时分防止了每次都进行重定位。

+-------------------------------+    +-----------+       +--------------+
|                         1     |    |           |       |              |
| addr puts@plt   +-------------+----> puts@got  |       |  <printf>    |
|                 |             |    |           |       |              |
|    jpm *(puts@got)            |    |           |       |              |
|                        2      |    |           |   5   |              |
|    push offset<------------------+----+        +------>|              |
|        |3                     |    |           |       |              |
|        v                      |    +--+--------+       +--------------+
|    jmp *(__dl_runtime_resolve)|       |
|                            |  |  4    |
|                            +--+-------+
|                               | update addr
|                               |
+-------------------------------+

指令二

指令二会压入一个操作数,这个操作数实践是外部符号的标识符id,动态链接器经过它来区别要解析哪个外部符号以及解析完后需求更新哪个 GOT 表项的数据。这个操作数通常是这个函数在 .rel.plt 的下标或地址,经过 readelf -r elf_file 能够检查 .rel.plt 信息。

指令三

指令三会跳转到一个地址,这个地址是动态链接做符号解析和重定位的公共进口。由于一切的外部函数都需求经历这一进程,因而被提炼为公共函数,而非每个 PLT 表项都有一份重复指令。实践上这个公共进口指向 _dl_runtime_resolve,其完结符号解析和重定向作业后,将外部函数的真实地址填到对应的 GOT 表项中。

事例分析

这儿以 32 位 ELF 可履行文件进行分析。

#include <stdio.h>
int main(){
    printf("Hello Worldn");
    printf("Hello World Againn");
    return 0;
}
# 编译
gcc -Wall -g -o test.o -c test.c -m32
# 链接,增加 -z lazy,这样在 gdb 调试时才能够看到推迟绑定的进程
gcc -o test test.o -m32 -z lazy

检查 test 可履行文件的汇编代码 objdump -d test,其输出的部分结果如下
PLT 表的汇编如下:

Disassembly of section .plt:
00001030 <__libc_start_main@plt-0x10>:
    1030:	ff b3 04 00 00 00    	push   0x4(%ebx)
    1036:	ff a3 08 00 00 00    	jmp    *0x8(%ebx)
    103c:	00 00                	add    %al,(%eax)
	...
00001040 <__libc_start_main@plt>:
    1040:	ff a3 0c 00 00 00    	jmp    *0xc(%ebx)
    1046:	68 00 00 00 00       	push   $0x0
    104b:	e9 e0 ff ff ff       	jmp    1030 <_init+0x30>
00001050 <puts@plt>:
    1050:	ff a3 10 00 00 00    	jmp    *0x10(%ebx)
    1056:	68 08 00 00 00       	push   $0x8
    105b:	e9 d0 ff ff ff       	jmp    1030 <_init+0x30>
Disassembly of section .plt.got:
00001060 <__cxa_finalize@plt>:
    1060:	ff a3 18 00 00 00    	jmp    *0x18(%ebx)
    1066:	66 90                	xchg   %ax,%ax

代码段 main 部分的汇编如下:

0000119d <main>:
    119d:	8d 4c 24 04          	lea    0x4(%esp),%ecx
    11a1:	83 e4 f0             	and    $0xfffffff0,%esp
    11a4:	ff 71 fc             	push   -0x4(%ecx)
    11a7:	55                   	push   %ebp
    11a8:	89 e5                	mov    %esp,%ebp
    11aa:	53                   	push   %ebx
    11ab:	51                   	push   %ecx
    11ac:	e8 ef fe ff ff       	call   10a0 <__x86.get_pc_thunk.bx>
    11b1:	81 c3 4f 2e 00 00    	add    $0x2e4f,%ebx
    11b7:	83 ec 0c             	sub    $0xc,%esp
    11ba:	8d 83 08 e0 ff ff    	lea    -0x1ff8(%ebx),%eax
    11c0:	50                   	push   %eax
    11c1:	e8 8a fe ff ff       	call   1050 <puts@plt>
    11c6:	83 c4 10             	add    $0x10,%esp
    11c9:	83 ec 0c             	sub    $0xc,%esp
    11cc:	8d 83 14 e0 ff ff    	lea    -0x1fec(%ebx),%eax
    11d2:	50                   	push   %eax
    11d3:	e8 78 fe ff ff       	call   1050 <puts@plt>
    11d8:	83 c4 10             	add    $0x10,%esp
    11db:	b8 00 00 00 00       	mov    $0x0,%eax
    11e0:	8d 65 f8             	lea    -0x8(%ebp),%esp
    11e3:	59                   	pop    %ecx
    11e4:	5b                   	pop    %ebx
    11e5:	5d                   	pop    %ebp
    11e6:	8d 61 fc             	lea    -0x4(%ecx),%esp
    11e9:	c3                   	ret    

检查 test 可履行文件的重定位信息 readelf -r test,其输入部分如下:

Relocation section '.rel.dyn' at offset 0x384 contains 8 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00003ef4  00000008 R_386_RELATIVE   
00003ef8  00000008 R_386_RELATIVE   
00003ff8  00000008 R_386_RELATIVE   
00004018  00000008 R_386_RELATIVE   
00003fec  00000206 R_386_GLOB_DAT    00000000   _ITM_deregisterTM[...]
00003ff0  00000306 R_386_GLOB_DAT    00000000   __cxa_finalize@GLIBC_2.1.3
00003ff4  00000506 R_386_GLOB_DAT    00000000   __gmon_start__
00003ffc  00000606 R_386_GLOB_DAT    00000000   _ITM_registerTMCl[...]
Relocation section '.rel.plt' at offset 0x3c4 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0000400c  00000107 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.34
00004010  00000407 R_386_JUMP_SLOT   00000000   puts@GLIBC_2.0

经过 gdb 调试,在履行 printf("Hello Worldn") 时能够看到其调用了 call 0x56556050 <puts@plt>

─── Output/messages ─────────────────────────────────────────────────────────────────────────────────────────────
0x565561c1	4	    printf("Hello Worldn");
─── Assembly ────────────────────────────────────────────────────────────────────────────────────────────────────
 0x565561ac  main+15 call   0x565560a0 <__x86.get_pc_thunk.bx>
 0x565561b1  main+20 add    $0x2e4f,%ebx
!0x565561b7  main+26 sub    $0xc,%esp
 0x565561ba  main+29 lea    -0x1ff8(%ebx),%eax
 0x565561c0  main+35 push   %eax
 0x565561c1  main+36 call   0x56556050 <puts@plt>
 0x565561c6  main+41 add    $0x10,%esp
 0x565561c9  main+44 sub    $0xc,%esp
 0x565561cc  main+47 lea    -0x1fec(%ebx),%eax
 0x565561d2  main+53 push   %eax
─── Breakpoints ─────────────────────────────────────────────────────────────────────────────────────────────────
[1] break at 0x565561b7 in test.c:4 for main hit 1 time
─── Expressions ──────────────────────────────────────────────────────────────────────────────────────────────────
─── History ──────────────────────────────────────────────────────────────────────────────────────────────────────
─── Memory ───────────────────────────────────────────────────────────────────────────────────────────────────────
─── Registers ────────────────────────────────────────────────────────────────────────────────────────────────────
eax 0x56557008          ecx 0xffffcf00             edx 0xffffcf20               ebx 0x56559000          
esp 0xffffced0          ebp 0xffffcee8         esi 0xffffcfb4
edi 0xf7ffcb80          eip 0x565561c1          eflags [ PF AF SF IF ]           cs 0x00000023           
ss 0x0000002b           ds 0x0000002b          es 0x0000002b                    
fs 0x00000000           gs 0x00000063
─── Source ───────────────────────────────────────────────────────────────────────────────────────────────────────
~
~
 1  #include <stdio.h>
 2  
 3  int main(){
!4      printf("Hello Worldn");
 5      printf("Hello World Againn");
 6  
 7      return 0;
 8  }
─── Stack ──────────────────────────────────────────────────────────────────────────────────────────────────────────
[0] from 0x565561c1 in main+36 at test.c:4
─── Threads ────────────────────────────────────────────────────────────────────────────────────────────────────────
[1] id 5467 name test from 0x565561c1 in main+36 at test.c:4

disassemble 0x56556050 检查一下 puts@plt 中的内容,其包括三条指令。

>>> disassemble 0x56556050
Dump of assembler code for function puts@plt:
   0x56556050 <+0>:	jmp    *0x10(%ebx)
   0x56556056 <+6>:	push   $0x8
   0x5655605b <+11>:jmp    0x56556030
End of assembler dump.

指令一履行了 jmp *0x10(%ebx),其表示跳转到一个地址,地址值为存储在 ebx 寄存器中的值加上 0x10。
经过 info registers 检查寄存器的值为 0x56559000 加上 0x10 后最终地址为 0x56559010。经过 x 0x56559010 检查这个地址的内容为 0x56556056,这个地址也即 puts@plt 中第二条指令的方位。

>>> info registers
eax            0x56557008          1448439816
ecx            0xffffcf00          -12544
edx            0xffffcf20          -12512
ebx            0x56559000          1448448000
esp            0xffffced0          0xffffced0
ebp            0xffffcee8          0xffffcee8
esi            0xffffcfb4          -12364
edi            0xf7ffcb80          -134231168
eip            0x565561c1          0x565561c1 <main+36>
eflags         0x296               [ PF AF SF IF ]
cs             0x23                35
ss             0x2b                43
ds             0x2b                43
es             0x2b                43
fs             0x0                 0
gs             0x63                99
>>> x 0x56559010
0x56559010 <puts@got.plt>:	0x56556056

指令二压入 printf 的标识符。

指令三跳转到一个地址为 0x56556030,这个地址为动态链接器做符号解析和重定位的进口。其与 puts@plt 地址 0x56556050 相差 0x20 ,而这个数值正好等于汇编代码中 00001030 <__libc_start_main@plt-0x10>:00001050 <puts@plt>: 的差值。
对于 _dl_runtime_resolve 的履行进程咱们不去探求,其在符号解析和重定位结束后会根据指令二压入的操作数标识符更新 GOT 表项的地址。

>>> x /5i 0x56556030
=> 0x56556030:	push   0x4(%ebx)
   0x56556036:	jmp    *0x8(%ebx)
   0x5655603c:	add    %al,(%eax)
   0x5655603e:	add    %al,(%eax)
   0x56556040 <__libc_start_main@plt>:	jmp    *0xc(%ebx)

根据 PLT/GOT 机制进行 hook

测试程序

创立一个同享库 libtest.so,由 test.htest.c 组成。

# 编译生成 libtest.so
gcc test.h test.c -fPIC -shared -o libtest.so
// test.h
#ifndef TEST_H
#define TEST_H 1
#ifdef __cplusplus
extern "C" {
#endif
void say_hello();
#ifdef __cplusplus
}
#endif
#endif
// test.c
#include <stdlib.h>
#include <stdio.h>
void say_hello()
{
    char *buf = malloc(1024);
    if(NULL != buf)
    {
        snprintf(buf, 1024, "%s", "hellon");
        printf("%s", buf);
    }
}

创立一个测试程序 main,其调用了 libtest.so 中的函数。

# 编译生成履行文件
gcc main.c -L. -ltest -o main
# 增加 libtest.so 路径,使so可被动态链接
export LD_LIBRARY_PATH=/path/to/libtest.so
# 检查是否动态链接成功
ldd main
# linux-vdso.so.1 (0x00007fff596fc000)
# libtest.so => ./libtest.so (0x00007f1d9f61c000)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1d9f200000)
# /lib64/ld-linux-x86-64.so.2 (0x00007f1d9f628000)
// main
#include <test.h>
int main()
{
    say_hello();
    return 0;
}

履行的目标为对 libtest.so 同享库的 malloc 函数进行 hook 操作,替换成咱们自定义的一个 my_malloc 完结。
由上述 PLT/GOT 表作业原理 可知, libtest.so 调用 malloc 时会进行重定向操作找到 malloc 的地址进行调用。因而咱们只需求更改 got 表中 malloc 的地址指向,指向咱们完结的 my_malloc 地址即可完结 hook。

基地址

根据基址的符号偏移地址能够直接经过 readelf -r elf_file 指令检查 .rel.plt 中的信息确认。
我的履行环境的 libtest.so 中的 malloc 偏移地址为 0x4028

~/Documents/ProgramDesign/test_hook> readelf -r libtest.so
Relocation section '.rela.dyn' at offset 0x4a8 contains 7 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003e10  000000000008 R_X86_64_RELATIVE                    1150
000000003e18  000000000008 R_X86_64_RELATIVE                    1110
000000004030  000000000008 R_X86_64_RELATIVE                    4030
000000003fe0  000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTM[...] + 0
000000003fe8  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0  000600000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCl[...] + 0
000000003ff8  000700000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x550 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000004018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000004020  000300000007 R_X86_64_JUMP_SLO 0000000000000000 snprintf@GLIBC_2.2.5 + 0
000000004028  000500000007 R_X86_64_JUMP_SLO 0000000000000000 malloc@GLIBC_2.2.5 + 0
#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include "test.h"
#define PAGE_SIZE getpagesize()
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)
void *my_malloc(size_t size)
{
    printf("%zu bytes memory are allocated by libtest.son", size);
    return malloc(size);
}
void hook()
{
    char       line[512];
    FILE      *fp;
    uintptr_t  base_addr = 0;
    uintptr_t  addr;
    //find base address of libtest.so
    if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
    while(fgets(line, sizeof(line), fp))
    {
        if(NULL != strstr(line, "libtest.so") &&
           sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
            break;
    }
    fclose(fp);
    if(0 == base_addr) return;
    //the absolute address
    addr = base_addr + 0x4028;
    //add write permission
    mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
    //replace the function address
    *(void **)addr = my_malloc;
    //clear instruction cache
    __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}
int main()
{
    hook();
    say_hello();
    return 0;
}