1. 程式人生 > >PLT與GOT

PLT與GOT

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檔案)通過連結器將它們連結成一個可執行檔案。其中分為兩種情況:
  1. 如果是在其他中間檔案中已經定義了的函式,連結階段可以直接重定位到函式地址
  2. 如果是在動態庫中定義了的函式,連結階段無法直接重定位到函式地址,只能生成額外的小片段程式碼,也就是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的真實地址,進而基地址就可以由  基地址= 真實地址-偏移地址算出。

&n