如何用幾行代碼打造應用程序熱補丁?(二)

前言

在《如何用幾行代碼打造應用程序熱補丁?(一)》中,我們介紹了應用程序熱補丁技術的基本原理,同時實現了一個簡單的熱補丁。但是無法對本地函數打熱補丁,同時手動編寫熱補丁比較麻煩、可能非常復雜容易出錯。

為了解決這些問題,本文將會介紹一種自動生成應用程序熱補丁技術,可以生成應用程序和動態鏈接庫中任意函數的熱補丁。

自動生成熱補丁綜述

自動生成熱補丁是利用熱補丁生成工具,對現有的源代碼和補丁文件進行處理,自動輸出熱補丁的技術。

我們知道,熱補丁的基本原理是新函數替換舊函數,也就是完整的函數的替換。補丁中可能包含一個或多個函數的修改,這些被修改的函數都會被替換掉。上一篇文章介紹過,熱補丁首先把新函數放入目標進程的內存中,然后修改舊函數的入口,使之跳轉到新函數。

自動生成熱補丁中最主要的部分是自動生成新函數的二進制代碼,也稱為是替換代碼。在生成替換代碼時,主要由以下部分組成:

自動生成替換代碼。

解析替換代碼中使用的符號。

接下來會對以上部分進行詳細的介紹。在介紹之前,必須假設系統環境是Linux X86/X86_64,應用程序是C語言編譯鏈接的ELF格式可執行文件,并且擁有原始程序的源代碼。

前后代碼比較生成替換代碼

1. 動機與挑戰

為了生成替換代碼,首先需要知道代碼修復之后,哪些函數發生了改變,然后根據這些改變,生成替換代碼。使用一種二進制比較的方法,通過比較原始程序的二進制和修復后程序的二進制,提取出生成替換代碼所需的全部信息。

我們選擇在目標文件(object file)的級別,進行前后比較。這樣做的好處是顯而易見的:

首先,任何源代碼的改變都會在目標文件的二進制代碼中顯示出來。舉個例子,頭文件.h文件中函數的參數如果從int變成了long long,調用這個函數的.c文件由于隱含的類型轉換,并不發生改變。如果在前后代碼對比發生在源代碼級別,甚至預處理之后,也不能發現前后.c文件發生了改變。

其次,我們不需要處理語言級別的特性,比如inline關鍵字、隱含類型轉換、宏等等。這些語言相關的特性可能隨著語言的發展會愈發復雜,并且我們也不希望熱補丁局限于某種語言。C語言、C++、匯編都是我們希望可以處理的語言。

最后,我們只關心代碼的二進制表達。在目標文件的級別,二進制代碼和代碼組成信息是最完整的,在此進行前后代碼比較也是最合理的。

所以,生成替換代碼的思路是,通過比較前后編譯的目標文件,提取出差異部分,組成替換代碼。

比較目標文件也面臨很多挑戰,主要在于如何從提取出發生改變的函數。舉個例子,由于目標文件中默認所有的函數代碼都會放在.text段,同時.text段中的相對地址跳轉都是相對于.text段的。也就是說,如果某個函數發生了改變,就算是一行代碼的改變,后面的相對地址跳轉也很可能會發生改變(由于符號位置發生了改變)。

2. 解決辦法

我們對目標應用程序代碼和修復后代碼分別編譯,逐一比較兩次編譯產生的若干個目標文件。如圖所示:

首先,我們編譯原始源代碼,保留所有中間過程中產生的目標文件。

然后,我們對原始源代碼打上修復補丁,再次編譯,構建系統(make)一般只會編譯改變的源文件,保留新生成的目標文件。如果沒有構建系統或者構建系統不如期工作,可以保留所有的目標文件。

最后,我們比較新生成的目標文件和對應的原始代碼編譯出來的目標文件,提取出差異部分,組成替換代碼,生成熱補丁。

在比較過程中,我們希望做到可以在函數級別上進行比較,這樣可以只提取出發生改變的函數,并且我們也希望生成的替換代碼是地址無關的,因為替換代碼可能被加載到任意的內存地址。

GCC編譯器提供了-ffunciton-sections和-fdata-section的編譯選項,作用是把函數和變量放入目標文件中獨立的段(每個函數代碼都由獨立的段來表示)。這樣編譯出的函數代碼都是地址無關的、更加通用的二進制,可以提取到替換代碼中被加載到內存的任意位置運行。

