延迟绑定机制详解

本文根据这篇文章跟踪调试,对延迟绑定的过程又有了新的感悟,写个文记录一下,建议看原文 https://www.anquanke.com/post/id/184099

lazy binding过程跟踪

针对动态链接会减速程序运行速度的现状,操作系统实现了延迟绑定(Lazy Binding)的技术:函数第一次被用到时才对函数进行绑定。
通过延迟绑定大大加快了程序的启动速度。而ELF 则使用了PLT(Procedure Linkage Table,过程链接表)的技术来实现延迟绑定。

在lazy binding中,got表项存储的东西

got: plt+6
got + 4: link_map
got + 8: _dl_runtime_resolve

_dl_runtime_resolve的源码:https://code.woboq.org/userspace/glibc/sysdeps/i386/dl-trampoline.S.html
可以看到_dl_runtime_resolve的核心在于call _dl_fixup

动态链接中最重要的结构应该是dynamic段,这个段里面保存了动态链接器所需要的基本信息。
比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。
strze不是,截图截手残了

是一个结构体数组,结构体的定义为:

typedef struct {
    Elf32_Sword     d_tag;
    union {
        Elf32_Word  d_val;
        Elf32_Addr  d_ptr;
    } d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];

Elf32_Dyn结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。
下面给出和延迟绑定相关的类型值的定义。

d_tag类型 	                              d_un的定义
#define DT_STRTAB 5 	动态链接字符串表的地址,d_ptr表示.dynstr的地址 (Address of string table)
#define DT_SYMTAB 6 	动态链接符号表的地址,d_ptr表示.dynsym的地址 (Address of symbol table)
#define DT_JMPREL 23 	动态链接重定位表的地址,d_ptr表示.rel.plt的地址 (Address of PLT relocs)
#define DT_RELENT 19 	单个重定位表项的大小,d_val表示单个重定位表项大小 (Size of one Rel reloc )
#define DT_SYMENT 11 	单个符号表项的大小,d_val表示单个符号表项大小 (Size of one symbol table entry )

如上图所示,可以看到字符串表.dynstr的地址为0x804822c,符号表.dynsym地址为0x80481cc,其单个符号表项的大小为16,重定位表.rel.plt的地址为 0x80482d8,其单个重定位表项的大小为8

.rel.plt重定位表中包含了需要重定位的函数的信息,其也是一个结构体数组,结构体Elf32_Rel定义如下,其中r_offset表示got表地址,即动态解析函数后真正的函数地址需要填入的地方。
r_info由两部分构成,r_info>>8表示该函数对应在符号表.dynsym中的下标,r_info&0xff则表示重定位类型。

typedef struct {
    Elf32_Addr        r_offset;
    Elf32_Word       r_info;
} Elf32_Rel;

我们通过readelf观察重定位表

 ┌─────(root)─────(/ctf/work/ctf-wiki/ret2dl/32) 
 └> $ readelf -r demo
Relocation section '.rel.plt' at offset 0x2d8 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804a010  00000207 R_386_JUMP_SLOT   00000000   __stack_chk_fail@GLIBC_2.4
0804a014  00000407 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0

可以看到重定位表.rel.plt为一个Elf32_Rel数组,demo程序中该数组包含三个元素,第一个是read的重定位表项Elf32_Rel结构体,第二个是__stack_chk_fail,第三个是__libc_start_main。
read的重定位表r_offset为0x0804a00c,为read的got地址,即在动态解析函数完成后,将read的函数地址填入到r_offset为0x0804a00c中。
r_info为0x00000107表示read函数的符号表为.dynsym数组中的0x00000107>>8(即0x1)个元素,它的类型为0x00000107&0xff(即0x7)对应为R_386_JUMP_SLOT类型。

接着我们去看符号表.dynsym节,它也是一个结构体Elf32_Sym数组,其结构体的定义如下:

typedef struct
{
  Elf32_Word    st_name; //符号名,是相对.dynstr起始的偏移
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info; //对于导入函数符号而言,它是0x12
  unsigned char st_other;
  Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

其中st_name指向的是函数名称在.dynstr表中的偏移。在dynamic段中我们知道了符号表.dynsym地址为0x80481cc,查看它的值:

pwndbg> x/20wx 0x80481cc
0x80481cc:      0x00000000      0x00000000      0x00000000      0x00000000
0x80481dc:      0x0000002b      0x00000000      0x00000000      0x00000012
0x80481ec:      0x0000001a      0x00000000      0x00000000      0x00000012
0x80481fc:      0x00000042      0x00000000      0x00000000      0x00000020
0x804820c:      0x00000030      0x00000000      0x00000000      0x00000012

以及使用readelf -s查看符号表的内容:

从重定位表.rel.plt中,我们知道了read的r_info>>8为0x1,即read的符号表项对应的是.dynsym第二个元素,果然可以看到.dynsym第一个元素为read函数的Elf32_Sym结构体
可以看到它的st_name对应的是0x0000002b,即read字符串应该在.dynstr表偏移为0x2b的地方,由dynamic我们知道了.dynstr表的地址为地址为0x804822c
那么果然如此,0x804822c+0x2b的地方存储着”read“字符串

到这里似乎对read函数的解析过程有了一个简单的了解:

1,可以先通过dynamic段获取各个表的地址,包括有字符串表.dynstr的地址为0x804822c,
符号表.dynsym地址为0x80481cc,其单个符号表项的大小为16,
重定位表.rel.plt的地址为 0x80482d8,其单个重定位表项的大小为8。
2,read函数为.rel.plt表中的第一个元素,定位它的重定位表项,知道了read函数的r_offset为0x0804a00c,
以及它在符号表中的下标为0x000001,它的类型为0x7,R_386_JUMP_SLOT。
3,由0x000001知道了read函数的符号表是.dynsym第二个元素,获取到该结构体,
得到了它对应的st_name对应的是0x0000002b,即获取了read字符串应该在.dynstr表偏移为0x2b的地方。
4,最后调用函数解析匹配read字符串所对应的函数地址,将其填至r_offset为0x0804a00c,即read的got地址中。

回过头继续看_dl_runtime_resolve函数,其中的_dl_fixup的源码在下面这个文件里

延迟绑定技术总结

第一次call func@plt时

// plt表项内容
jump *(func@got)   --->    got表项的值为plt+6的地址,即push n的地址
push n     // 将需要延迟绑定的符号在重定位表中的index压栈
jmp PLT0

上面n为.rel.plt表中的偏移
PLT0的指令:

// PLT0指令
push *(got + 4)      // 存储的是link_map的地址
jmp *(got + 8)       // 存储的是_dl_runtime_resolve函数地址

_dl_runtime_resolve会调用_dl_fixup函数,其功能:

1. 从link_map获取字符串表.dynstr      符号表.dynsym      重定位表.rel.plt的地址
2.通过(第二个参数)偏移reloc_arg(push n)+.rel.plt的地址获取函数对应的重定位结构的位置,继而获取r_offset和r_info
3.根据.dynsym以及r_info>>8获取符号结构体,得到st_name (.dynstr + offset),即函数名相对于.dynstr的偏移量
4.拿到函数名后取libc中匹配函数名,找到相应函数并将地址填回got表,绑定完成

流程图

一个实例

posted @ 2020-11-07 11:06  lemon想学二进制  阅读(1154)  评论(0编辑  收藏  举报