向正在運行的Linux應用程序注入代碼

原作者:Gregory Shpitalnik
翻譯:0×80

1、簡介

假設Linux上正在運行某程序,像Unix守護程序等,我們不想終止該程序,但是同時又需要更新程序的功能。首先映入腦海的可能是更新程序中一些已知函數,添加額外的功能,這樣就不會影響到程序已有的功能,且不用終止程序。考慮向正在運行的程序中注入一些新的代碼,當程序中已存在的另一個函數被調用時觸發這些新代碼。也許這種想法有些異想天開,但並不是不能實現的,有時我們確實需要向正在運行的程序中注入一些代碼,當然其與病毒的代碼注入技術與存在一定關聯。

在本文中,我會向讀者解釋如何向正在Linux系統上運行的程序中注入一段C函數代碼,而不必終止該程序。文中我們會討論Linux目標文件格式Executable and Linkable Format(ELF),討論目標文件sections(段)、symbols(符號)以及relocations(重定位)。

2、示例概述
筆者會利用以下簡單的示例程序向讀者一步步解釋代碼注入技術。示例由以下三部分組成:

1)由源碼dynlib.hdynlib.c編譯的動態(共享)庫libdynlib.so
2)由源碼app.c編譯的app程序,會鏈接libdynlib.so
3injection.c文件中的注入函數

下面看一下這些代碼:

//dynlib.h
extern void print();

dynlib.h文件中聲明瞭printf()函數。

//dynlib.c
 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include "dynlib.h"
 extern void print()
 {
     static unsigned int counter = 0;
     ++counter;
     printf("%d : PID %d : In print()\n", counter, getpid());
 }

dynlib.c文件實現了print()函數,該函數只是打印一個計數(每次函數被調用時都會使該值增加)以及當前進程的pid。

//app.c
#include <stdio.h>
#include <unistd.h>
#include "dynlib.h"
int main()
{
     while(1)
     {
         print();
         printf("Going to sleep...\n");
         sleep(3);
         printf("Waked up...\n");
     }
     return 0;
}

app.c文件中的函數調用print()函數(來自libdynlib.so動態庫),之後睡眠幾秒鐘,然後繼續執行該無限循環。

//injection.c
#include <stdlib.h>
extern void print();
extern void injection()
{ 
     print();  //原本的工作,調用print()函數
     system("date");  //添加的額外工作
}

injection()函數調用會替換app.c文件中main()函數調用的print()函數調用。injection()函數首先會調用原print()函數,之後進行額外的工作。例如,它可以利用system()函數運行一些外部可執行程序,或者像本例中一樣打印當前的日期。

3、編譯並運行程序

首先利用gcc編譯器編譯這些源文件:

$ gcc -g -Wall dynlib.c -fPIC -shared -o libdynlib.so
$ gcc g app.c ldynlib L ./ -o app
$ gcc -Wall injection.c -c -o injection.o

編譯後的程序爲:

-rwxrwxr-x 1 0×80 0×80 6224 Oct 15 14:04 app
-rw-rw-r 1 0×80 0×80 888 Oct 16 17:53 injection.o
-rwxrwxr-x 1 0×80 0×80 5753 Oct 16 17:52 libdynlib.so

需要注意的是動態庫libdynlib.so在編譯時指定了-fPIC選項,用來生成地址無關的程序。下面運行app可執行程序:

[0x80@localhost dynlib]$ ./app
./app: error while loading shared libraries: libdynlib.so: cannot open shared object file: No such file or directory

如果產生以上錯誤,我們需要將生成的libdynlib.so文件拷貝到/usr/lib/目錄下,再執行該程序,得到如下結果:

[0x80@localhost dynlib]$ ./app
1 : PID 25658 : In print()
Going to sleep
Waked up
2 : PID 25658 : In print()
Going to sleep
Waked up
3 : PID 25658 : In print()
Going to sleep

4、調試應用程序
程序app只是一個簡單的循環程序,這裏我們假設其已經運行了幾周,在不終止該程序的情況下,將我們的新代碼注入到該程序中。在注入過程中利用Linux自帶的功能強大的調試器gdb。首先我們需要利用pid(見程序的輸出)將程序附着到gdb:

[0x80@localhost dynlib]$ gdb app 25658
GNU gdb Red Hat Linux (6.3.0.0-1.122rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type show copying to see the conditions.
There is absolutely no warranty for GDB. Type show warranty for details.
This GDB was configured as i386-redhat-linux-gnu”…Using host libthread_db library “/lib/libthread_db.so.1″.
Attaching to program: /home/0×80/dynlib/app, process 25658
Reading symbols from shared object read from target memorydone.
Loaded system supplied DSO at 0×464000
`shared object read from target memory’ has disappeared; keeping its symbols.
Reading symbols from /usr/lib/libdynlib.so…done.
Loaded symbols for /usr/lib/libdynlib.so
Reading symbols from /lib/libc.so.6…done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2…done.
Loaded symbols for /lib/ld-linux.so.2
0×00464410 in __kernel_vsyscall ()
(gdb)

5、將注入代碼加載到可執行程序的內存中
如前所述,目標文件injection.o初始並不包含在app可執行進程鏡像中,我們首先需要將injection.o加載到進程的內存地址空間。可以通過mmap()系統調用,該系統調用可以將injection.o文件映射到app進程地址空間中。在gdb調試器中:

(gdb) call open(“injection.o”, 2)
$1 = 3
(gdb) call mmap(0, 888, 1|2|4, 1, 3, 0)
$2 = 1118208
(gdb)

首先利用O_RDWR(值爲2)的讀/寫權限打開injection.o文件。一會之後我們在加載注入代碼時做寫修改,因此需要寫權限。返回值爲系統分配的文件描述符,可以看到值爲3。之後調用mmap()系統調用將該文件載入進程的地址空間。mmap()函數原型如下:

#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

函數包含6個參數:

start表示映射區的開始地址,設置爲0時表示由系統決定映射區起始地址。
length表示映射區的長度,這裏爲injection.o文件的長度,該值在前文第3節出現過。
prot表示期望的內存保護標誌(即映射權限),不能與文件的打開模式衝突,這裏爲1|2|4(即PROT_READ | PROT_WRITE | PROT_EXEC,讀/寫/執行)
flags指定映射對象的類型,映射選項和映射頁是否可以共享,
fd表示已經打開的文件描述符,這裏爲3。
offset表示被映射對象內容的起點,這裏爲0。
如果函數執行成功,則返回被映射文件在映射區的起始地址 
通過查看/proc/[pid]/maps的內容(這裏pid爲要注入的可執行進程的pid,本例爲25593),我們可以確定injection.o文件實際被映射到的進程地址空間,在Linux系統中,文件包含當前正在運行的進程的內存佈局信息

[0x80@localhost ~]$ cat /proc/25658/maps
00111000-00112000 rwxs 00000000 03:02 57933979 /home/0x80/dynlib/injection.o
00464000-00465000 r-xp 00464000 00:00 0 [vdso]
00500000-00501000 r-xp 00000000 03:01 5464089 /usr/lib/libdynlib.so
00501000-00502000 rw-p 00000000 03:01 5464089 /usr/lib/libdynlib.so
007bb000-007d4000 r-xp 00000000 03:01 1311704 /lib/ld-2.4.so
007d4000-007d5000 r--p 00018000 03:01 1311704 /lib/ld-2.4.so
007d5000-007d6000 rw-p 00019000 03:01 1311704 /lib/ld-2.4.so
007d8000-00904000 r-xp 00000000 03:01 1311705 /lib/libc-2.4.so
00904000-00907000 r--p 0012b000 03:01 1311705 /lib/libc-2.4.so
00907000-00908000 rw-p 0012e000 03:01 1311705 /lib/libc-2.4.so
00908000-0090b000 rw-p 00908000 00:00 0
08048000-08049000 r-xp 00000000 03:02 57933977 /home/ 0x80 /dynlib/app
08049000-0804a000 rw-p 00000000 03:02 57933977 /home/ 0x80 /dynlib/app
09ca5000-09cc6000 rw-p 09ca5000 00:00 0 [heap]
b7f94000-b7f95000 rw-p b7f94000 00:00 0
b7fa4000-b7fa6000 rw-p b7fa4000 00:00 0
bfb91000-bfba6000 rw-p bfb91000 00:00 0 [stack]
[0x80@localhost ~]$

可以看到/home/0×80/dynlib/injection.o起始於進程地址空間的0×00111000地址處(轉換成十進制即爲1118208),終止於地址空間的0×00112000地址處。以上輸出同時包含了其它動態庫的映射信息。現在我們已經將所有需要的組件加載到可執行進程的內存空間中了。

6、重定位
下面,我們從內部檢查ELF格式的二進制可執行文件程序app。我們使用Linux自帶的readelf程序,來顯示ELF格式的目標文件(Linux中的任意object文件、庫或可執行文件)中的不同數據,即查看app程序中的符號重定位信息。我們只對其中的print()函數調用的重定位感興趣。

[0x80@localhost dynlib]$ readelf -r app
Relocation section ‘.rel.dyn at offset 0×338 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
08049678 00000c06 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section ‘.rel.plt at offset 0×340 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
08049688 00000107 R_386_JUMP_SLOT 00000000 print
0804968c 00000207 R_386_JUMP_SLOT 00000000 puts
08049690 00000407 R_386_JUMP_SLOT 00000000 sleep
08049694 00000607 R_386_JUMP_SLOT 00000000 __libc_start_main
08049698 00000c07 R_386_JUMP_SLOT 00000000 __gmon_start__
[0x80@localhost dynlib]$

如讀者所見,print符號重定位位於app程序的絕對(虛擬)地址0×08049688偏移處,重定位的類型爲R_386_JUMP_SLOT。在程序被加載到內存且在運行之前,重定位地址是一個絕對虛擬地址。注意該重定位駐留在程序二進制鏡像的.rel.plt段內。PLT即Procedure Linkage Table的縮寫,是爲函數間接調用提供的表,即在調用一個函數是,不是直接跳轉到函數的位置,而是首先跳轉到Procedure Linkage Table的入口處,之後再從PLT跳轉到函數的實際代碼處。如果要調用的函數位於一個動態庫中(如本例中的libdynlib.so),那麼這種做法是必要的,因爲我們不可能提前知道動態庫會被加載到進程空間的什麼位置,以及動態庫中的第一個函數是什麼(本位中爲print()函數)。所有這些知識只在程序被加載到內存之後且運行之前有效,這時系統的動態鏈接器(Linux系統中爲ld-linux.so)會解決重定位的問題,使請求的函數能夠被正確調用。在本文的例子中,動態鏈接器會將libdynlib.so加載到可執行進程的地址空間,找到print()函數在庫中的地址,並將該地址設置爲重定位地址0×08049688。

我們的目標是用injection.o目標文件中injection()函數的地址替換print()函數的地址,該函數在程序剛開始運行之初並不包含在它的進程地址空間中。
更多關於ELF格式、重定位以及動態鏈接器的的信息,讀者可以參考Executable and Linkable Format(ELF)文檔。

我們可以檢查地址0×08049688正是函數print()函數的地址:

(gdb) p & print
$3 = (void (*)()) 0x50051c (gdb) p/x * 0×08049688
$4 = 0x50051c
(gdb)

injection()函數的地址可以通過對injection.o文件運行readelf –s(顯示目標文件的符號表)得到:

[0x80@localhost dynlib]$ readelf -s injection.o
Symbol table ‘.symtab contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS injection.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 6
8: 00000000 25 FUNC GLOBAL DEFAULT 1 injection
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND print
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND system
[0x80@localhost dynlib]$

函數(符號)injection位於injection.o文件.text段的偏移0處,但.text段起始於injection.o文件的偏移0×000034處:

[0x80@localhost dynlib]$ sudo readelf -S injection.o
There are 11 section headers, starting at offset 0xd4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000019 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000360 000018 08 9 1 4
[ 3] .data PROGBITS 00000000 000050 000000 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000050 000000 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 000050 000005 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 000055 00002d 00 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 000082 000000 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 000082 000051 00 0 0 1
[ 9] .symtab SYMTAB 00000000 00028c 0000b0 10 10 8 4
[10] .strtab STRTAB 00000000 00033c 000024 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
[0x80@localhost dynlib]$

7、用injection()函數替換print()函數
這裏提醒讀者,injection.o文件已經被加載到app進程內存空間的地址0×00111000處(見上文)。因此injection()函數的最終絕對虛擬地址爲0×00111000+0×000034.
下面用該地址替換print()函數的重定位地址0×08069688:

(gdb) set *0×08049688 = 0×00111000 + 0×000034
(gdb)

到這裏,我們已經成功用對injection()函數的調用替換了對print()函數的調用。

8、解決injection()函數的重定位

不過我們還有一些工作要做。injection()函數的代碼目前還不能運行,因爲我們仍有3個重定位沒有解決:

[0x80@localhost dynlib]$ readelf -r injection.o
Relocation section ‘.rel.text at offset 0×360 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
00000007 00000902 R_386_PC32 00000000 print
0000000e 00000501 R_386_32 00000000 .rodata
00000013 00000a02 R_386_PC32 00000000 system
[0x80@localhost dynlib]$

print重定位引用libdynlib.so庫中的print()函數調用,.rodata重定位指向保存在.rodata只讀數據段的“date”常量字符串(譯者注:即system(date)調用中的“date”),system重定位引用系統的system()函數調用。需要注意的是所有這三個重定位是駐留在.rel.text段中的,因此它們的偏移是相對於.text段而言的。

我們需要手動解決以上三個重定位,爲這三個內存位置設置適當的地址。程序進程地址空間中的這些重定位地址是通過求和計算出來的:

1injection.o在進程地址空間中的起始地址(0×00111000)。
2).text段在injection.o目標文件中的起始偏移量(0×000034)。
3)相對於.text段的重定位偏移量(print0×00000007 .rodata0x0000000esystem0×00000013)。

可以看到print與system的重定位類型爲R_386_PC32,意味着要設置的重定位地址的值應該利用程序計數寄存器PC來計算,這樣纔是相對於重定位地址的。

(譯者注:所謂重定位類型,就是規定了使用何種方式,去計算這個值,具體有哪些變量參與計算如同如何進行計算一樣也是不固定的,各種重定位類型有自己的規定。據規範裏面的規定,重定位類型R_386_PC32的計算需要有三個變量參與:S,A和P。其計算方式是 S+A-P。根據規範,當R_386_PC32類型的重定位發生在link editor鏈接若干個.o對象文件從而形成可執行文件的過程中的時候,變量S指代的是被重定位的符號的實際運行時地址,而變量P是重定位所影響到的地址單元的實際運行時地址。在運行於x86架構上的Linux系統中,這兩個地址都是虛擬地址。變量A最簡單,就是重定位所需要的附加數,它是一個常數。別忘x86架構所使用的重定位條目結構體類型Elf32_Rela,所以附加數就存在於受重定位影響的地址單元中。重定位最後將計算得到的值patch到這個地址單元中。)

R_386_32表示絕對地址的重定位,可以直接使用符號的地址;R_386_PC32表示對相對地址的重定位,要用“符號地址-重定位地址”得出相對地址。
R_386_32 類型規定只是將附加數加上符號的值作爲所需要的值,即.rodata的重定位需要在地址0×00111000的基礎上加上一個附加數。
計算方法如下:

(gdb) p & system
$7 = ( *) 0×733650 //system()函數的地址
(gdb) p * (0×00111000 + 0×000034 + 0×000000013)
$8 = -4 // system符號重定位的加數
(gdb) set * (0×00111000 + 0×000034 + 0×000000013) = 0×733650  (0×00111000 + 0×000034 + 0×000000013)  4

(gdb) p & print
$9 = (void (*)(void)) 0x40000be8 // print()函數的地址
(gdb) p * (0×00111000 + 0×000034 + 0×0000007)
$10 = -4 // print符號重定位的加數
(gdb) set * (0×00111000 + 0×000034 + 0×0000007) = 0x40000be8  (0×00111000 + 0×000034 + 0×0000007)  4

(gdb) p * (0×00111000 + 0×000034 + 0x0000000e)
$11 = 0 // .rodata符號重定位的加數
(gdb) set * (0×00111000 + 0×000034 + 0x0000000e) = 0×00111000 + 0×000050
//0×000050爲.rodata 段在injection.o目標文件中的偏移(見上文第6節結尾處)

解決了injection()函數代碼中的所有3個重定位,那麼要做的準備工作就做完了,可以退出gdb調試器了。應用程序會繼續運行,並且在此之後,除了繼續之前的打印工作,程序同時還會輸出當前的日期。

(gdb) q
A debugging session is active.
Inferior 1 [process 25658] will be detached.

Quit anyway? (y or n) y
Detaching from program: /home/0×80/dynlib/app, process 25658
[0x80@localhost dynlib]$ [lnx63:code_injection]

// app程序會繼續執行
Waked up 
Thu Oct 12 20:09:40 IST 2012
4: PID 25658: In print()
Going to sleep 
Waked up 
Thu Oct 12 20:09:43 IST 2012
5: PID 25658: In print()
Going to sleep 
Waked up 
Thu Oct 12 20:09:46 IST 2012
6: PID 25658: In print()
Going to sleep 
Waked up 
Thu Oct 12 20:09:49 IST 2012
7: PID 25658: In print()
Going to sleep 
Waked up 

9、結論
  在本文中,筆者演示瞭如何向正在運行於Linux系統上的應用程序注入一個C函數,而不必終止該程序。需要注意的是當前用戶必須是被注入的進程的,或者擁有對進程內存處理的相應權限。

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