《程序员的自我修养》- 动态链接库基本知识


本文使用下面的示例程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lib.h
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
// lib.c
#include <stdio.h>
void foobar(int i) {
printf("Printing from lib.so %d\n", i);
}
// program1.c
#include "lib.h"
int main() {
foobar(1);
return 0;
}

将lib.c使用gcc -fPIC -shared -o lib.so lib.c编译成动态链接库文件,并分别编译 program1和program2。

#动态链接库的内存映射

使用radare2调试program1,查看内存映射,我们得到如下结果:

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
[0x55671fb1b135]> dm=
map 132K - 0x00007fffdd00d000 ------- 0x00007fffdd02e000 rw- [stack]
map 12K - 0x00007fffdd1d2000 ------- 0x00007fffdd1d5000 r-- [vvar]
map 8K - 0x00007fffdd1d5000 ------- 0x00007fffdd1d7000 r-x [vdso]
map 4K - 0xffffffffff600000 ------- 0xffffffffff601000 r-x [vsyscall]
map 12K - 0x00007fb617f90000 ------- 0x00007fb617f93000 rw- unk0
map 148K - 0x00007fb617f93000 ------- 0x00007fb617fb8000 r-- /lib/x86_64-linux-gnu/libc-2.29.so
map 1.3M - 0x00007fb617fb8000 ------- 0x00007fb6180ff000 r-x /lib/x86_64-linux-gnu/libc-2.29.so
map 292K - 0x00007fb6180ff000 ------- 0x00007fb618148000 r-- /lib/x86_64-linux-gnu/libc-2.29.so
map 4K - 0x00007fb618148000 ------- 0x00007fb618149000 --- /lib/x86_64-linux-gnu/libc-2.29.so
map 12K - 0x00007fb618149000 ------- 0x00007fb61814c000 r-- /lib/x86_64-linux-gnu/libc-2.29.so
map 12K - 0x00007fb61814c000 ------- 0x00007fb61814f000 rw- /lib/x86_64-linux-gnu/libc-2.29.so
map 16K - 0x00007fb61814f000 ------- 0x00007fb618153000 rw- unk1
map 4K - 0x00007fb618164000 ------- 0x00007fb618165000 r-- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/lib.so
map 4K - 0x00007fb618165000 ------- 0x00007fb618166000 r-x /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/lib.so
map 4K - 0x00007fb618166000 ------- 0x00007fb618167000 r-- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/lib.so
map 4K - 0x00007fb618167000 ------- 0x00007fb618168000 r-- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/lib.so
map 4K - 0x00007fb618168000 ------- 0x00007fb618169000 rw- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/lib.so
map 8K - 0x00007fb618169000 ------- 0x00007fb61816b000 rw- unk2
map 4K - 0x00007fb61816b000 ------- 0x00007fb61816c000 r-- /lib/x86_64-linux-gnu/ld-2.29.so
map 124K - 0x00007fb61816c000 ------- 0x00007fb61818b000 r-x /lib/x86_64-linux-gnu/ld-2.29.so
map 32K - 0x00007fb61818b000 ------- 0x00007fb618193000 r-- /lib/x86_64-linux-gnu/ld-2.29.so
map 4K - 0x00007fb618193000 ------- 0x00007fb618194000 r-- /lib/x86_64-linux-gnu/ld-2.29.so
map 4K - 0x00007fb618194000 ------- 0x00007fb618195000 rw- /lib/x86_64-linux-gnu/ld-2.29.so
map 4K - 0x00007fb618195000 ------- 0x00007fb618196000 rw- unk3
map 4K - 0x000055671fb1a000 ##----- 0x000055671fb1b000 r-- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/program1
map 4K * 0x000055671fb1b000 -##---- 0x000055671fb1c000 r-x /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/program1
map 4K - 0x000055671fb1c000 --###-- 0x000055671fb1d000 r-- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/program1
map 4K - 0x000055671fb1d000 ----##- 0x000055671fb1e000 r-- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/program1
map 4K - 0x000055671fb1e000 -----## 0x000055671fb1f000 rw- /mac/Users/ctsinon/kali/pwn_work/learn/DynamicLinking/program1

我们可以看到刚刚编译的lib.so被映射进了内存中,查看映射的第一页,可以看到的确如此:

1
2
3
[0x55671fb1b135]> px @0x00007fb618164000
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7fb618164000 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............

然而我们可以确定,libc.so并不存在于program1中,那么首先要解决的问题是操作系统在映射内存空间时是从哪里找到libc.so的。

#so在哪里

