动态链接的秘密:PLT与GOT表的深度剖析

191 阅读4分钟

在Linux系统中,动态链接是程序运行的重要组成部分。而过程链接表(PLT)和全局偏移表(GOT)则是实现动态链接的关键结构。本文将深入探讨PLT与GOT表的工作原理,以及它们在动态库调用中的作用,同时还会涉及相关的安全风险与防护措施。

动态库调用流程解析

非首次调用(常规流程)

当程序并非首次调用动态库接口时,调用流程相对简单。以printf函数为例,其调用过程如下:

  1. 跳转到PLT表:代码中对printf的调用会被编译为printf@plt形式,即先跳转到PLT表中对应的条目。
  2. PLT表结构:PLT表是一个由汇编代码组成的数组,除索引0外,每个条目对应一个外部符号的调用。假设printf对应PLT表索引为n。
  3. 跳转到GOT表:PLT[n]的第一条指令是跳转到GOT表中对应的条目(假设对应GOT表索引为m)。
  4. 执行函数:此时GOT[m]已存储实际函数地址,直接跳转到动态库中的printf函数执行。
  5. 返回调用处:函数执行完成后返回原调用处,完成一次调用。

首次调用(延迟绑定机制)

Linux采用延迟绑定(Lazy Binding)机制,首次调用时GOT表中尚未填充实际函数地址。以下是详细的调用流程:

PLT条目的结构

printf对应的PLT[n]为例,其汇编结构如下(伪代码):

printf@plt:
    jmp *GOT[m]       ; 跳转到GOT表第m项(首次调用时指向本条目的下一条指令)
    push n            ; 将PLT索引n压栈作为参数
    jmp PLT[0]        ; 跳转到PLT[0]的桩代码

PLT[0]桩代码结构

PLT表索引0的特殊桩代码结构如下(伪代码):

PLT0:
    pushq [GOT+8]     ; 将GOT[1]中的link_map结构地址压栈
    jmp [GOT+16]      ; 跳转到GOT[2]指向的_dl_runtime_resolve函数
    nop               ; 内存对齐填充
    nop

地址解析过程

  1. 首次调用时:GOT[m]指向PLT[n]的第二条指令(push n)。
  2. 执行push n:将PLT索引压栈,再跳转到PLT[0]。
  3. 跳转到_dl_runtime_resolve:PLT[0]将GOT[1](link_map结构地址)压栈,然后跳转到GOT[2]指向的_dl_runtime_resolve函数。
  4. 符号解析_dl_runtime_resolve函数通过两个参数(link_map指针和PLT索引n)执行符号解析:
    • 根据PLT索引n找到.rela.plt重定位节中的对应条目(Elf64_Rela结构)。
    • 通过条目中的r_info字段定位.dynsym动态符号表中的符号信息(Elf64_sym结构)。
    • 利用符号信息中的st_name字段从.dynstr动态字符串表获取符号名(如"printf")。
    • 遍历link_map记录的已加载库,查找符号对应的实际地址(如libc.so中的printf地址)。
  5. 地址绑定:将解析得到的实际地址写入GOT[m],完成地址绑定。
  6. 执行实际函数:执行实际函数并返回,后续调用将直接使用GOT[m]中的地址(即常规流程)。

安全风险与防护

PLT/GOT机制虽然高效,但也存在潜在的安全风险。攻击者可以通过修改GOT表中的地址,将函数调用重定向到恶意代码,执行完成后再跳转回原流程,从而篡改程序行为。这种攻击方式在恶意软件和黑客攻击中较为常见。

针对此类风险,常用的防护手段是“加壳”。加壳工具可以对程序进行保护,增加逆向分析和篡改的难度。例如,Virbox Protector是一款非常优秀的加壳工具,它支持多种文件类型,能够有效提升程序的安全性。通过使用Virbox Protector,开发者可以为程序添加多层保护,防止恶意篡改和逆向工程。

总结

PLT与GOT表是Linux系统中动态库调用的核心机制,它们通过巧妙的设计实现了高效的符号解析和函数调用。然而,这种机制也带来了安全风险。通过使用像Virbox Protector这样的加壳工具,开发者可以有效保护程序的安全性,防止恶意篡改和逆向工程。希望本文能够帮助你更好地理解PLT与GOT表的工作原理,以及如何保护你的程序免受安全威胁。