深入理解GOT表和PLT表

0x01 前言

操作系統通常使用動態鏈接的方法來提高程序運行的效率。
在動態鏈接的情況下,程序加載的時候並不會把鏈接庫中所有函數都一起加載進來,而是程序執行的時候按需加載,如果有函數並沒有被調用,那麼它就不會在程序生命中被加載進來。
這樣的設計就能提高程序運行的流暢度,也減少了內存空間。而且現代操作系統不允許修改代碼段,只能修改數據段,那麼GOT表與PLT表就應運而生。

0x02 初探GOT表和PLT表

我們先簡單看一個例子
在這裏插入圖片描述
我們跟進一下scanf@plt

會發現,有三行代碼

jmp 一個地址
push 一個值到棧裏面
jmp 一個地址

看函數的名字就可以知道這是scanf函數的plt表,先不着急去了解plt是做什麼用的,我們繼續往下看
我們先看一下第一個jmp是什麼跳到哪裏
在這裏插入圖片描述
其實這是got表對應函數的got表,而且我們會發現0x201020的值是壓棧命令的地址,其他地方爲0,此時就想問:
一、got表與plt表有什麼意義,爲什麼要跳來跳去?
二、got表與plt表有什麼聯繫,有木有什麼對應關係?
那麼帶着疑問先看答案,再去印證
我們要明白操作系統通常使用動態鏈接的方法來提高程序運行的效率,而且不能回寫到代碼段上。
在上面例子中我們可以看到,call scanf —> scanf的plt表 —>scanf的got表,至於got表的值暫時先不管,我們此刻可以形成這樣一個思維,它能從got表中找到真實的scanf函數供程序加載運行。
我們這麼認爲後,那麼這就變成了一個間接尋址的過程
在這裏插入圖片描述
我們就把獲取數據段存放函數地址的那一小段代碼稱爲PLT(Procedure Linkage Table)過程鏈接表
存放函數地址的數據段稱爲GOT(Global Offset Table)全局偏移表
我們形成這麼一個思維後,再去仔細理解裏面的細節

0x03 再探GOT表和PLT表

已經明白了這麼一個大致過程後,我們來看一下這其中是怎麼一步一步調用的
上面有幾個疑點需要去解決:
一、got表怎麼知道scanf函數的真實地址?
二、got表與plt表的結構是什麼?
我們先來看plt表
剛纔發現scanf@plt表低三行代碼是 jmp 一個地址 ,跟進看一下是什麼
在這裏插入圖片描述
其實這是一個程序PLT表的開始(plt[0]),它做的事情是:

push got[1]
jmp *got[2]

後面是每個函數的plt表。
此時我們再看一下這個神祕的GOT表
在這裏插入圖片描述
除了這兩個(printf和scanf函數的push 0xn的地址,也就是對應的plt表的第二條代碼的地址),其它的got[1], got[2] 爲0,那麼plt表只想爲0的got表幹什麼呢?
因爲我們落下了一個條件,現代操作系統不允許修改代碼段,只能修改數據段,也就是回寫,更專業的稱謂應該是運行時重定位
我們把程序運行起來,我們之前的地址和保存的內容就變了
在這之前,我們先把鏈接時的內容保存一下,做一個對比
在這裏插入圖片描述

② 尋找printf的plt表
③ jmp到plt[0]
④ jmp got[2] -> 0x00000
⑤⑥ printf和scanf的got[3] got[4] -> plt[1] plt[2]的第二條代碼的地址
⑦⑧ 證實上面一點

運行程序,在scanf處下斷點
在這裏插入圖片描述
可以發現,此時scanf@plt表變了,查看got[4]裏內容
在這裏插入圖片描述
依然是push 0x1所在地址
繼續調試,直到這裏,got[4]地址被修改
在這裏插入圖片描述
此時想問了,這是哪裏?
在這裏插入圖片描述
在這裏插入圖片描述
然後就是got[2]中call<_dl_fixup>從而修改got[3]中的地址
那麼問題就來了,剛纔got[2]處不是0嗎,怎麼現在又是這個(_dl_runtime_resolve)?這就是運行時重定位。其實got表的前三項是:

got[0]:address of .dynamic section 也就是本ELF動態段(.dynamic段)的裝載地址
got[1]:address of link_map object( 編譯時填充0)也就是本ELF的link_map數據結構描述符地址,作用:link_map結構,結合.rel.plt段的偏移量,才能真正找到該elf的.rel.plt表項。
got[2]:address of _dl_runtime_resolve function (編譯時填充爲0) 也就是_dl_runtime_resolve函數的地址,來得到真正的函數地址,回寫到對應的got表位置中。

那麼此刻,got表怎麼知道scanf函數的真實地址?這個問題已經解決了。
我們可以看一下其中的裝在過程:
在這裏插入圖片描述
在這裏插入圖片描述
說到這個,可以看到在_dl_runtimw_resolve之前和之後,會將真正的函數地址,也就是glibc運行庫中的函數的地址,回寫到代碼段,就是got[n](n>=3)中。
也就是說在函數第一次調用的時,才通過連接器動態解析並加載到.got.plt中,而這個過程稱之爲延時加載或者惰性加載
到這裏,也要接近尾聲了,當第二次調用同一個函數的時候,就不會與第一次一樣那麼麻煩了,因爲got[n]中已經有了真實地址,直接jmp該地址即可。

0x04 尾記

當時學習時看大佬精心製作的一張動圖,在此借用一下,特別感謝。
在這裏插入圖片描述

想學習二進制的pwn弟弟還需要努力,希望我的小白學習經驗記錄下來可以幫助更多和我一樣的小白。
另見:http://bey0nd.xyz/2020/02/08/1/

發佈了6 篇原創文章 · 獲贊 9 · 訪問量 8352
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章