[pwn基礎]動態鏈接原理
[pwn基礎]動態鏈接原理
動態鏈接概念
為了解決空間浪費和更新困難問題最簡單的辦法就是把程式的模組相互分割開來,形成獨立的文件,而不是將它們靜態鏈接在一起。
簡單的說:不對那些組成程式的目標文件進行鏈接,等到程式要運行時候才進行鏈接。
把鏈接這個過程推遲到了運行時再進行
,這就是動態鏈接(Dynamic Linking)
的基本思想。
動態鏈接調用so例子
LibTest.h LibTest.c
#ifndef LIBTEST_H
#define LIBTEST_H
void foobar(int i);
#endif
#include "LibTest.h"
#include <stdio.h>
void foobar(int i)
{
printf("Printing from Lib.so %d\n",i);
}
編譯成.so(動態鏈接庫)
gcc -fPIC -shared LibTest.c -o libtest.so
#-fPIC是與地址無關選項
#-shared 是編譯成so 動態聯機
#-o 輸出,so文件名必須以lib開頭
Program1.c
#include "LibTest.h"
int main(int argc,char *argv[])
{
foobar(1);
return 0;
}
Program2.c
#include "Lib.h"
int main(int argc,char *argv[])
{
foobar(2);
return 0;
}
分別編譯Program1 和Program2動態調用libtest.so
gcc Program1.c -L. -ltest -o Program1
gcc Program2.c -L. -ltest -o Program2
export LD_LIBRARY_PATH=/home/pwn/testdemo:$LD_LIBRARY_PATH
#上面命令的意思分別是
#-L. 代表的是so在本地當前目錄查找
#-ltest 動態調用so有一套自己的命名規則,一般必須是lib帶頭,然後才是so名字.所以-l後面跟的是lib之後的so名,忽略後綴。
#export LD_LIBRARY_PATH代表的是把動態鏈接目錄加入環境變數,默認是/usr/lib下
~/testdemo » ldd Program1
linux-vdso.so.1 (0x00007ffd25b6e000)
libtest.so => /home/pwn/testdemo/libtest.so (0x00007f3ae466f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3ae446a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3ae467b000)
#ldd命令可以用來查看當前程式所調用的動態so。
運行結果:
~/testdemo » ./Program1
Printing from Lib.so 1
~/testdemo » ./Program2
Printing from Lib.so 2
GOT(全局偏移表)
GOT表的全稱是Global Offset Table(全局偏移表)
可以把它理解成為了動態鏈接,把所有的符號偏移量或(絕對地址)都放入到了一個表裡這就是GOT表
- .got表(一般放的是全局變數和static變數)
- .got.plt表(一般放的就是引用so的函數,即導入函數)
下面我們來做個實驗加深下理解。
/*a.c源碼*/
extern int shared; //外部符號,跨模組
int main()
{
int a = 100;
swap(&a,&shared);//外部符號,調用外部模組的swap函數
}
/*b.c源碼*/
extern int shared = 1;
void swap(int *a,int *b)
{
*a ^= *b ^= *a ^= *b;
}
#編譯成.so
gcc -fPIC -shared b.c -o libb.so
export LD_LIBRARY_PATH=/home/pwn/got:$LD_LIBRARY_PATH
#編譯a可執行程式
gcc a.c -L. -lb -o a
從下圖中可以看到shared變數的訪問和之前我沒靜態鏈接篇的訪問方式是一模一樣的,用的是當rip+偏移這種間接定址的方式來訪問三方模組的全局變數,而函數swap
在這裡則變成了swap@plt
。
利用斷點跟入swap@plt函數
,然後跟到了plt表
,後面會將plt表的用途,可以看到有個jmp是間接跳轉,加上偏移後剛好就是got表的位置,對應的是存放swap函數的絕對地址。
got表劫持小實驗
#include <stdio.h>
void fun()
{
system("id");
}
int main()
{
//下面演示:Printf("id") 變成shell命令
printf("id");
return 0;
}
最後成功劫持,將printf劫持成了system函數,輸出了當前id
PLT(延遲綁定)
PLT概念
首先, 我們要知道, GOT和PLT只是一種重定向
的實現方式. 所以為了理解他們的作用, 就要先知道什麼是重定向, 以及我們為什麼需要重定向.
重定向我在靜態鏈接文章中已經介紹過,就是編譯成.o文件時候,那些外部符號變數和函數無法確定時候,預留的填充值,比如用0填充,然後等待鏈接時候才真實的被寫入。
之前介紹的是靜態鏈接的情況,那麼動態鏈接時候會怎麼樣呢?一遍實戰一遍學習。
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(int argc,char *argv[])
{
print_banner();
return 0;
}
#編譯分別生成.o 和可執行程式
gcc -c plt.c -o plt.o -m32
gcc -o plt plt.c -m32
編譯後產生了.o和plt可執行程式,我們先用objdump來看看plt.o的彙編源碼,命令是objdump -M intel -dw plt.o
可以看到call printf的這個地址是填0的,因為這時候編譯器並不知道printf的函數真實地址,printf函數是需要程式被裝載後才能確定地址
,那麼動態鏈接器為什麼不在程式運行起來後,裝載起來後
再把真實的printf地址
填進去呢?因為這個call printf
的語句是在.text
程式碼段的,運行起來後程式碼段是無法被修改的,只能修改.data
數據段。
????????那怎麼搞啊,都不能修改程式碼段,那搞什麼。
只能羨慕大佬么的技巧,大佬么總是那麼騷,還是有辦法搞的,動態鏈接器生成了一段額外的小程式碼判斷,通過這段程式碼獲取printf函數地址,並完成對它的調用。
延遲綁定(PLT表)
用來存放這小片段程式碼的地方就是PLT表,下面是偽程式碼片段。
.text
....
//調用printf的call指令
call printf_plt
....
printf_plt:
mov rax,[printf函數的存儲地址] //GOT表中
jmp rax //跳過去執行printf函數
.got.plt
.....
printf下標
這裡存儲了printf函數重定位後的真實地址
鏈接階段發現printf
定義在動態庫 glibc
時,鏈接器生成一段小程式碼
print@plt
,然後printf@plt
地址取代原來的printf
。因此轉化為鏈接階段對printf@plt
做鏈接重定位,而運行時才對printf
做運行時重定位,具體調用流程圖,可以參考如下:
實戰學習
好的,接下來我們繼續用上面的例子,詳細對的PLT表進行分析,首先我們用命令objdump -M intel -dw plt
查看每個段的數據,有彙編則反彙編。
000003a0 <.plt>:
3a0: ff b3 04 00 00 00 push DWORD PTR [ebx+0x4]
3a6: ff a3 08 00 00 00 jmp DWORD PTR [ebx+0x8]
3ac: 00 00 add BYTE PTR [eax],al
...
000003b0 <puts@plt>:
3b0: ff a3 0c 00 00 00 jmp DWORD PTR [ebx+0xc]
3b6: 68 00 00 00 00 push 0x0
3bb: e9 e0 ff ff ff jmp 3a0 <.plt>
...
0000051d <print_banner>:
51d: 55 push ebp
51e: 89 e5 mov ebp,esp
...
53a: e8 71 fe ff ff call 3b0 <puts@plt>
...
547: c3 ret
+0000 0x56556fd8 e0 1e 00 00 00 00 00 00 00 00 00 00 90 cd e4 f7 │....│....│....│....│
+0010 0x56556fe8 b0 de df f7 00 00 00 00 80 58 e1 f7 00 00 00 00
OK,我們對上面的程式碼進行分析,收我們關注printf_banner
函數調用的printf
,這裡因為編譯優化的緣故printf變成了puts
,在53a
這裡可以看到調用了
puts@plt
,puts@plt這裡有3句彙編程式碼,分別是jmp到ebx+0xC值的地址,然後又push0,又jmp到0x3a0,因為這裡我們不知道ebx是什麼值,所以需要動態調試來一步步詳細的觀察下,用命令pwndbg,gdb plt
,start
,b printf_banner
c
,然後單步到call puts@plt
。
從上圖中可以發現,ebx的值是0x56556fd8
,這其實是got表裝載到記憶體後的地址,我們可以用readelf -SW plt
查看文件中got表的偏移。
可以發現偏移正好是fd8
,虛擬記憶體的起始地址加上fd8就是0x56556fd8
。
那麼如何查看程式載入的起始地址呢?可以藉助強大的pwndbg中的vmmap
命令來查看記憶體分布。
正好是(起始地址)0x56555000
+偏移(0xfd5
)=0x56556fd8
。
OK現在回歸正題,我們已經知道puts@plt
中的jmp是要跳轉到GOT表中偏移0xC的位置,那麼這個位置存放的是什麼值呢?
聰明的你已經猜到了,他其實就是puts
函數的真實地址,但是!
為了不影響程式運行的速度,因為我們程式一運行就把所有符號地址都確定,然後都填入got表,那一但我們調用到非常的動態庫時候,性能肯定會受影響的。所以,採用了延遲綁定機制。
000003b0 <puts@plt>:
3b0: ff a3 0c 00 00 00 jmp DWORD PTR [ebx+0xc]
3b6: 68 00 00 00 00 push 0x0
3bb: e9 e0 ff ff ff jmp 3a0 <.plt>
延遲綁定機制原理
我們先來看看,這個got表偏移+0xC位置,在文件位置中的值是多少,可以看到他的值是0x3B6
,你可以仔細看看puts@plt
函數,jmp後下一句彙編地址是多少?
00001ee0
00000000
00000000
000003B6 [ebx+0xC]
剛好是0x3b6
,對應的彙編語句是push 0,接著又跳到了jmp 0x3a0 <.plt>,跳到了plt表。
3b6: 68 00 00 00 00 push 0x0
plt表中的彙編如下:
這幾句彙編程式碼會調用內核的_dl_runtime_resolve()
函數,把puts函數在動態庫中的真實地址
放入到got表
中。
000003a0 <.plt>:
3a0: ff b3 04 00 00 00 push DWORD PTR [ebx+0x4]
3a6: ff a3 08 00 00 00 jmp DWORD PTR [ebx+0x8]
3ac: 00 00 add BYTE PTR [eax],al
所以延遲綁定機制的原理:就是第一次在調用函數時候,才把真實的地址放入got表(進行綁定),之後再調用這函數則直接jmp到真實地址。
最後,在其他大佬部落格上偷了張詳細的函數調用plt表延遲綁定的流程圖。
參考文獻:
//yjy123123.github.io/2021/12/06/延遲綁定過程分析/
//evilpan.com/2018/04/09/about-got-plt/ 非常完整詳細的講解部落格
《程式設計師的自我修養 鏈接、裝載與庫》 這本書,真的是神書,全部仔細看完肯定有幫助。