在對前后目標文件比較的時候,我們在ELF段的級別進行比較(也就是函數的級別,因為函數都在自己獨立的段中)。因為目標文件是ELF文件,遵守通用的標準。我們可以解析目標文件的ELF Header,找到段的開頭(Section Header),由此找到所有的段。這里我們建議使用一些ELF解析庫,如libelf、libbfd等。

逐一比較前后目標文件中的表示代碼的段,段的內容就是函數的代碼,找到發生改變的函數:

首先,比較段的大小,如果大小不同,說明函數發生了改變。

接著,對段的內容進行比較,如果某個字節不同,而且字節不屬于重定向的一部分,說明函數發生了改變。如果是重定向,檢查重定向計算之后的指令內容是否相同,如果不同,說明函數發生了改變(引用了與之前不同的函數或者變量)。

最后,如果段的大小和段的內容都沒有改變,說明函數沒有改變。

需要注意的是,如果補丁中修改的是宏或inline函數,我們無法做到只提取宏或者inline函數的差異,因為宏和inline函數會在編譯過程中被放置在其調用函數中,所有調用函數都會改變,所以提取的是所有調用的函數。

此時的替換代碼還不能直接運行,因為替換代碼中還可能包含對其他符號的引用,我們需要在運行替換代碼之前解析這些符號。

替換代碼中的符號解析

1. 動機與挑戰

我們的替換代碼中只包含改變的函數代碼,這些函數中可能會引用其他沒有改變的符號(函數或變量),所以我們需要根據符號引用的規則(一般為相對PC地址的相對地址),對引用符號手動重定向。

解析符號地址面臨的挑戰主要有兩個:

找到運行中的程序的符號所在的地址。

如果存在兩個或者以上相同名字的符號,找到正確符號。

2. 解決方法

符號的地址是在最終鏈接的時候決定的,如果可執行文件是pie(position independent executable)或者是動態鏈接庫,符號的地址是一個相對地址,是相對于可執行文件或者動態鏈接庫的代碼段在內存中地址的偏移,計算公式Addr = Base + Offset。其他情況,符號的地址是一個絕對地址,無需計算。

同樣名稱的符號在可執行文件中可能出現多次(例如兩個文件中定義了名字相同的static變量),我們需要從中找到正確的符號。在鏈接之后,可執行文件中的符號表會遵守一些固定的規則,相同源文件中符號會一起連續地記錄,并且在記錄的開頭會有類型為STT_FILE的符號,名字為源文件的名字。

舉個例子,我們在1.c和2.c中同時定義了static int a,最終可執行文件中的符號表如下所示:

# readelf -s exe
…
Symbol table '.symtab' contains 73 entries:
…    
41: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 1.c    
42: 0000000000601038     4 OBJECT  LOCAL  DEFAULT   25 a    
43: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 2.c    
44: 000000000060103c     4 OBJECT  LOCAL  DEFAULT   25 a

?因此我們可以通過file+sym規則找到正確的符號位置。假設函數引用變量a,并且函數在2.c源文件中,我們首先找到類型為STT_FILE的2.c這個符號,然后找到符號a就可以了。

我們知道符號的地址后可以對替換代碼中的引用進行手動重定向編寫,將編譯時符號引用的重定向轉換為我們自己在運行時可以識別的重定向(self relocation)。

首先,根據重定向類型和計算公式,計算出引用的符號,記錄符號的信息,其中符號的地址在運行時可以得到。

然后,記錄原有的重定向信息,其中重定向的地址在運行時可以得到。

最后,在替換代碼(熱補丁)被加載到目標程序的內存中時,通過之前記錄的重定向內容,修改替換代碼符號引用位置的內容,內容由重定向的類型、重定向的地址、符號的地址計算得到。

舉個例子,替換代碼中包含函數x,函數x會在地址P引用正在運行的程序中的函數y。當替換代碼被加載到程序的地址空間時,地址P可以被確定,函數y的符號地址S可以被確定,根據替換代碼中記錄的重定向信息(假設重定向類型是R_X86_64_PC32,Addend是A),那么地址P的內容需要被修改為S+A-P。這樣當函數x執行時才能引用到函數y的位置。

POC:生成替換代碼、符號解析

本章節利用objdump等工具,對生成替換代碼中的關鍵步驟進行POC驗證。

假設我們有目標程序T(修復后名為T-patched),包含如下代碼:

void print(int i)
{        
while (i) {                
printf("%d ", i--);        
}        
printf("\n");
}

void func()
{        
print(4);
}

