0x01 什么是PLT和GOT
名稱:
- PLT : 程序鏈接表(PLT,Procedure Link Table)
- GOT : 重局偏移表(GOT, Global Offset Table)
緣由:
這緣起於動態鏈接,動態鏈接需要考慮的各種因素,但實際總結起來說兩點:
- 需要存放外部函數的數據段 —— PLT
- 獲取數據段存放函數地址的一小段額外代碼 —— GOT
如果可執行文件中調用多個動態庫函數,那每個函數都需要這兩樣東西,這樣每樣東西就形成一個表,每個函數使用中的一項。
存放函數地址的數據表,稱為全局偏移表(GOT, Global Offset Table),而那個額外代碼段表,稱為程序鏈接表(PLT,Procedure Link Table)。
內容:
舉個例子,對於一個函數,這里命名為common,其plt如下:
080482a0 <common@plt>: 80482a0: pushl 0x80496f0 80482a6: jmp *0x80496f4
...
第一句,pushl 0x80496f0,是將地址壓到棧上,也即向最終調用的函數傳遞參數。
第二句,jmp *0x80496f4,這是跳到最終的函數去執行,不過猜猜就能想到,這是跳到能解析動態庫函數地址的代碼里面執行。
0x80496f4屬於GOT表中的一項,進程還沒有運行時它的值是0x00000000,當進程運行起來后,它的值變成了0xf7ff06a0。
如果做更進一步的調試會發現這個地址位於動態鏈接器內,對應的函數是_dl_runtime_resolve。(相應的過程圖在下面貼出)
如果將PLT和GOT抽象起來描述,可以寫成以下的偽代碼:
plt[0]:
pushl got[1]
jmp *got[2]plt[n]: // n >= 1
jmp *got[n+2]// GOT前3項為公共項,第3項開始才是函數項,plt[1]對應的GOT[3],依次類推
push (n-1)*8
jmp plt[0]—————————————————————————————————————————
got[0] = address of .dynamic section
//本ELF動態段(.dynamic段)的裝載地址
got[1] = address of link_map object( 編譯時填充0)//本ELF的link_map數據結構描述符地址
got[2] = address of _dl_runtime_resolve function (編譯時填充為0)//_dl_runtime_resolve函數的地址
got[n+2] = plt[n] + 6 (即plt[n]代碼片段的第二條指令)
特點:
PLT表結構有以下特點:
PLT表中的第一項為公共表項,剩下的是每個動態庫函數為一項(當然每項是由多條指令組成的,jmp *0xXXXXXXXX這條指令是所有plt的開始指令)
每項PLT都從對應的GOT表項中讀取目標函數地址
GOT表結構有以下特點:
GOT表中前3個為特殊項,分別用於保存 .dynamic段地址、本鏡像的link_map數據結構地址和_dl_runtime_resolve函數地址;
但在編譯時,無法獲取知道link_map地址和_dl_runtime_resolve函數地址,所以編譯時填零地址,進程啟動時由動態鏈接器進行填充3個特殊項后面依次是每個動態庫函數的GOT表項
注意點:
以printf函數為例,三個問題:
- _dl_runtime_resolve是怎么知要查找printf函數的
- _dl_runtime_resolve找到printf函數地址之后,它怎么知道回填到哪個GOT表項
- 到底_dl_runtime_resolve是什么時候被寫到GOT表的
printf@plt>: jmp *0x80496f8 push $0x00 jmp common@plt
每個xxx@plt的第二條指令push的操作數都是不一樣的,它就相當於函數的id,動態鏈接器通過它就可以知道是要解析哪個函數了。
它倆的運行關系如下:
0x02 重定位
重定位分為以下三種:
- 鏈接重定位:將一個或多個中間文件(.o文件)通過鏈接器將它們鏈接成一個可執行文件。其中分為兩種情況:
- 如果是在其他中間文件中已經定義了的函數,鏈接階段可以直接重定位到函數地址
- 如果是在動態庫中定義了的函數,鏈接階段無法直接重定位到函數地址,只能生成額外的小片段代碼,也就是PLT表,然后重定位到該代碼片段
- 運行重定位:運行后加載動態庫,把動態庫中的相應函數地址填入GOT表,由於PLT表是跳轉到GOT表的,這就構成了運行時重定位
- 延遲重定位:只有動態庫函數在被調用時,才會進行地址解析和重定位工作,這時候動態庫函數的地址才會被寫入到GOT表項中,過程如下圖:
PLT屬於代碼段,在進程加載和運行過程都不會發生改變,PLT指向GOT表的關系在編譯時已完全確定,唯一能發生變化的是GOT表。
示例:
重定位時:
重定位后:
0X03 在PWN中的應用 —— ret2libc
應用場景:
在一些提供單獨 libc(版本號).so的pwn題中,大部分情況是要在這個so文件中尋找一些函數的偏移地址,而且大部分情況下,為了方便,只會使用已經在程序中出現的函數的got表中的實際地址,我們可以直接把這個so文件拖進IDA中進行尋找,
經典的例題如Jarvis oj上面的pwn level3,這里只給出較好的wp地址,可以看到,用read函數寫入,然后return已經使用過一次的write函數的plt地址,從而調用這個函數(之后的那個padding是系統call用來跳轉執行的下一個地址,可以deadbeef也可以換成想要執行的函數地址),繼而是對ret的這個write函數的參數進行輸入,其中第二個參數就是要輸出在顯示屏上的內容,因此我們填入write的got表地址,輸出write的真實地址,進而基地址就可以由 基地址= 真實地址-偏移地址算出。