vlambda博客
学习文章列表

Linux | 浅学一下,进程内存取证分析

We don't use the word 'intelligence' with software. We regard that as a naive idea. We say that it’s 'complex.' Which means that we don't

always understand what it’s doing. —— Orson Scott Card, Ender's Shadow

浅学一下,Linux进程内存取证思路,参考O'Neill(ELFmaster)。

1. 进程内存布局

取证时可以通过分析该文件,获得特定文件的位置或者进程中的内存映射。

下面使用dmesg程序进程的内存布局示例:

1.1 可执行文件内存映射

1-3行为可执行文件的内存映射,显示了文件路径。

第一行为text段,权限为可读、可执行。

第二行为data段第一部分,因为使用了RELRO(只读重定位)保护,因此只标记为只读。

第三行为data端的剩余部分,权限为可写。

 00400000-00408000 r-xp 00000000 fd:00 134786044 /usr/bin/dmesg 00607000-00608000 r--p 00007000 fd:00 134786044 /usr/bin/dmesg 00608000-0060b000 rw-p 00008000 fd:00 134786044 /usr/bin/dmesg

1.2 程序堆区

在ASLR之前,堆区位于data段后面,在ASLR出现之后,堆区是内存中随机映射的,只不过在maps文件中显示在data段之后。

 01eca000-01eeb000 rw-p 00000000 00:00 0 [heap]

当调用 malloc()请求的内存块大小超过 MMAP_THRESHOLD 时, 会创建匿名内存段。这种类型的匿名内存段不会被标上[heap]标签。

MMAP_THRESHOLD:默认值为128KB,在32位系统上最大值为512KB,64位系统上的最大值为32MB,由于默认开启mmap分配阈值动态调整,该字段的值会动态修改,但不会超过最大值。

1.3 共享库映射

共享库 libc-2.17.so 的内存映射。可以看到位于 text 段和data 段之间的内存映射没有权限标志。这样做的目的就是占用 text 段和 data段之间的内存空间,从而无法创建任意的内存映射。

 7f38fc1f8000-7f38fc3bc000 r-xp 00000000 fd:00 202460984 /usr/lib64/libc-2.17.so 7f38fc3bc000-7f38fc5bb000 ---p 001c4000 fd:00 202460984 /usr/lib64/libc-2.17.so 7f38fc5bb000-7f38fc5bf000 r--p 001c3000 fd:00 202460984 /usr/lib64/libc-2.17.so 7f38fc5bf000-7f38fc5c1000 rw-p 001c7000 fd:00 202460984 /usr/lib64/libc-2.17.so
 7f38fc5c6000-7f38fc5e8000 r-xp 00000000 fd:00 201326730 /usr/lib64/ld-2.17.so 7f38fc7d6000-7f38fc7dd000 r--s 00000000 fd:00 201328831 /usr/lib64/gconv/gconv-modules.cache 7f38fc7dd000-7f38fc7e0000 rw-p 00000000 00:00 0 7f38fc7e5000-7f38fc7e7000 rw-p 00000000 00:00 0 7f38fc7e7000-7f38fc7e8000 r--p 00021000 fd:00 201326730 /usr/lib64/ld-2.17.so 7f38fc7e8000-7f38fc7e9000 rw-p 00022000 fd:00 201326730 /usr/lib64/ld-2.17.so 7f38fc7e9000-7f38fc7ea000 rw-p 00000000 00:00 0

1.4 栈、VDSO 和 vsyscall

在映射文件的末尾是栈段、VDSO(虚拟动态共享目标文件)和vsyscall:

 7ffdefd0f000-7ffdefd30000 rw-p 00000000 00:00 0 [stack] 7ffdefd56000-7ffdefd58000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

glibc使用VDSO来调用一些常用到的系统调用。VDSO通过执行用户层特定的系统调用来进行加速执行。在x86_64位系统上,vsyscall页已经被弃用了,不过在32位系统上,vsyscall的功能跟VDSO相同。进程布局如下:

Linux | 浅学一下,进程内存取证分析

2. 进程注入技术

(1)注入相关

共享目标文件注入(ET_DYN):这种方式是通过ptrace()系统调用和一段使用了mmap()或者__libc_dlopen_mode()函数来加载共享库文件的shellcode实现的。共享目标文件本身可能不是一个真正的共享目标文件。有可能是一个PIE可执行文件