?patch文件如下:

void func()
{
-       print(4);
+       print(6); 
}

?分別將源代碼和修復后的源代碼編譯,生成T.o和T-patched.o。

通過objdump我們可以發現func函數前后發生了改變,地址0x4的指令不同。(其他函數不變,省略)。如下所示:

# objdump -hdr -j .text.func T.o
…
Disassembly of section .text.func:
0000000000000000 :  
 0:   55                      push   %rbp  
 1:   48 89 e5                mov    %rsp,%rbp  
 4:   bf 04 00 00 00          mov    $0x4,%edi  
 9:   e8 00 00 00 00          callq  e                         
a: R_X86_64_PLT32       print-0x4  
 e:   c9                      leaveq  
 f:   c3                      retq?
# objdump -hdr -j .text.func T-patched.o
…
Disassembly of section .text.func:
0000000000000000 :  
 0:   55                      push   %rbp  
 1:   48 89 e5                mov    %rsp,%rbp  
 4:   bf 06 00 00 00          mov    $0x6,%edi  
 9:   e8 00 00 00 00          callq  e                         
a: R_X86_64_PLT32       print-0x4  
 e:   c9                      leaveq  
 f:   c3                      retq

?所以提取出T-patched.o中的.text.func段。這就完成了替換代碼的提取。

因為T-patched.o中的func函數在0xa的位置上引用了print函數,我們需要對print符號進行解析,在替換代碼(熱補丁)被加載到目標進程內存時,對0xa-0xd的四個字節重定向。

我們記錄下這個重定向,類型R_X86_64_PLT32,Addend -4,符號print。

在替換代碼被加載到目標進程中時,我們可以確定替換代碼加載的地址。假設替換代碼中func地址為0x7fa357ed79b0,原程序中print地址為0x7fa358a9979c。那么我們根據之前記錄的重定向信息進行計算(V = S + A - P),將func偏移0xa的位置(4字節)寫入0xbc1dde,計算方法如下:

0x7fa358a9979c + (-4) - 0x7fa357ed79ba = 0xbc1dde

?這樣重定向之后,替換代碼中的func函數就能正確引用到原程序中的print函數。

最后,我們通過GDB觀察T程序打入熱補丁之后的行為:

(gdb) disas func
Dump of assembler code for function func:  
 0x00007fa358a997d8 <+0>:     movabs $0x7fa357ed79b0,%rax  
 0x00007fa358a997e2 <+10>:    jmpq   *%rax
…

以上可以看出0x7fa357ed79b0是熱補丁中func函數的入口,也能看出0x00007fa357ed79b9地址的指令中引用了正確的print符號。

我們通過objdump、gdb驗證了替換代碼的靜態和動態形式,展示了自動生成熱補丁的具體細節,希望可以借此讓讀者對此有更清晰的理解。

其他注意事項

這種前后目標文件比較生成替換代碼的方法,要求我們必須擁有正在運行的目標程序的源代碼。同時,在編譯目標文件時強烈建議使用和目標程序相同版本的gcc和編譯選項,使用不同的gcc和不同的選項可能會導致目標文件和原始程序的二進制不匹配,導致不能生成正確的替換代碼。不正確的替換代碼可能會導致符號解析錯誤,進而是程序崩潰,這里需要特別注意。

寫在最后

本文介紹了二進制比較方式的自動生成熱補丁技術,相比于上一篇文章中介紹的簡單熱補丁技術,優勢在于:

通過工具自動生成熱補丁,無需手動編寫熱補丁,減少了人為出錯的可能。

可以修復本地函數和全局函數,并且不需要引入函數的依賴鏈。

兼容多種編譯型語言(C語言、C++、匯編等,具體實現不同,但是思路一致)。

使用這種熱補丁生成技術,可以解決應用程序幾乎所有的安全漏洞。例如最近出現的QEMU CVE-2017-2615(cirrus驅動內存越界訪問),我們對有bug的函數都打上了熱補丁,通過替換bug函數,實現了在線熱修復。

生成熱補丁是應用程序熱補丁技術框架中非常關鍵的一個組件,本文介紹了一種自動生成熱補丁的技術,但是完整、成熟的熱補丁框架還包含了其他技術,例如多線程、管理多個熱補丁、多版本管理、熱補丁與程序之間的一致性檢查等等。接下來,在最后一篇文章中,我們會對這些問題進行解答,并介紹UCloud應用程序熱補丁技術的完整框架,對框架中各個組件進行解析。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容