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

admin 2022年5月5日11:07:17评论551 views字数 8010阅读26分42秒阅读模式

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. 进程内存布局

/proc/PID/maps是Linux系统中非常重要的虚拟文件系统。通过此接口,可以访问运行中的程序对应的整个进程地址空间信息。

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

打了Grsecurity(一套增强 Linux 内核安全的内核补丁集)补丁的Linux内核,有一个内核选项GRKERNSEC_PROC_MEMMAP,如果开启此选项,就会清空/proc/PID/maps文件,此时无法获取地址空间的值。增加了进程分析的难度。

下面使用dmesg程序进程的内存布局示例:Linux | 浅学一下,进程内存取证分析

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

动态链接库也是共享库的一种。可以通过查看libc映射之后的文件映射,来查看共享库映射的地址空间。

 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)劫持可执行文件

PLT/GOT重定向:劫持共享库函数最常见的方式是修改给定共享库的GOT条目,这样就可以用地址反映出攻击者注入代码的位置。这种方式跟重写函数指针类似。

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

劫持VDSO拦截系统调用:映射到进程地址空间中的VDSO页保存了用于进行系统调用的代码。攻击者可以使用ptrace(PTRACE_SYSCALL,...)来定位到这些代码,然后使用想要进行的系统调用来替换%rax寄存器。

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%sn", buff);
     fclose(fp);
     getchar();
     return 0;
 }

3.1 分析进程的地址空间

    分析进程的第一步是映射出进程的地址空间。最直接的方式是查看/proc/<pid>/maps 文件。这里需要记下任何异常的文件映射和有异常权限的段。在下面的示例中,需要检查存放环境变量的栈,因此也需要记录栈在内存中的位置。

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

在程序运行时,程序的环境变量存储在栈底。栈是从高地址向低地址分配。/proc/PID/maps文件中可以看到栈的位置。

    栈顶          栈底7fff25bdb000-7fff25bfc000 rw-p 00000000 00:00 0                          [stack]

        可以通过GDB查看地址的内容,假设环境变量在栈的前2048的字节,可以从栈底-2048的地址开始查看。

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 劫持。Linux | 浅学一下,进程内存取证分析

识别 GOT 地址

GDB打印上述3个函数的GOT地址。

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

       发现fopen函数的地址 0x00007efc140933da 在 7efc14090000-7efc14096000 之间属于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 对恶意的共享库进行预加载,很容易就能识别出可疑的共享库。

Jynx2 Rootkit 会将编译成的恶意动态链接库复制在LD_PRELOAD的默认配置文件/etc/ld.so.preload中,以此实现全局劫持,并未修改环境变量。这种情况也容易识别出可疑的函数地址。

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

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

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

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

利用open()/mmap() shellcode:通过对已存在的进程的text段中注入shellcode(使用ptrace)并执行shellcode,利用共享库上的open/mmap操作,将任何文件(包括共享库)注入到进程的地址空间中。这个技术的需要解决的问题是大多数要注入到进程中的共享库都需要进行重定位。open()/mmap()函数只会将文件 加载到内存中,但是不会去处理代码重定位。

利用dlopen() shellcode:一个可执行文件在没有第一时间被链接的情况下,会使用dlopen()函数来动态加载共享库。程序可以通过dlopen()函数凭空加载一个共享库,实际上是调用了动态链接库来进行所有的重定位操作。存在一个问题,大多数进程没有可用的dlopen(),因为这个函数是在libdl.so.2中的,程序必须显示链接到libdl.so.2才能够调用dlopen()函数。幸好针对这个问题,存在一个解决方案:默认情况下几乎每个程序都有libc.so会一起映射到进程地址空间,并且在libc.so中有个和dlopen()函数类似的函数__libc_dlopen_mode()。这个函数只需要多设置一个标识。

#define DLOPEN_MODE_FLAG 0X8000000

        在使用__libc_dlopen_mode()之前,首先要进行远程解析:得到目标进程汇总libc.so的基址,解析__libc_dlopen_mode()的符号,然后将符号值st_value与libc基址相加得到__libc_dlopen_mode()的最终地址。然后可以使用C语言或者汇编语言设计shellcode,调用__libc_dlopen_mode(),来将共享库装载到进程中。然后就可以使用共享库中的__libc_dlsym()函数来对符号进行解析。

/* 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, %%raxn" :: "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

原文始发于微信公众号(TahirSec):Linux | 浅学一下,进程内存取证分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月5日11:07:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Linux | 浅学一下,进程内存取证分析https://cn-sec.com/archives/972198.html

发表评论

匿名网友 填写信息