重定位目标文件注入(ET_REL):将一个重定位目标文件注入到进程中以便于进行热补丁。可以使用ptrace系统调用将shellcode注入到进程中,利用shellcode将目标文件映射到内存中。

PIC代码注入:通常使用ptrace将shellcode注入到进程中。注入shellcode是往进程中注入更加复杂的代码(ET_DYN和ET_REL文件)的第一个阶段。

(2)劫持可执行文件

内联函数钩子(Inline Hook):攻击者会使用一个jmp指令替换函数代码中前5-7个字节,这个jmp跳转指令能够将控制转向一个恶意的函数。可以通过扫描所有函数的初始字节代码来检测内联函数钩子。

3. 共享库注入取证

Jynx2 rootkit 是基于 LD_PRELOAD 技术的用户级 Rootkit。本节使用GDB和Linux环境变量来检查被感染的进程。

注:共享目标文件、共享库、DLL、ET_DYN是同义词。

编写demo,调用fopen函数,即加载了glibc库中的函数。

 #include <stdio.h>
 
 int main(int argc, char **argv)
 {
     FILE *fp;
     char buff[256];
 
     fp = fopen("test.txt", "r");
     fgets(buff, 256, fp);
     printf("\n%s\n", buff);
     fclose(fp);
     getchar();
     return 0;
 }

3.1 分析进程的地址空间

Linux | 浅学一下,进程内存取证分析Linux | 浅学一下,进程内存取证分析

        重点关注下面的信息,/XxJynx/jynx2.so明显不是标准的共享库的路径,意味着可能进行了共享库的注入,这种情况下需要首先对LD_PRELOAD环境变量进行检查。

7efc1408b000-7efc14090000 rw-p 00000000 00:00 07efc14090000-7efc14096000 r-xp 00000000 fd:00 134354322 /XxJynx/jynx2.so7efc14096000-7efc14295000 ---p 00006000 fd:00 134354322 /XxJynx/jynx2.so7efc14295000-7efc14296000 r--p 00005000 fd:00 134354322 /XxJynx/jynx2.so7efc14296000-7efc14297000 rw-p 00006000 fd:00 134354322 /XxJynx/jynx2.so7efc14297000-7efc142b9000 r-xp 00000000 fd:00 201326730 /usr/lib64/ld-2.17.so7efc144a8000-7efc144b1000 rw-p 00000000 00:00 07efc144b5000-7efc144b8000 rw-p 00000000 00:00 07efc144b8000-7efc144b9000 r--p 00021000 fd:00 201326730 /usr/lib64/ld-2.17.so7efc144b9000-7efc144ba000 rw-p 00022000 fd:00 201326730 /usr/lib64/ld-2.17.so7efc144ba000-7efc144bb000 rw-p 00000000 00:00 07fff25bdb000-7fff25bfc000 rw-p 00000000 00:00 0 [stack]7fff25bfe000-7fff25c00000 r-xp 00000000 00:00 0 [vdso]ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

3.2 查找栈中的LD_PRELOAD

 栈顶 栈底7fff25bdb000-7fff25bfc000 rw-p 00000000 00:00 0 [stack]
gdb -q attach `pidof demo`x/2048s (0x7fff25bfc000 - 2048)

并没有发现LD_PRELOAD环境变量被修改,故可以检查PLT/GOT(PLT的全局偏移表)中函数劫持的情况。

实际上,Jynx2将编译成的恶意动态链接库复制在LD_PRELOAD的默认配置文件/etc/ld.so.preload中,以此实现全局劫持,并未修改环境变量,所以无法找到。Linux | 浅学一下,进程内存取证分析

3.3 分析PLT/GOT钩子

检查PLT/GOT(PLT的全局偏移表)中函数劫持

在检查位于 ELF 中的.got.plt 节(在可执行文件的 data 段)的 PLT/GOT之前,先看一下./demo 程序中的哪个函数设置了 PLT/GOT 相关的重定位。全局偏移表的重定位条目是<ARCH>_JUMP_SLOT 类型的。可以看到demo中调用了几个常见的 glibc 函数。可能有一部分或者全部的函数被恶意的共享库 jynx2.so 劫持。

(gdb) x/gx 0x6010480x601048 <fopen@got.plt>: 0x00007efc140933da
#/proc/PID/maps7efc14090000-7efc14096000 r-xp 00000000 fd:00 134354322 /XxJynx/jynx2.so

