[pwn基礎]動態連結原理

2022-06-15 18:02:57

[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 pltstart,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表延遲繫結的流程圖。

參考文獻:

https://yjy123123.github.io/2021/12/06/延遲繫結過程分析/

https://evilpan.com/2018/04/09/about-got-plt/ 非常完整詳細的講解部落格

《程式設計師的自我修養 連結、裝載與庫》 這本書,真的是神書,全部仔細看完肯定有幫助。