最近在研究緩衝區溢出攻擊的試驗,發現其中有一種方法叫作ret2plt。plt?這個詞好熟悉,在彙編代碼裏常常見到,和plt常常一塊兒出現的還有一個叫got的東西,可是對這兩個概念一直很模糊,趁着這個機會研究一下。git
能夠先說一下結論 : plt和got是動態連接中用來重定位的。github
咱們知道,通常咱們的代碼都須要引用外部文件的函數或者變量,好比#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
callq *(addr in GOT)
或者movq offset(%rip) %rax
(%rax
就是全局變量的地址,能夠用(%rax)
解引用)。可是這樣有一個問題,一個動態庫可能有成百上千個符號,可是咱們引入該動態庫可能只會使用其中某幾個符號,像上面那種方式就會形成不使用的符號也會進行重定位,形成沒必要要的效率損失。咱們知道,動態連接比靜態連接慢1% ~ 5%,其中一個緣由就是動態連接須要在運行時查找地址進行重定位。對象
因此ELF採用了延遲綁定的技術,當函數第一次被用到時才進行綁定。實現方式就是使用plt。blog
咱們能夠先本身獨立思考如何實現延遲綁定。
_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地址緩存起來。
上面說過_dl_runtime_resolve
會將肯定好的符合地址放到GOT中,那麼在須要延遲加載的狀況下,GOT裏存放什麼地址?上面說過須要咱們須要將肯定好的符號地址緩存起來,那麼ELF是如何經過PLT與GOT的配合作到延遲加載的?咱們直接看一個真實的例子就行:
#include <stdio.h> int main(){ printf("Hello World"); printf("Hello World Again"); return 0; }
gdb調試一下:
第一次調用printf,會調用printf對應的plt代碼片斷,與上面咱們本身分析實現延遲加載的步驟同樣:
(gdb) ni 0x000000000040054e in main () => 0x000000000040054e <main+14>: e8 71 fe ff ff callq 0x4003c4 <printf@plt>
進到<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函數裏了。
(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>
說明這個是什麼地址??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地址了。
(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的虛擬地址。
下面這張圖能夠總結上面的五步過程:
(完)
朋友們能夠關注下個人公衆號,得到最及時的更新: