動態連接的PLT與GOT

最近在研究緩衝區溢出攻擊的試驗,發現其中有一種方法叫作ret2plt。plt?這個詞好熟悉,在彙編代碼裏常常見到,和plt常常一塊兒出現的還有一個叫got的東西,可是對這兩個概念一直很模糊,趁着這個機會研究一下。git

能夠先說一下結論 : plt和got是動態連接中用來重定位的。github

GOT

咱們知道,通常咱們的代碼都須要引用外部文件的函數或者變量,好比#include<stdio.h>裏的printf,可是因爲咱們代碼中用到的共享對象是運行時加載進來的,在虛擬地址空間的位置並不肯定,因此代碼裏call <addr of printf>addr of printf不肯定,只有等運行時共享對象被加載到進程的虛擬地址空間裏時,才能最終肯定printf的地址,再進行重定位地址緩存

看一個最簡單的例子:ide

#include <stdio.h>

int main(){

    printf("Hello World");

    return 0;
}

用GDB調試一下(關於GDB調試彙編能夠參考以前寫的GDB 單步調試彙編 ):函數

(gdb) ni
0x000000000040054e in main ()
=> 0x000000000040054e <main+14>:	e8 71 fe ff ff	callq  0x4003c4 <printf@plt>

能夠看出,call <addr of printf>callq 0x4003c4代替,而這個0x4003c4並非真正的printf函數的地址。操作系統

可能有人已經想到了,爲何不能直接在printf函數地址肯定後,直接將call <addr of printf>修改成call <real addr of printf>,像靜態連接那樣呢(靜態連接是在連接階段進行重定位,直接修改的代碼段)?有兩個緣由:調試

  • 現代操做系統不容許修改代碼段,只能修改數據段。
  • 若是上面的代碼片斷是在一個共享對象內,修改了代碼段,那麼它就沒法作到系統內全部進程共享同一個共享對象,由於代碼段被修改了。而動態庫的主要一個優勢就是多個進程共享同一個共享對象的代碼段,節省內存空間,可是進程擁有數據段的獨立副本。

因此,咱們很容易的想到,既然不能修改代碼段,能修改數據段,咱們能夠在共享對象加載完成後,將真實的符號地址放到數據段中,代碼中直接讀取數據段內的地址就行,這裏開闢的空間就叫作GOT(圖有點挫)。code

image

  • 爲每個須要重定位的符號創建一個GOT表項。
  • 當動態連接器裝載共享對象時查找每個須要重定位符號的變量地址,填充GOT。
  • 當指令須要訪問變量或者函數的地址時,從對應的GOT表項中讀出地址,再訪問便可。對應的指令多是callq *(addr in GOT)或者movq offset(%rip) %rax(%rax就是全局變量的地址,能夠用(%rax)解引用)。

可是這樣有一個問題,一個動態庫可能有成百上千個符號,可是咱們引入該動態庫可能只會使用其中某幾個符號,像上面那種方式就會形成不使用的符號也會進行重定位,形成沒必要要的效率損失。咱們知道,動態連接比靜態連接慢1% ~ 5%,其中一個緣由就是動態連接須要在運行時查找地址進行重定位。對象

因此ELF採用了延遲綁定的技術,當函數第一次被用到時才進行綁定。實現方式就是使用plt。blog

PLT

咱們能夠先本身獨立思考如何實現延遲綁定。

  • 上文描述的是動態連接器主動將肯定好的符號地址放到GOT中,延遲綁定須要咱們本身主動告訴一個模塊:我如今須要該符號的肯定地址。假設該模塊叫作_dl_runtime_resolve()
  • 咱們須要告訴_dl_runtime_resolve()須要尋找的符號,也就是函數參數。能夠放到棧中或者寄存器傳遞。
  • _dl_runtime_resolve()尋找完符號的特定地址後,放到寄存器上,好比%rax,供調用者使用。

因此初步的實現步驟是:

callq plt_printf    <printf@plt>
......
......

plt_printf:
    pushq   %rbp            ## allocate stack frame     
    movq    %rsp,%rbp
    pushq iden_of_printf        ## 告訴_dl_runtime_resolve()找printf函數地址,即_dl_runtime_resolve()的參數>
    callq _dl_runtime_resolve()
    callq %rax     ## %rax存放printf真實地址
    leaveq    ## deallocate stack frame
    retq

上面的步驟能夠實現經過一段小代碼(plt)實現延遲綁定,可是存在一個問題:每一次調用printf的時候都須要走一遍這個步驟,然而printf的地址一旦肯定就不會變了,因此咱們須要一個緩存機制,將查找好的printf地址緩存起來。

PLT與GOT

上面說過_dl_runtime_resolve會將肯定好的符合地址放到GOT中,那麼在須要延遲加載的狀況下,GOT裏存放什麼地址?上面說過須要咱們須要將肯定好的符號地址緩存起來,那麼ELF是如何經過PLT與GOT的配合作到延遲加載的?咱們直接看一個真實的例子就行:

#include <stdio.h>

int main(){

    printf("Hello World");

    printf("Hello World Again");

    return 0;
}

