向正在运行的Linux应用程序注入代码

来源:互联网 发布:网络诗选 编辑:程序博客网 时间:2024/04/29 15:15

翻译:0×80

1、简介

假设Linux上正在运行某程序,像Unix守护程序等,我们不想终止该程序,但是同时又需要更新程序的功能。首先映入脑海的可能是更新程序中一些已知函数,添加额外的功能,这样就不会影响到程序已有的功能,且不用终止程序。考虑向正在运行的程序中注入一些新的代码,当程序中已存在的另一个函数被调用时触发这些新代码。也许这种想法有些异想天开,但并不是不能实现的,有时我们确实需要向正在运行的程序中注入一些代码,当然其与病毒的代码注入技术与存在一定关联。

在本文中,我会向读者解释如何向正在Linux系统上运行的程序中注入一段C函数代码,而不必终止该程序。文中我们会讨论Linux目标文件格式Executable and Linkable Format(ELF),讨论目标文件sections(段)、symbols(符号)以及relocations(重定位)。

2、示例概述
笔者会利用以下简单的示例程序向读者一步步解释代码注入技术。示例由以下三部分组成:

1)由源码dynlib.hdynlib.c编译的动态(共享)库libdynlib.so2)由源码app.c编译的app程序,会链接libdynlib.so3injection.c文件中的注入函数

下面看一下这些代码:

//dynlib.hextern 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]$ ./app1 : PID 25658 : In print()Going to sleepWaked up2 : PID 25658 : In print()Going to sleepWaked up3 : PID 25658 : In print()Going to sleep

4、调试应用程序
程序app只是一个简单的循环程序,这里我们假设其已经运行了几周,在不终止该程序的情况下,将我们的新代码注入到该程序中。在注入过程中利用Linux自带的功能强大的调试器gdb。首先我们需要利用pid(见程序的输出)将程序附着到gdb:

[0x80@localhost dynlib]$ gdb app 25658GNU 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 arewelcome 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 25658Reading 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.soReading symbols from /lib/libc.so.6…done.Loaded symbols for /lib/libc.so.6Reading symbols from /lib/ld-linux.so.2…done.Loaded symbols for /lib/ld-linux.so.20×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/maps00111000-00112000 rwxs 00000000 03:02 57933979 /home/0x80/dynlib/injection.o00464000-00465000 r-xp 00464000 00:00 0 [vdso]00500000-00501000 r-xp 00000000 03:01 5464089 /usr/lib/libdynlib.so00501000-00502000 rw-p 00000000 03:01 5464089 /usr/lib/libdynlib.so007bb000-007d4000 r-xp 00000000 03:01 1311704 /lib/ld-2.4.so007d4000-007d5000 r--p 00018000 03:01 1311704 /lib/ld-2.4.so007d5000-007d6000 rw-p 00019000 03:01 1311704 /lib/ld-2.4.so007d8000-00904000 r-xp 00000000 03:01 1311705 /lib/libc-2.4.so00904000-00907000 r--p 0012b000 03:01 1311705 /lib/libc-2.4.so00907000-00908000 rw-p 0012e000 03:01 1311705 /lib/libc-2.4.so00908000-0090b000 rw-p 00908000 00:00 008048000-08049000 r-xp 00000000 03:02 57933977 /home/ 0x80 /dynlib/app08049000-0804a000 rw-p 00000000 03:02 57933977 /home/ 0x80 /dynlib/app09ca5000-09cc6000 rw-p 09ca5000 00:00 0 [heap]b7f94000-b7f95000 rw-p b7f94000 00:00 0b7fa4000-b7fa6000 rw-p b7fa4000 00:00 0bfb91000-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 appRelocation section ‘.rel.dyn at offset 0×338 contains 1 entries:Offset Info Type Sym.Value Sym. Name08049678 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. Name08049688 00000107 R_386_JUMP_SLOT 00000000 print0804968c 00000207 R_386_JUMP_SLOT 00000000 puts08049690 00000407 R_386_JUMP_SLOT 00000000 sleep08049694 00000607 R_386_JUMP_SLOT 00000000 __libc_start_main08049698 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.oSymbol table ‘.symtab contains 11 entries:Num: Value Size Type Bind Vis Ndx Name0: 00000000 0 NOTYPE LOCAL DEFAULT UND1: 00000000 0 FILE LOCAL DEFAULT ABS injection.c2: 00000000 0 SECTION LOCAL DEFAULT 13: 00000000 0 SECTION LOCAL DEFAULT 34: 00000000 0 SECTION LOCAL DEFAULT 45: 00000000 0 SECTION LOCAL DEFAULT 56: 00000000 0 SECTION LOCAL DEFAULT 77: 00000000 0 SECTION LOCAL DEFAULT 68: 00000000 25 FUNC GLOBAL DEFAULT 1 injection9: 00000000 0 NOTYPE GLOBAL DEFAULT UND print10: 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.oThere 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 1Key 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.oRelocation section ‘.rel.text at offset 0×360 contains 3 entries:Offset Info Type Sym.Value Sym. Name00000007 00000902 R_386_PC32 00000000 print0000000e 00000501 R_386_32 00000000 .rodata00000013 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) qA debugging session is active.Inferior 1 [process 25658] will be detached.Quit anyway? (y or n) yDetaching 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 20124: PID 25658: In print()Going to sleep Waked up Thu Oct 12 20:09:43 IST 20125: PID 25658: In print()Going to sleep Waked up Thu Oct 12 20:09:46 IST 20126: PID 25658: In print()Going to sleep Waked up Thu Oct 12 20:09:49 IST 20127: PID 25658: In print()Going to sleep Waked up 

9、结论
  在本文中,笔者演示了如何向正在运行于Linux系统上的应用程序注入一个C函数,而不必终止该程序。需要注意的是当前用户必须是被注入的进程的,或者拥有对进程内存处理的相应权限。

转自:http://www.freebuf.com/articles/system/6388.html

0 0
原创粉丝点击