深入瞭解GOT,PLT和動態鏈接

thumb
之前幾篇介紹exploit的文章, 有提到return-to-plt的技術. 當時只簡單介紹了
GOT和PLT表的基本作用和他們之間的關係, 所以今天就來詳細分析下其具體的工作過程.

本文所用的依然是Linux x86 64位環境, 不過分析的ELF文件是32位的(-m32).

大局觀

首先, 我們要知道, GOT和PLT只是一種重定向的實現方式. 所以爲了理解他們的作用,
就要先知道什麼是重定向, 以及我們爲什麼需要重定向.

重定向(relocations), 簡單來說就是二進制文件中留下的"坑", 預留給外部變量或函數.
這裏的變量和函數統稱爲符號(symbols). 在編譯期我們通常只知道外部符號的類型
(變量類型和函數原型), 而不需要知道具體的值(變量值和函數實現). 而這些預留的"坑",
會在用到之前(鏈接期間或者運行期間)填上. 在鏈接期間填上主要通過工具鏈中的連接器,
比如GNU鏈接器ld; 在運行期間填上則通過動態連接器, 或者說解釋器(interpreter)來實現.
比如:

$ file /bin/ls
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3c233e12c466a83aa9b2094b07dbfaa5bd10eccd, stripped

可以看到/bin/ls的解釋器是/lib64/ld-linux-x86-64.so.2.

在本文中, 用下面兩個簡單的c文件來進行說明, 首先是symbol.c, 定義了一個函數變量:

// symbol.c
int my_var = 42;
int my_func(int a, int b) {
    return a + b;
}

編譯爲動態鏈接庫:

gcc -g -m32 -masm=intel -shared -fPIC symbol.c -o libsymbol.so

另一個文件是main.c, 調用該動態鏈接庫:

// main.c
int var = 10;
extern int my_var;
extern int my_func(int, int);

int main() {
    int a, b;
    a = var;
    b = my_var;
    return my_func(a, b);
}

分別編譯兩個版本, 位置相關的main和位置無關的main_pi, 具體會稍後解釋.

# 位置相關
gcc -g -m32 -masm=intel -L. -lsymbol -no-pie -fno-pic main.c libsymbol.so -o main
# 位置無關
gcc -g -m32 -masm=intel -L. -lsymbol main.c libsymbol.so -o main_pi

符號表

函數和變量作爲符號被存在可執行文件中, 不同類型的符號又聚合在一起, 稱爲符號表.
有兩種類型的符號表, 一種是常規的(.symtab和.strtab), 另一種是動態的(.dynsym和.dynstr),
他們都在對應的section中, 以main爲例:

$ readelf -S ./main
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 5] .dynsym           DYNSYM          080481ec 0001ec 0000b0 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804829c 00029c 000085 00   A  0   0  1
...
  [33] .symtab           SYMTAB          00000000 00120c 000490 10     34  52  4
  [34] .strtab           STRTAB          00000000 00169c 0001e1 00      0   0  1

常規的符號表通常只在調試時用到. 我們平時用的strip命令刪除的就是該符號表;
而動態符號表則是程序執行時候真正會查找的目標.

位置無關代碼

剛剛編譯動態鏈接庫時指定了-fPIC, 編譯main_pi時(默認)指定了-pie, 其實都是爲了
生成位置無關的代碼, 那麼什麼是位置無關? 爲什麼要位置無關?

我們執行一個可執行文件的時候, 其實是先將磁盤上的該文件讀取到內存中, 然後再執行.
而每個進程都有自己的虛擬內存空間, 以32位程序爲例, 就有2^32=4GB的尋址空間, 從0x00000000
到0xffffffff. 這裏暫時不深入介紹, 只需要知道虛擬內存最終會通過頁表映射到物理內存中.

當然, 如果你感興趣, 強烈推薦你去看下Gustavo Duarte的這篇文章.

按照鏈接器的約定, 32位程序會加載到0x08048000這個地址中(爲什麼?),
所以我們寫程序時, 可以以這個地址爲基礎, 對變量進行絕對地址尋址. 以main爲例:

$ readelf -S ./main | grep "\.data"
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [25] .data             PROGBITS        0804a014 001014 00000c 00  WA  0   0  4

.data部分在可執行文件中的偏移量爲0x1014, 那麼加載到虛擬內存中的地址應該是
0x8048000+0x1014=0x804a14, 正好和顯示的結果一樣. 再看看main函數的彙編代碼:

