ELF運行時注入
https://www.freebuf.com/articles/system/6388.html
原作者:Gregory Shpitalnik
翻譯:0×80
1、簡介
假設Linux上正在運行某程序,像Unix守護程序等,我們不想終止該程序,但是同時又需要更新程序的功能。首先映入腦海的可能是更新程序中一些已知函數,添加額外的功能,這樣就不會影響到程序已有的功能,且不用終止程序。考慮向正在運行的程序中注入一些新的代碼,當程序中已存在的另一個函數被調用時觸發這些新代碼。也許這種想法有些異想天開,但並不是不能實現的,有時我們確實需要向正在運行的程序中注入一些代碼,當然其與病毒的代碼注入技術與存在一定關聯。
在本文中,我會向讀者解釋如何向正在Linux系統上運行的程序中注入一段C函數代碼,而不必終止該程序。文中我們會討論Linux目標文件格式Executable and Linkable Format(ELF),討論目標文件sections(段)、symbols(符號)以及relocations(重定位)。
2、示例概述
筆者會利用以下簡單的示例程序向讀者一步步解釋代碼注入技術。示例由以下三部分組成:
(1)由源碼dynlib.h與dynlib.c編譯的動態(共享)庫libdynlib.so (2)由源碼app.c編譯的app程序,會鏈接libdynlib.so庫 (3)injection.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 memory…done. 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