動態鏈接GOT與PLT
鏈接一共分為兩種,靜態鏈接和動態鏈接,其中動態鏈接的相關理論是支撐pwn的基礎,非常重要。
動態鏈接庫
在我們寫程序的過程中,不會自己實現所有的功能,一般情況下會調用我們所需要的系統庫和第三方庫來實現我們的功能。
舉個栗子
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[]) {
char buf[32];
strncpy(buf, "Hello, World\n", 32);
printf("%s",buf);
}
編譯成二進制文件后查看symbol

可以看到printf和strncpy函數都是未定義的,這里的”未定義”其實是為了支持動態鏈接的功能的。
通過objdump命令查看相關函數的匯編反匯編模塊,objdump -D "file name" | less

可以看到,首先進行jmpq操作跳轉到相關代碼段,對應的代碼段是為了給“地址無關代碼”做動態地址重定位用的。我們提過,動態鏈接庫可以映射到不同進程的不同的虛擬地址空間,所以屬于“地址無關代碼”,鏈接器把對這個函數的調用代碼跳轉到程序運行時動態裝載地址。
linux提供了一個很方便的查看二進制文件依賴的動態鏈接庫命令ldd

總結一下鏈接器需要對動態鏈接庫需要做的最基本的事情:
- 鏈接庫在將目標文件鏈接成可執行文件的時候如果發現某一個變量或者函數在目標文件中找不到,會按照 gcc 預定義的動態庫尋找路徑尋找動態庫中定義的變量或者函數。
- 如果鏈接庫在某一個動態鏈接庫中找到了該變量或者函數定義,鏈接庫首先會把這個動態鏈接庫寫到可執行文件的依賴庫中,然后生成這個當前變量或者函數的代理 symbol.
- 在偏移表代碼中生成真正的動態跳轉指令,并且在庫函數(比如strncpy,printf)代理symbol中跳轉到中相應的偏移位置。
動態鏈接的程序的啟動過程

依靠上面的流程圖,我們總結一下當我們bash運行一個程序的時候,linux做了哪些事情
- 首先 bash 進行 fork 系統調用,生成一個子進程,接著在子進程中運行 execve 函數指定的 elf 二進制程序( Linux中執行二進制程序最終都是通過 execve 這個庫函數進行的),execve 會調用系統調用把 elf 文件 load 到內存中的代碼段(
_text)中。 - 如果有依賴的動態鏈接庫,會調用動態鏈接器進行庫文件的地址映射,動態鏈接庫的內存空間是被多個進程共享的。
- 內核從 elf 文件頭得到
_start的地址,調度執行流從_start指向的地址開始執行,執行流在_start執行的代碼段中跳轉到libc中的公共初始化代碼段__libc_start_main,進行程序運行前的初始化工作。 __libc_start_main的執行過程中,會跳轉到_init中全局變量的初始化工作,隨后調用我們的main函數,進入到主函數的指令流程。
動態鏈接(Dynamic Linking)
- 動態鏈接
- 一種運行時才會加載和鏈接程序所依賴的共享庫技術
- Linux最常見的共享庫是libc
- 重定位(Relocations)
- 指二進制文件中的待填充項
- 鏈接器在鏈接時填充,例如鏈接多個目標文件時,修正相互引用的函數、變量地址
- 動態連接器在運行時填充,例如動態解析庫函數(printf)
- 指二進制文件中的待填充項
- 動態鏈接中的延遲綁定
- 外部函數的地址在運行時才會確定
- 外部函數符號通常在首次調用時才會被解析
- 外部變量不使用延遲綁定機制
GOT表(Global Offset Table)
- GOT表常常用于存放外部庫函數地址(或外部變量,例如printf)
- GOT表項初始狀態指向一段PLT(過程鏈接表,Procedure Linkage Table)代碼
- 當庫函數被首次調用,真正的函數地址會被解析并填入相應的GOT表項
- 每個外部函數均有一段PLT(過程鏈接表,Procedure Linkage Table)代碼,用于跳轉到相應GOT表項中存儲的地址
舉個栗子
在gdb中觀察延遲綁定(具體gdb使用操作在后續章節中會有詳細教程)

在PC<+37>可以看到call <puts@plt>在調用動態庫之前下一個斷點,隨后觀察整個過程

單步執行call <puts@plt> stepi,跳轉到相應代碼段。跳轉到的是puts函數的plt代碼,首先是jump *puts@got -> puts@plt+6。GOT表項初始化為puts@plt+6,首次調用會執行_dl_runtime_resolve,執行完畢后GOT表項內的值就會填充正確的puts函數地址。
后續我們對puts函數的調用都無需再次解析,可以直接找到相應代碼。
- GOT表位于.got和.got.plt Ssection
- .got Section 存放外部全局變量的GOT表,非延遲綁定
- .got.plt Section 存放外部函數的GOT表,例如printf,采用延遲綁定。
| .got.plt(解析前) | .got.plt(解析后) |
|---|---|
| .dynamic section地址 | .dynamic section地址 |
| link_map地址 | link_map地址 |
| _dl_runtime_resolve | _dl_runtime_resolve |
| puts@plt + 6 | puts |
- .got.plt前三項有特殊含義,從第四項開始保存引用的各個外部函數的GOT表項:
- 第一項保存的是.dynamic section的地址(為動態鏈接提供信息,例如符號表、字符串表等)
- 第二項保存的是link_map結構地址(鏈表,包含所有加載的共享庫信息)
- 第三項保存了_dl_runtime_resolve函數的地址(用于解析外部函數符號的函數,解析完成后會直接執行該函數)

該圖片引用自長亭科技
.plt Section中存放所有外部函數對應的PLT代碼
總結一下
圖片引用自長亭科技
例如,我們要首次調用一個新的函數foo(),從call指令跟進去進入foo@plt代碼區域,接著jmp *(foo@GOT)跳轉到.got.plt中的foo@plt+6,實際上指向push index。后續會引導去執行_dl_runtime_resolve,將GOT表的值修復成真實的foo()函數地址,后續調用foo()的話會直接跳轉到相應的代碼段執行該函數的功能。
參考