0x01 前言
操作系統(tǒng)通常使用動(dòng)態(tài)鏈接的方法來(lái)提高程序運(yùn)行的效率。在動(dòng)態(tài)鏈接的情況下,程序加載的時(shí)候并不會(huì)把鏈接庫(kù)中所有函數(shù)都一起加載進(jìn)來(lái),而是程序執(zhí)行的時(shí)候按需加載,如果有函數(shù)并沒(méi)有被調(diào)用,那么它就不會(huì)在程序生命中被加載進(jìn)來(lái)。這樣的設(shè)計(jì)就能提高程序運(yùn)行的流暢度,也減少了內(nèi)存空間。而且現(xiàn)代操作系統(tǒng)不允許修改代碼段,只能修改數(shù)據(jù)段,那么got表與plt表就應(yīng)運(yùn)而生。
0x02 初探GOT表和PLT表
我們先簡(jiǎn)單看一個(gè)例子
我們跟進(jìn)一下scanf@plt
會(huì)發(fā)現(xiàn),有三行代碼
jmp 一個(gè)地址 push 一個(gè)值到棧里面 jmp 一個(gè)地址
看函數(shù)的名字就可以知道這是scanf函數(shù)的plt表,先不著急去了解plt是做什么用的,我們繼續(xù)往下看我們先看一下第一個(gè)jmp是什么跳到哪里。
其實(shí)這是plt表對(duì)應(yīng)函數(shù)的got表,而且我們會(huì)發(fā)現(xiàn)0x201020的值是壓棧命令的地址,其他地方為0,此時(shí)就想問(wèn):
一、got表與plt表有什么意義,為什么要跳來(lái)跳去?
二、got表與plt表有什么聯(lián)系,有木有什么對(duì)應(yīng)關(guān)系?
那么帶著疑問(wèn)先看答案,再去印證我們要明白操作系統(tǒng)通常使用動(dòng)態(tài)鏈接的方法來(lái)提高程序運(yùn)行的效率,而且不能回寫(xiě)到代碼段上。
在上面例子中我們可以看到,call scanf —> scanf的plt表 —>scanf的got表,至于got表的值暫時(shí)先不管,我們此刻可以形成這樣一個(gè)思維,它能從got表中找到真實(shí)的scanf函數(shù)供程序加載運(yùn)行。
我們這么認(rèn)為后,那么這就變成了一個(gè)間接尋址的過(guò)程
我們就把獲取數(shù)據(jù)段存放函數(shù)地址的那一小段代碼稱(chēng)為PLT(Procedure Linkage Table)過(guò)程鏈接表存放函數(shù)地址的數(shù)據(jù)段稱(chēng)為GOT(Global Offset Table)全局偏移表。我們形成這么一個(gè)思維后,再去仔細(xì)理解里面的細(xì)節(jié)。
0x03 再探GOT表和PLT表
已經(jīng)明白了這么一個(gè)大致過(guò)程后,我們來(lái)看一下這其中是怎么一步一步調(diào)用的上面有幾個(gè)疑點(diǎn)需要去解決:
一、got表怎么知道scanf函數(shù)的真實(shí)地址?
二、got表與plt表的結(jié)構(gòu)是什么?我們先來(lái)看plt表剛才發(fā)現(xiàn)scanf@plt表第三行代碼是 jmp 一個(gè)地址 ,跟進(jìn)看一下是什么
其實(shí)這是一個(gè)程序PLT表的開(kāi)始(plt[0]),它做的事情是:
push got[1] jmp **got[2]
后面是每個(gè)函數(shù)的plt表。此時(shí)我們?cè)倏匆幌逻@個(gè)神秘的GOT表
除了這兩個(gè)(printf和scanf函數(shù)的push 0xn的地址,也就是對(duì)應(yīng)的plt表的第二條代碼的地址),其它的got[1], got[2] 為0,那么plt表指向?yàn)?的got表干什么呢?因?yàn)槲覀兟湎铝艘粋€(gè)條件,現(xiàn)代操作系統(tǒng)不允許修改代碼段,只能修改數(shù)據(jù)段,也就是回寫(xiě),更專(zhuān)業(yè)的稱(chēng)謂應(yīng)該是運(yùn)行時(shí)重定位。我們把程序運(yùn)行起來(lái),我們之前的地址和保存的內(nèi)容就變了在這之前,我們先把鏈接時(shí)的內(nèi)容保存一下,做一個(gè)對(duì)比
② 尋找printf的plt表 ③ jmp到plt[0] ④ jmp got[2] -> 0x00000 ⑤⑥ printf和scanf的got[3] got[4] -> plt[1] plt[2]的第二條代碼的地址 ⑦⑧ 證實(shí)上面一點(diǎn)
運(yùn)行程序,在scanf處下斷點(diǎn)
可以發(fā)現(xiàn),此時(shí)scanf@plt表變了,查看got[4]里內(nèi)容
依然是push 0x1所在地址繼續(xù)調(diào)試,直到這里,got[4]地址被修改
此時(shí)想問(wèn)了,這是哪里?
然后就是got[2]中call<_dl_fixup>從而修改got[3]中的地址;
那么問(wèn)題就來(lái)了,剛才got[2]處不是0嗎,怎么現(xiàn)在又是這個(gè)(_dl_runtime_resolve)?這就是運(yùn)行時(shí)重定位。
其實(shí)got表的前三項(xiàng)是:
got[0]:address of .dynamic section 也就是本ELF動(dòng)態(tài)段(.dynamic段)的裝載地址 got[1]:address of link_map object( 編譯時(shí)填充0)也就是本ELF的link_map數(shù)據(jù)結(jié)構(gòu)描述符地址,作用:link_map結(jié)構(gòu),結(jié)合.rel.plt段的偏移量,才能真正找到該elf的.rel.plt got[2]:address of _dl_runtime_resolve function (編譯時(shí)填充為0) 也就是_dl_runtime_resolve函數(shù)的地址,來(lái)得到真正的函數(shù)地址,回寫(xiě)到對(duì)應(yīng)的got表位置中。
那么此刻,got表怎么知道scanf函數(shù)的真實(shí)地址?
這個(gè)問(wèn)題已經(jīng)解決了。我們可以看一下其中的裝載過(guò)程:
說(shuō)到這個(gè),可以看到在_dl_runtimw_resolve之前和之后,會(huì)將真正的函數(shù)地址,也就是glibc運(yùn)行庫(kù)中的函數(shù)的地址,回寫(xiě)到代碼段,就是got[n](n>=3)中。也就是說(shuō)在函數(shù)第一次調(diào)用時(shí),才通過(guò)連接器動(dòng)態(tài)解析并加載到.got.plt中,而這個(gè)過(guò)程稱(chēng)之為延時(shí)加載或者惰性加載。
到這里,也要接近尾聲了,當(dāng)?shù)诙握{(diào)用同一個(gè)函數(shù)的時(shí)候,就不會(huì)與第一次一樣那么麻煩了,因?yàn)間ot[n]中已經(jīng)有了真實(shí)地址,直接jmp該地址即可。