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()
", 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...
");
sleep(3);
printf("Waked up...
");
}
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