原作者: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()函数。
![](/qrcode.jpg)
//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 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段而言的。
我们需要手动解决以上三个重定位,为这三个内存位置设置适当的地址。程序进程地址空间中的这些重定位地址是通过求和计算出来的:
(1)injection.o在进程地址空间中的起始地址(0×00111000)。
(2).text段在injection.o目标文件中的起始偏移量(0×000034)。
(3)相对于.text段的重定位偏移量(print为0×00000007, .rodata为0x0000000e,system为0×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函数,而不必终止该程序。需要注意的是当前用户必须是被注入的进程的,或者拥有对进程内存处理的相应权限。