$ objdump  -d ./main | grep "<main>" -A 15
080484db <main>:
 80484db:   8d 4c 24 04             lea    ecx,[esp+0x4]
 80484df:   83 e4 f0                and    esp,0xfffffff0
 80484e2:   ff 71 fc                push   DWORD PTR [ecx-0x4]
 80484e5:   55                      push   ebp
 80484e6:   89 e5                   mov    ebp,esp
 80484e8:   51                      push   ecx
 80484e9:   83 ec 14                sub    esp,0x14
 80484ec:   a1 1c a0 04 08          mov    eax,ds:0x804a01c
 80484f1:   89 45 f4                mov    DWORD PTR [ebp-0xc],eax
 80484f4:   a1 20 a0 04 08          mov    eax,ds:0x804a020
 80484f9:   89 45 f0                mov    DWORD PTR [ebp-0x10],eax
 80484fc:   83 ec 08                sub    esp,0x8
 80484ff:   ff 75 f0                push   DWORD PTR [ebp-0x10]
 8048502:   ff 75 f4                push   DWORD PTR [ebp-0xc]
 8048505:   e8 a6 fe ff ff          call   80483b0 <my_func@plt>

注意80484ec這行, 可以看到獲取變量直接用的絕對地址0x804a01c(正好在.data範圍內).
用gdb(在啓動程序之前)可看到該地址正是var變量的地址, 且初始值爲10:

$ gdb ./main
(gdb) x/xw 0x804a01c
0x804a01c <var>:    0x0000000a

按絕對地址尋址, 對可執行文件來說不是什麼大問題, 因爲一個進程只有一個主函數.
可對於動態鏈接庫而言就比較麻煩, 如果每個.so文件都要求加載到某個絕對地址,
那簡直是個噩夢, 因爲你無法保證不和別人的.so加載地址衝突. 所以就有了位置無關代碼的概念.
以位置無關的方式編譯的main_pi, 來看看其相關信息:

$ readelf -S ./main_pi | grep "\.data"
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [25] .data             PROGBITS        00002014 001014 00000c 00  WA  0   0  4

偏移量還是固定的, 但Addr部分不再是絕對地址. 也就是說程序可以加載到虛擬內存的任意位置.
聽起來很神奇? 其實實現很簡單, 繼續看看main()的彙編:

$ objdump -d main_pi | grep "<main>" -A 20
00000660 <main>:
 660:   8d 4c 24 04             lea    ecx,[esp+0x4]
 664:   83 e4 f0                and    esp,0xfffffff0
 667:   ff 71 fc                push   DWORD PTR [ecx-0x4]
 66a:   55                      push   ebp
 66b:   89 e5                   mov    ebp,esp
 66d:   53                      push   ebx
 66e:   51                      push   ecx
 66f:   83 ec 10                sub    esp,0x10
 672:   e8 36 00 00 00          call   6ad <__x86.get_pc_thunk.ax>
 677:   05 89 19 00 00          add    eax,0x1989
 67c:   8b 90 1c 00 00 00       mov    edx,DWORD PTR [eax+0x1c]
 682:   89 55 f4                mov    DWORD PTR [ebp-0xc],edx
 685:   8b 90 f0 ff ff ff       mov    edx,DWORD PTR [eax-0x10]
 68b:   8b 12                   mov    edx,DWORD PTR [edx]
 68d:   89 55 f0                mov    DWORD PTR [ebp-0x10],edx
 690:   83 ec 08                sub    esp,0x8
 693:   ff 75 f0                push   DWORD PTR [ebp-0x10]
 696:   ff 75 f4                push   DWORD PTR [ebp-0xc]
 699:   89 c3                   mov    ebx,eax
 69b:   e8 20 fe ff ff          call   4c0 <my_func@plt>

注意67c~682處, 和之前的區別是這次通過eax寄存器來對變量進行尋址, 不過有個__x86.get_pc_thunk.ax函數,
其作用很簡單, 在之前的IOLI-crackme0x06-0x09 writeup中有簡單介紹過:

objdump -d main_pi | grep "__x86.get_pc_thunk.ax" -A 2
000006ad <__x86.get_pc_thunk.ax>:
 6ad:   8b 04 24                mov    eax,DWORD PTR [esp]
 6b0:   c3                      ret