gdb調試一下:

One 調用printf的plt

第一次調用printf,會調用printf對應的plt代碼片斷,與上面咱們本身分析實現延遲加載的步驟同樣:

(gdb) ni
0x000000000040054e in main ()
=> 0x000000000040054e <main+14>:	e8 71 fe ff ff	callq  0x4003c4 <printf@plt>

Two 調到printf對應的GOT裏存儲的地址

進到<printf@plt>看看:

(gdb) si
0x00000000004003c4 in printf@plt ()
=> 0x00000000004003c4 <printf@plt+0>:	ff 25 56 05 20 00	jmpq   *0x200556(%rip)        # 0x600920 <printf@got.plt>

這裏跳到了printf對應的GOT裏存儲的地址。(elf對got作了細分:got存放全局變量引用的地址,got.plt存放函數引用的地址

看看動態連接在將肯定的符號地址放到GOT前,GOT裏存放的是什麼地址:

(gdb) x 0x600920
0x600920 <printf@got.plt>:	0x004003ca
(gdb) disas  0x4003c4
Dump of assembler code for function printf@plt:
   0x00000000004003c4 <+0>:	jmpq   *0x200556(%rip)        # 0x600920 <printf@got.plt>
=> 0x00000000004003ca <+6>:	pushq  $0x0
   0x00000000004003cf <+11>:	jmpq   0x4003b4
End of assembler dump.

有意思的是jmp到了下一條指令的地址。其實這個時候咱們已經能夠猜出來了:延遲加載以前,got.plt裏存放的是下一條指令地址,延遲加載以後,got.plt裏存放的就是真實的符號地址,就能夠直接jmp到printf函數裏了。

Three 將printf對應的標識壓到棧中,並跳到plt[0]

(gdb) ni
0x00000000004003ca in printf@plt ()
=> 0x00000000004003ca <printf@plt+6>:	68 00 00 00 00	pushq  $0x0
(gdb) ni
0x00000000004003cf in printf@plt ()
=> 0x00000000004003cf <printf@plt+11>:	e9 e0 ff ff ff	jmpq   0x4003b4
(gdb) si
0x00000000004003b4 in ?? ()      ## 這裏應該是plt[0],可是gdb不知道爲何沒有顯示出來
=> 0x00000000004003b4:	ff 35 56 05 20 00	pushq  0x200556(%rip)        # 0x600910 <_GLOBAL_OFFSET_TABLE_+8>

Four 在plt[0]中調用_dl_runtime_resolve查找符合真實地址

說明這個是什麼地址??0x600910

(gdb)
0x00000000004003b4 in ?? ()
=> 0x00000000004003b4:	ff 35 56 05 20 00	pushq  0x200556(%rip)        # 0x600910 <_GLOBAL_OFFSET_TABLE_+8>
(gdb)
0x00000000004003ba in ?? ()
=> 0x00000000004003ba:	ff 25 58 05 20 00	jmpq   *0x200558(%rip)        # 0x600918 <_GLOBAL_OFFSET_TABLE_+16>
(gdb)
_dl_runtime_resolve () at ../sysdeps/x86_64/dl-trampoline.S:34
34		subq $56,%rsp
=> 0x00007ffff7deef30 <_dl_runtime_resolve+0>:	48 83 ec 38	sub    $0x38,%rsp

咱們不用管_dl_runtime_resolve是怎麼處理的,直接看_dl_runtime_resolve處理完成後printf對應的GOT的值:

(gdb)
56		jmp *%r11		# Jump to function address.
=> 0x00007ffff7deef8e <_dl_runtime_resolve+94>:	41 ff e3	jmpq   *%r11
   0x00007ffff7deef91:	66 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00	data32 data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
(gdb)
0x00007ffff7a7b5d0 in printf () from /lib64/libc.so.6
=> 0x00007ffff7a7b5d0 <printf+0>:	48 81 ec d8 00 00 00	sub    $0xd8,%rsp
(gdb)
......
......
(gdb) x 0x600920
0x600920 <printf@got.plt>:	0xf7a7b5d0

與以前猜想的同樣,printf對應的GOT表項目前已經存放了printf真實的虛擬地址。那麼在下次調用時就避免再重定位,直接跳到printf地址了。

Five 第二次調用printf

(gdb) si
0x00000000004003c4 in printf@plt ()
=> 0x00000000004003c4 <printf@plt+0>:	ff 25 56 05 20 00	jmpq   *0x200556(%rip)        # 0x600920 <printf@got.plt>
(gdb) x 0x600920
0x600920 <printf@got.plt>:	0xf7a7b5d0
(gdb) si
0x00007ffff7a7b5d0 in printf () from /lib64/libc.so.6
=> 0x00007ffff7a7b5d0 <printf+0>:	48 81 ec d8 00 00 00	sub    $0xd8,%rsp

直接跳到printf的虛擬地址。

下面這張圖能夠總結上面的五步過程:

image

(完)

朋友們能夠關注下個人公衆號,得到最及時的更新:

相關文章
相關標籤/搜索