首先应该了解的是,一个动态链接的程序中包含着一个.dynamic段,这个段中储存着下面的结构数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
Elf32_Off d_off;
} d_un;
} Elf32_Dyn;
typedef struct {
Elf64_Xword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;

其中,d_tag指出该元素的某种属性,根据d_tag的不同,结构的另一项可能是一个数值(d_val),也可能是一个地址(d_ptr),或者是一个偏移量(d_off) 根据ORCALE的Linker and Libraries Guide,我们可以看到对这一问题的解答,即Directories Searched by the Runtime Linker,主要分为以下几种情况:

  • 操作系统默认的动态链接库文件目录,对于32和64位程序来说,该目录分别是/lib, /usr/lib/lib/64, /usr/lib/64,也有可能是/lib32, /usr/lib32/lib, /usr/lib
  • 环境变量LD_LIBRARY_PATHLIBRARY_PATH中的路径
  • 链接时显式引用的so文件所在的路径,这些文件被记录在ELF文件呢的.dynamic段中,并且拥有DT_NEEDED的flag。可以使用readelf -d来查看这些数据。
  • 声明在.dynamic段中,拥有d_tag DT_RPATH的项。
  • 声明在.dynamic段中,拥有d_tag DT_RUNPATH的项。
  • /etc/ld.so.conf中的目录

注意:对于这些路径来说,优先级是 rpath > LD_LIBRARY_PATH > runpath > /etc/ld.so.conf > default ones 以program1为例:

1
2
3
4
5
$ readelf -d program1
Dynamic section at offset 0x2de8 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [./lib.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

让人好奇的是,readelf是怎么知道so库的文件名的,这就引入了一个新的段dynstr,这个段专门用于储存与动态链接有关的字符串。 我们分别查看这两个段的十六进制内容:

1
2
3
4
5
6
7
8
9
10
> # xxd -s 0x003d8 -l 256 program1
000003d8: 002e 2f6c 6962 2e73 6f00 5f49 544d 5f64 ../lib.so._ITM_d
000003e8: 6572 6567 6973 7465 7254 4d43 6c6f 6e65 eregisterTMClone
000003f8: 5461 626c 6500 5f5f 676d 6f6e 5f73 7461 Table.__gmon_sta
00000408: 7274 5f5f 005f 4954 4d5f 7265 6769 7374 rt__._ITM_regist
00000418: 6572 544d 436c 6f6e 6554 6162 6c65 0066 erTMCloneTable.f
00000428: 6f6f 6261 7200 6c69 6263 2e73 6f2e 3600 oobar.libc.so.6.
00000438: 5f5f 6378 615f 6669 6e61 6c69 7a65 005f __cxa_finalize._
00000448: 5f6c 6962 635f 7374 6172 745f 6d61 696e _libc_start_main
00000458: 0047 4c49 4243 5f32 2e32 2e35 0000 0000 .GLIBC_2.2.5....
1
2
3
4
5
6
7
8
9
[0x00002de8]> pf 2p8p8 d_tag d_val
0x00002de8 [0] {
d_tag : 0x00002de8 = (qword)0x0000000000000001
d_val : 0x00002df0 = (qword)0x0000000000000001
}
0x00002df8 [1] {
d_tag : 0x00002df8 = (qword)0x0000000000000001
d_val : 0x00002e00 = (qword)0x0000000000000056
}

可以看到,d_val的值正是相应的字符串在.dynstr段中相对段首的偏移量。

#so中的数据在哪里

so文件中少不了对数据或者函数的引用,在装载基址固定的情况下,只需要引用绝对地址即可,然而作为一个动态链接库文件,这样做会导致其灵活性降低不少,这意味着动态链接库文件不能假设其装载基地址。那么怎样的处理方式可以做到这一点呢。

#装载时重定位

我们已经了解过将两个或多个对象文件静态链接时链接器的处理方法:在基地址确定之后,查重定位表并将重定位表中提及的地址进行修正。同理,在装载动态链接库时也可以采用同样的做法。这样的做法称为装载时重定位,相应的,进行静态链接时的重定位被称为链接时重定位。(此技术已废弃)

#地址无关代码

不容忽视的是,装载时重定位的链接库文件由于直接对so内部的数据进行了修改,一个进程需要保有一份so文件的拷贝,这样一来通过映射使多个进程共享一份so文件的目的就又达不到了,解决这一问题的方法便是使用地址无关代码(PIC,Position-Independent Code)技术。 地址无关的代码技术可以分为下面几个类型来理解:

#模块内部调用或跳转

由于现代体系架构提供了相对地址调用的调用方式,此类代码本身就是地址无关的,因此不必进行处理。

#模块内部数据访问

这种类型的基本思想同样是数据相对当前指令的偏移固定,但需要分为两种情况:

  1. 32位下:由于32位缺乏PC相对寻址的寻址方式而且不能直接访问eip,需要间接得到当前PC的值再通过偏移计算出正确地址。用于得到当前PC位置的函数被称为__i686.get_pc_thunk.xx,其基本思路即将esp处的数据(call之后储存着eip的值)赋给相应的寄存器。
  2. 64位下:64位可以直接进行PC相对寻址,即操作数即偏移,因此可以利用偏移量直接访问数据。例如
1
2
3
1109:   c7 05 19 2f 00 00 01    movl   $0x1,0x2f19(%rip)        # 402c <a>
1110: 00 00 00
1113: 48 8b 05 be 2e 00 00 mov 0x2ebe(%rip),%rax # 3fd8 <b>

观察机器码可以发现,指令直接使用了数据所在地址相对rip的偏移量来进行寻址。

#模块间数据访问与函数调用

这个过程较为复杂,将会单独写一篇文章来介绍

#其他问题

#PIC 与 PIE

我们之前提到过用于使ELF文件装载基地址随机化的PIE技术(Position-Independent Executable),这可以看作针对可执行文件的PIC技术,它的直接作用是使可执行文件的数据和函数操作不受装载基地址的影响,因而可以实现随机化。

#executable 与 shared object

在实验的过程中,我们发现使用file查看program1时结果如下:

1
2
3
4
> # file program1
program1: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter
/lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4b76c146ab7fceb059654b0efed9f0e267db511f, for
GNU/Linux 3.2.0, not stripped

这意味着这个可执行文件开启了PIE。然而这可以认为是file的一个bug,因为开启PIE的executable文件和开启PIC的动态链接库文件结构相似,file还未对此做出恰当的区分。

#动态链接库文件只有一个副本吗

对于位置无关代码的so文件来说,由于代码段在装载时不需要做任何修改,因此多个进程可以将同一份so文件映射到自己的进程空间中。而对于动态链接库的数据段来说,为了避免冲突,只能使用进程之间相互隔离的副本(一般情况)。


← Prev MRCTF - 部分WP | BJDCTF 2nd - 部分WP Next →