作用就是把esp(即返回地址)的值保存在eax(PIC寄存器)中, 在接下來尋址用.
有人可能好奇, 爲什麼這麼麻煩, 直接用eip寄存器不就行了?
其實64位下就是這樣操作的! 不過32位下不支持直接訪問PC寄存器,
所以就多了一層間接的函數調用.

扯遠了, 經過672和677兩條指令後, eax的值將等於相對當前PC指針的固定位移.
只看靜態代碼的話, 可知eax=0x677+0x1989=0x2000, 而這個地址是...

$ readelf -S ./main_pi | grep 2000 -C 1
  [23] .got              PROGBITS        00001fe4 000fe4 00001c 04  WA  0   0  4
  [24] .got.plt          PROGBITS        00002000 001000 000014 04  WA  0   0  4
  [25] .data             PROGBITS        00002014 001014 00000c 00  WA  0   0  4

.got.plt的起始地址! 這個section我們接下來會說到. 現在先看彙編的67c處,
通過eax+0x1c=0x201c獲取了變量的值, 這個地址已經進入到了.data之中:

$ gdb ./main_pi 
(gdb) x/xw 0x2000+0x1c
0x201c <var>:   0x0000000a

所以, 位置無關代碼實際上就是通過運行時PC指針的值來找到代碼所引用的
其他符號的位置, 不管二進制文件被加載到哪個位置, 都可以正確執行.

缺點

位置無關代碼的缺點是, 在執行時要保留一個寄存器作爲PIC寄存器,
有可能會導致寄存器不夠用; 還有一個缺點是運行時要經過計算來獲得
符號的地址, 從某種方面來說也對運行速度有點小影響.

優點

位置無關代碼的優點就跟他名字一樣, 可以保證加載到任意地址都能
正常執行, 這也是每個動態鏈接庫都需要支持的.

動態鏈接

剛剛我們說位置無關代碼的時候有看到, PIC寄存器爲.got.plt的地址, 然後按偏移量
來獲取變量. 上面只看了eax+0x1c即從.data段獲取的內容(var), 還有一個參數是通過
eax-0x10即.got段之中獲取的my_var. 後者是在symbol.c中定義的, 所以其內容在編譯期
未知. 如果是靜態鏈接, 則可以在鏈接時解析符號的值. 我們這裏主要考慮動態鏈接的情況.

一些定義

上面說了很多.got, .plt啥的, 那麼這些section到底是做什麼用的呢. 其實這些都是
鏈接器(或解釋器, 下面統稱爲鏈接器)在執行重定向時會用到的部分, 先來看他們的定義.

.got

這是我們常說的GOT, 即Global Offset Table, 全局偏移表. 這是鏈接器在執行鏈接時
實際上要填充的部分, 保存了所有外部符號的地址信息.
不過值得注意的是, 在i386架構下, 除了每個函數佔用一個GOT表項外,GOT表項還保留了
3個公共表項, 每項32位(4字節), 保存在前三個位置, 分別是:

  • got[0]: 本ELF動態段(.dynamic段)的裝載地址
  • got1: 本ELF的link_map數據結構描述符地址
  • got2: _dl_runtime_resolve函數的地址

其中, link_map數據結構的定義如下:

struct link_map
{
   /* Shared library's load address. */
   ElfW(Addr) l_addr;                 
   /* Pointer to library's name in the string table. */                                 
   char *l_name;    
   /* 
        Dynamic section of the shared object.
        Includes dynamic linking info etc.
        Not interesting to us.  
   */                   
   ElfW(Dyn) *l_ld;   
   /* Pointer to previous and next link_map node. */                 
   struct link_map *l_next, *l_prev;   
};

.plt

這也是我們常說的PLT, 即Procedure Linkage Table, 進程鏈接表. 這個表裏包含了一些代碼,
用來(1)調用鏈接器來解析某個外部函數的地址, 並填充到.got.plt中, 然後跳轉到該函數; 或者
(2)直接在.got.plt中查找並跳轉到對應外部函數(如果已經填充過).

.got.plt

.got.plt相當於.plt的GOT全局偏移表, 其內容有兩種情況, 1)如果在之前查找過該符號,
內容爲外部函數的具體地址. 2)如果沒查找過, 則內容爲跳轉回.plt的代碼, 並執行查找.
至於爲什麼要這麼繞, 後面會說明具體原因.

.plt.got

說實話, 這部分我還不知道有什麼具體作用, 可能是爲了對稱吧. 逃)