3.4 共享库注入的其他方法

使用 LD_PRELOAD 对恶意的共享库进行预加载,很容易就能识别出可疑的共享库。

实际上,有许多其他形式的恶意软件会通过 ptrace()或者使用了 mmap()/__libc_dlopen_mode()的 shellcode 注入共享库。

判断是否进行了共享库注入的其他方法,需要先了解其他的共享库注入。

共享库远程注入:将共享库注入到已经存在的进程中。注入共享库后,需要通过PLT/GOT重定向、函数蹦床(function trampoline)等将控制流重定向到共享库。

利用LD_PRELOAD:在程序执行时加载共享库,可以通过设置LD_PRELOAD环境变量,将我们想要的共享库放在其他共享库之前加载。不过对已存在进程没有影响。

#define DLOPEN_MODE_FLAG 0X8000000
/* Taken from Saruman's launcher.c */#define __RTLD_DLOPEN 0x80000000 //glibc internal dlopen flag#define __BREAKPOINT__ __asm__ __volatile__("int3");
#define __RETURN_VALUE__(x) __asm__ __volatile__("mov %0, %%rax\n" :: "g"(x))__PAYLOAD_KEYWORDS__ void * dlopen_load_exec(const char *path,void *dlopen_addr){ void * (*libc_dlopen_mode)(const char *, int) = dlopen_addr; void *handle; handle = libc_dlopen_mode(path, __RTLD_DLOPEN|RTLD_NOW|RTLD_GLOBAL); __RETURN_VALUE__(handle); __BREAKPOINT__;}

        dlopen()也可以装载 PIE 可执行文件。这就意味着可以将一个完整的程序注入到进程中并执行。事实上,可以在单个进程中运行任意程序。这是一项很好的反取证分析技术,在使用线程注入时,可以让注入的线程并发地执行。

        ELFmaster设计了一款名为 Saruman 的工具,就可以将程序注入到进程中。工具中使用了两种注入方法:open()/mmap()方法,使用手动重定位或者使用__libc_dlopen_mode()方法。

        saruman:http://www.bitlackeys.org/#saruman

3.5 总结共享库注入取证思路

1.从/proc/PID/maps文件中获取共享目标文件路径的列表。

2.检查可执行文件中是否存在与查看的共享库相对应的有效DT_NEEDED条目。如果存在,则说明是合法的共享库。在验证了给定共享库的合法性之后,可以检查共享库的动态段,并枚举动态段中的DT_NEEDED条目。所有与之对应的共享库都可以被标记为合法的。这就是传递共享目标文加载的概念。

3.查看进程对应的实际可执行程序的PLT/GOT。如果使用了任何的dlopen调用,则继续对代码进行分析,找到所有的dlopen调用。可以对dlopen中传递的参数进行静态检查。例如:

 void *handle = dlopen("somelib.so", RTLD_NOW);

字符串会作为静态常量存储于二进制文件的.rodata节中。因此,可以检查.rodata节中是否存在想要验证的共享库文件的路径。

4.如果映射文件中的共享目标文件路径没有对应的DT_NEEDED条目,也没有任何的dlopen调用,那么这个共享目标文件可能是通过LD_PRELOAD预加载进来的。或者是通过其他方式注入的。此时,就可以认为该共享目标文件是异常的。

3.6 检测PLT/GOT钩子工具

以下工具本质上都是使用上述的思路实现的。

Linux VMA Voodoo:检测多种类型的进程内存感染,目前只能在 32位的系统上工作。在 http://www.bitlackeys.org/#vmavudu 查看 VMA Voodoo 的相关内容。

ECFS(扩展核心文件快照): 这项技术最初是作为 Linux 中的进程内存取证分析工具的本地快照格式而设计的,现在已经演变成一项更加复杂的技术,详情见 https://github.com/elfmaster/ecfs

Volatility plt_hook:Volatility 软件主要面向整个系统的内存分析。GeorgWicherski 在 2013 年设计了一个插件, 专门用于检测进程中的 PLT/GOT感染。该插件使用的启发方法跟之前讨论的非常类似。此功能已经与 Volatility 的 源 码 合 并 了,详情见 https://github.com/volatilityfoundation/volatility

reference

《Learning Binary Analysis》elfmaster-ONeill