之前幾篇介紹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字節), 保存在前三個位置, 分別是:
其中, 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隨機化的等級, 會在棧和內核空間之間, 棧和動態庫(mmap)之間, 堆和.bss之間
都分別加上隨機的偏移. 所以此時libc.so的地址是未知的, ret2libc攻擊也就得到緩解了.
ret2plt
但是, 雖然ASLR隨機化了上面的幾個地址, 在位置相關代碼的情況下, PLT的地址還是確定的!
所以如果沒有啓用位置無關代碼的話, 即使啓用了ASLR, 我們還是可以通過PLT來跳轉到libc
中的函數執行, 這種攻擊方法就叫ret2plt.
除此之外, 因爲.got.plt是有寫入權限的, 攻擊者還可以通過代碼中的內存破壞漏洞對
.got.plt段進行覆蓋, 從而間接控制代碼的執行流程.
攻擊緩解
ret2plt這麼屌, 就沒人管管嗎? 當然有! 一個最簡單的辦法就是啓用位置無關代碼,
不過就算可執行程序的代碼是位置無關的, 鏈接器還是有可能將其加載到老地方.
一個更正確的緩解措施是RELRO
即relocations 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的寫權限, 但也犧牲了延時綁定帶來的好處.
參考文章
- ELF Format Reference
- Examining Dynamic Linking with GDB
- RELRO - A (not so well known) Memory Corruption Mitigation Technique
- What is the symbol and the global offset table?
- How the ELF ruined Christmas
- anatomy-of-a-program-in-memory
- got-and-plt-for-pwning
- 本文地址https://www.pppan.net/blog/detail/2018-04-09-about-got-plt
歡迎交流, 文章轉載請註明出處, 謝謝!