對於我們將要研究的main程序, 這些段的地址如下:

$ readelf -S main | egrep  '.plt|.got'
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [12] .plt              PROGBITS        080483a0 0003a0 000030 04  AX  0   0 16
  [13] .plt.got          PROGBITS        080483d0 0003d0 000008 00  AX  0   0  8
  [23] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
  [24] .got.plt          PROGBITS        0804a000 001000 000014 04  WA  0   0  4

變量

有了上面的定義, 先看變量的解析過程, 以main爲例(位置相關的),
查看需要重定向的符號:

$ readelf --relocs ./main

Relocation section '.rel.dyn' at offset 0x358 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ffc  00000206 R_386_GLOB_DAT    00000000   __gmon_start__
0804a020  00000605 R_386_COPY        0804a020   my_var

Relocation section '.rel.plt' at offset 0x368 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   my_func
0804a010  00000307 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0

my_var的地址爲0804a020, 注意這裏實際上是.bss段, 如下:

$ readelf -S main | grep 0804a020 -B 2
  [24] .got.plt          PROGBITS        0804a000 001000 000014 04  WA  0   0  4
  [25] .data             PROGBITS        0804a014 001014 00000c 00  WA  0   0  4
  [26] .bss              NOBITS          0804a020 001020 000008 00  WA  0   0  4

因爲main.c裏只是聲明變量而且沒初始化, 在鏈接前並不知道是否在外部定義.
同時, 該變量的值一開始是不知道的, 我們可以通過gdb來驗證:

(gdb) x/dw 0x0804a020
0x804a020 <my_var>: 0

顯示值爲0, 但實際上在symbol.c中定義了其值爲42, 啓動前我們先在這裏下個觀察點,
看看究竟是什麼時候加載進去的:

(gdb) set environment LD_LIBRARY_PATH=.
(gdb) watch -l *0x804a020
Hardware watchpoint 1: -location *0x804a020
(gdb) run
Starting program: /home/pan/project/cFile/shared_library/plt/main 

Hardware watchpoint 1: -location *0x804a020

Old value = 0
New value = 42
0xf7ff2e08 in ?? () from /lib/ld-linux.so.2
(gdb) x/xd 0x804a020
0x804a020 <my_var>: 42

所以, 確實是鏈接器/lib/ld-linux.so.2負責填充了該變量的內容.
而且是在程序運行之前就完成了符號解析.

函數

接下來看看外部函數符號. 外部函數的內容(指令)也是像變量一樣在
程序運行之前完成填充的嗎? 其實這理論上是可以的, 事實上稍有不同.

靜態分析

我們先從彙編看看main是如何調用my_func()函數的:

(gdb) disassemble  main
Dump of assembler code for function main:
   0x080484db <+0>: lea    ecx,[esp+0x4]
   0x080484df <+4>: and    esp,0xfffffff0
   0x080484e2 <+7>: push   DWORD PTR [ecx-0x4]
   0x080484e5 <+10>:    push   ebp
   0x080484e6 <+11>:    mov    ebp,esp
   0x080484e8 <+13>:    push   ecx
   0x080484e9 <+14>:    sub    esp,0x14
   0x080484ec <+17>:    mov    eax,ds:0x804a01c
   0x080484f1 <+22>:    mov    DWORD PTR [ebp-0xc],eax
   0x080484f4 <+25>:    mov    eax,ds:0x804a020
   0x080484f9 <+30>:    mov    DWORD PTR [ebp-0x10],eax
   0x080484fc <+33>:    sub    esp,0x8
   0x080484ff <+36>:    push   DWORD PTR [ebp-0x10]
   0x08048502 <+39>:    push   DWORD PTR [ebp-0xc]
   0x08048505 <+42>:    call   0x80483b0 <my_func@plt>

調用的地址是0x80483b0, 在.plt段中, 之前說了PLT的定義, 現在具體看看裏面的內容:

(gdb) disassemble 0x80483b0
Dump of assembler code for function my_func@plt:
   0x080483b0 <+0>: jmp    DWORD PTR ds:0x804a00c
   0x080483b6 <+6>: push   0x0
   0x080483bb <+11>:    jmp    0x80483a0
End of assembler dump.

首先是跳轉到*0x804a00c, 該地址在.got.plt之中, 之前說了, .got.plt相當於
.plt的GOT, 而GOT本身相當於一個數組, 看看該"數組"的內容:

(gdb) x/4xw 0x804a00c
0x804a00c:  0x080483b6  0x080483c6  0x00000000  0x00000000

所以, 0x080483b0這裏的跳轉, 相當於跳轉到0x080483b6, 即下一條指令!
這個多餘的跳轉先打個問號, 把流程走完再說. 接着, 跳轉到了0x80483a0,
這個地址, 是.plt的起始地址, 這裏的指令如下:

(gdb) x/2i 0x080483a0
   0x80483a0:   push   DWORD PTR ds:0x804a004
   0x80483a6:   jmp    DWORD PTR ds:0x804a008

跳轉到了0x804a008, 在前面我們知道0x804a000是.got.plt的地址,
而在上一節的定義中, 也知道了.got表前三項的作用, 0x804a008
正好是第三項got2, 即_dl_runtime_resolve函數的地址. 0x804a004
則是調用該函數的參數, 且值爲got1, 即本ELF的link_map的地址.
如下, 在進程未啓動前, got1和got2都爲0, 在啓動時由鏈接器裝填:

(gdb) x/4xw 0x804a000
0x804a000:  0x08049f0c  0x00000000  0x00000000  0x080483b6

因此, 實際上(第一次)調用my_func@plt就相當於調用了
_dl_runtime_resolve((link_map *)m, 0)! 其中link_map提供了運行時的必要信息,
而0則是my_func函數的偏移(在my_func@plt中push 0x0).
該函數定義在glibc/sysdeps/i386/dl-trampoline.S中, 關鍵代碼如下:

_dl_runtime_resolve:
        cfi_adjust_cfa_offset (8)
        pushl %eax                # Preserve registers otherwise clobbered.
        cfi_adjust_cfa_offset (4)
        pushl %ecx
        cfi_adjust_cfa_offset (4)
        pushl %edx
        cfi_adjust_cfa_offset (4)
        movl 16(%esp), %edx        # Copy args pushed by PLT in register.  Note
        movl 12(%esp), %eax        # that `fixup' takes its parameters in regs.
        call _dl_fixup                # Call resolver.
        popl %edx                # Get register content back.
        cfi_adjust_cfa_offset (-4)
        movl (%esp), %ecx
        movl %eax, (%esp)        # Store the function address.
        movl 4(%esp), %eax
        ret $12                        # Jump to function address.

從註釋裏也可以看出來, 該函數實際上做了兩件事:

  • 1)解析出my_func的地址並將值填入.got.plt中.
  • 2)跳轉執行真正的my_func函數.

動態分析

上面雖然用了gdb, 但程序並未運行, 只是分析靜態的彙編代碼, 爲了驗證上面的說法,
我們需要進行動態分析. 接着上面的分析, 我們這次在調用_dl_runtime_resolve
前打上斷點. 還記得之前在my_func@plt中一次多餘的跳轉嗎? 當時打了個問號,
現在就來解答這個疑問. 在0x804a00c處打上觀察點並運行:

(gdb) b *0x80483a6
(gdb) watch -l *0x804a00c
(gdb) run
Breakpoint 1, 0x080483a6 in ?? ()
(gdb) x/xw 0x804a00c
0x804a00c:  0x080483b6
(gdb) continue
Hardware watchpoint 1: -location *0x804a00c

Old value = 0x80483b6
New value = 0xf7fcf4f0
0xf7fe8113 in ?? () from /lib/ld-linux.so.2
(gdb) disassemble 0xf7fcf4f0
Dump of assembler code for function my_func:
...

可以看到, 在_dl_runtime_resolve之前, 0x804a00c地址的值爲0x080483b6,
即下一條指令. 而運行之後, 該地址的值變爲0xf7fcf4f0, 正是my_func的加載地址!
也就是說, my_func函數的地址是在第一次調用時, 才通過連接器動態解析並加載到
.got.plt中的. 而這個過程, 也稱之爲延時加載或者惰性加載.

延時加載

延時加載的好處是, 只有當外部函數被調用了纔會去進行動態加載, 降低程序的啓動時間.
而第一次加載之後, 對於後續的調用就可以直接跳轉而不需要再去加載.
這樣一方面減少了進程的啓動開銷, 另一方面也不會造成太多額外的運行時開銷,
所以延時加載在當今也是廣泛應用的一個思想. 對於位置無關的代碼,
延時加載的過程也是類似的, 並沒有太大區別. 讀者可以自己去追蹤一下.

相關攻擊

上節的分析忽略了一個重要的地方, 那就是各個段的權限, 再重溫一下各個section:

$ readelf -S main
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [12] .plt              PROGBITS        080483a0 0003a0 000030 04  AX  0   0 16
  [13] .plt.got          PROGBITS        080483d0 0003d0 000008 00  AX  0   0  8
  [14] .text             PROGBITS        080483e0 0003e0 0001a2 00  AX  0   0 16
  [23] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
  [24] .got.plt          PROGBITS        0804a000 001000 000014 04  WA  0   0  4
  [25] .data             PROGBITS        0804a014 001014 00000c 00  WA  0   0  4
  [26] .bss              NOBITS          0804a020 001020 000008 00  WA  0   0  4
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),

爲了使得結果更清晰, 我刪除了一些無關的輸出. 從上表的Flg行可以看到, 前三個段
都有可執行權限(X), 卻沒有寫(W)權限; 而後幾個都有寫權限, 卻不可執行.
現代的操作系統一般都支持NX特性, 所以這樣的結果是很常見的.

同時, 這也是爲什麼要將PLT和GOT分開的原因. 鏈接器運行時填充的區域, 必須是可寫的,
但可寫的區域一般不可執行, 對外部變量沒有影響, 但對於外部函數來說就需要
引入一個可執行的區域作爲引導, 這就是PLT的作用.

ret2libc

我在棧溢出攻擊和緩解中有提到, ret2libc的使用場景是當棧不可執行時,
直接跳轉到libc.so某個函數的地址, 比如system(), 來獲得shell.
不過前提是要知道libc.so在運行時的加載地址. 如果沒啓用ASLR, 這個地址是固定的.
啓用ASLR之後就會有個隨機的偏移, 如下:

aslr

根據ASLR隨機化的等級, 會在棧和內核空間之間, 棧和動態庫(mmap)之間, 堆和.bss之間
都分別加上隨機的偏移. 所以此時libc.so的地址是未知的, ret2libc攻擊也就得到緩解了.

ret2plt

但是, 雖然ASLR隨機化了上面的幾個地址, 在位置相關代碼的情況下, PLT的地址還是確定的!
所以如果沒有啓用位置無關代碼的話, 即使啓用了ASLR, 我們還是可以通過PLT來跳轉到libc
中的函數執行, 這種攻擊方法就叫ret2plt.

除此之外, 因爲.got.plt是有寫入權限的, 攻擊者還可以通過代碼中的內存破壞漏洞對
.got.plt段進行覆蓋, 從而間接控制代碼的執行流程.

攻擊緩解

ret2plt這麼屌, 就沒人管管嗎? 當然有! 一個最簡單的辦法就是啓用位置無關代碼,
不過就算可執行程序的代碼是位置無關的, 鏈接器還是有可能將其加載到老地方.
一個更正確的緩解措施是RELROrelocations read-only.
RELRO是鏈接器的一個選項, 可以通過man ld來查看. 主要作用就是令重定向只讀.
有兩個RELRO的等級, 部分RELRO和完全RELRO.

部分RELRO(由ld -z relro啓用):

  • 將.got段映射爲只讀(但.got.plt還是可以寫)
  • 重新排列各個段來減少全局變量溢出導致覆蓋代碼段的可能性.

完全RELRO(由ld -z relro -z now啓用)

  • 執行部分RELRO的所有操作.
  • 讓鏈接器在鏈接期間(執行程序之前)解析所有的符號, 然後去除.got的寫權限.
  • 將.got.plt合併到.got段中, 所以.got.plt將不復存在.

因此可以看到, 只有完全RELRO才能防止攻擊者覆蓋.got.plt, 因爲在鏈接期間
就對程序符號進行了解析. 當然同時也放棄了延時綁定所帶來的好處.

總結

爲了靈活利用虛擬內存空間, 所以編譯器可以產生位置無關的代碼.
可執行文件可以是位置無關的, 也可以是位置相關的, 動態鏈接庫
絕大多數都是位置無關的. GOT表可寫不可執行, PLT可執行不可寫,
他們相互作用來實現函數符號的延時綁定. ASLR並不隨機化PLT部分,
所以對ret2plt攻擊沒有直接影響. 爲防止惡意修改got, 鏈接器提供了RELRO
選項, 去除got的寫權限, 但也犧牲了延時綁定帶來的好處.

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章