Linux下监控所有进程的退出事件(x86 |
您所在的位置:网站首页 › 如何退出centumvp监控 › Linux下监控所有进程的退出事件(x86 |
本文详细展示Linux下进阶HOOK技术(inline hook),对于普通系统调用的hook文章可以看下面这几篇: hook系统调用完整实例 Linux ARM64平台上Hook系统调用(以openat为例) Linux MIPS64下hook系统调用(kylin server v10) 相信看过前面相关文章的朋友,都可以看出,hook系统调用其实还是比较简单(以open为例): 1、获取系统调用表首地址,可以看这篇; 2、替换系统调用为自定义的函数(my_hook_open)地址: sys_call_table[__NR_open]=my_hook_open; 注:本文适用于linux(x86-64平台)内核版本为4.17以下版本,若要适配4.17及以上内核可以看这篇文章及这篇。 但是,事情往往不是都那么简单,这种只能hook到系统调用表里的系统调用,即那些可以通过__NR_xxxx在系统调用表中找到地址并替换的系统调用。如果无法通过__NR_xxx找到系统调用地址,则无法完成hook。 比如我们想捕获到每个程序启动的信息,则就要hook系统调用execve,我们很容易想到了通过__NR_execve来完成,在x86_64上,如果你这么做了,那么会很不幸的造成系统崩溃,为何会这样呢,查看内核源码会发现__NR_execve对应的其实是stub_execve,不能简单的替换成sys_execve,还需要处理栈平衡,那么该如何做呢,本文暂且不讲,后面会另起文章介绍如何hook系统调用execve。 言归正传,如何捕获到进程退出信息呢。这里我写了个简单的打印hello world程序,我们可以strace查看到该程序从启动到退出所调用的系统调用: 可以看到正常的程序退出调用的是exit_group,其实exit_group是进程正常退出的系统调用,如果只捕获正常程序退出则hook下sys_exit_group即可。 但是进程的终止的方式不止一种,最常见的信号机制,比如通过kill命令终止进程,或者是进程崩溃退出,那么这些异常退出的信息如何捕获到呢。 其实,所有进程的终止最终都是调用了do_exit来处理的,不管是进程正常退出,还是进程接收到信号异常退出,都要经过do_exit最终处理,它主要是从操作系统中删除对当前进程的所有引用(对于所有非共享资源)。调用关系如下图所示: 这样一来,我们的问题就好解决了,我们可以hook掉do_exit来捕获进程退出信息。但是do_exit在系统调用表中并没有__NR_xxx来获取,无法通过文章刚开始提到的方法来hook。 本文介绍一种方法来解决:通过修改offset实现跳转到自定义函数内,不会在原系统调用中添加或覆盖任何新指令。具体实现且看下文分解。 本文中实验环境为centos7.3,内核版本为3.10.0-514.el7.x86_64。 我们来看下do_exit部分源码(kernel\exit.c): void do_exit(long code) { struct task_struct *tsk = current; int group_dead; profile_task_exit(tsk); WARN_ON(blk_needs_flush_plug(tsk)); ...... ptrace_event(PTRACE_EVENT_EXIT, code); validate_creds_for_do_exit(tsk); ....... exit_signals(tsk); /* sets PF_EXITING */ ...... exit_sem(tsk); exit_shm(tsk); exit_files(tsk); exit_fs(tsk); if (group_dead) disassociate_ctty(1); exit_task_namespaces(tsk); exit_task_work(tsk); check_stack_usage(); exit_thread(); ...... if (tsk->io_context) exit_io_context(tsk); if (tsk->splice_pipe) free_pipe_info(tsk->splice_pipe); if (tsk->task_frag.page) put_page(tsk->task_frag.page); validate_creds_for_do_exit(tsk); preempt_disable(); if (tsk->nr_dirtied) __this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied); exit_rcu(); ...... }可以看出do_exit中有调用profile_task_exit,而profile_task_exit也是系统导出函数,可以获取到它的函数地址。我们可以获取到do_exit跟profile_task_exit的入口地址,从do_exit入口地址开始,查看call指令的机器码,再往下找出跟profile_task_exit地址相符的,替换offset为我们自定义的my_profile_task_exit的offset,这样所有调do_exit里profile_task_exit的地方都被跳转到了my_profile_task_exit的地方,这样就达到了劫持进程退出信息的目的了。 上面这段可能看起来有点不太好理解,下面我具体通过代码来看下如何做的。 1、首先找到do_exit\profile_task_exit的声明 grep -nr do_exit /usr/src/ grep -nr profile_task_exit /usr/src/可以在系统/proc/kallsyms中,查看到这俩系统调用的地址: 然后在代码中可以如下声明: void do_exit(long error_code); void profile_task_exit(struct task_struct * task); 2、根据函数声明来定义待替换的系统调用函数指针 typedef void (*profile_task_exit_t)(struct task_struct * task); void my_profile_task_exit(struct task_struct * task); unsigned long old_do_exit_func = 0; profile_task_exit_t orig_profile_task_exit = NULL; 3、获取do_exit\profile_task_exit系统调用入口地址 old_do_exit_func = kallsyms_lookup_name("do_exit"); orig_profile_task_exit = kallsyms_lookup_name("profile_task_exit"); 4、修改offset static int replace_kernel_func(unsigned long handler, unsigned long orig_func, unsigned long my_func) { unsigned char *tmp_addr = (unsigned char*)handler; int i = 0; do{ /* in x86_64 the call instruction opcode is 0x8e, * occupy 1+4 bytes(E8+offset) totally */ if(*tmp_addr == 0xe8){ unsigned int* offset = (unsigned int*)(tmp_addr+1); if(((unsigned long)tmp_addr+5 + *offset) == orig_func){ printk("call:0x%08x, offset:%08x, old_func:%08x.\n", (unsigned int)tmp, *offset, orig_func); /* replace with my_func relative addr(offset) */ *offset=my_func-(unsigned long)tmp_addr-5; printk("call:0x%08x, offset:%08x, new_func:%08x.\n", (unsigned int)tmp_addr, *offset, my_func); return 1; } } tmp_addr++; }while(i++ tgid, current->real_parent->tgid, current->group_leader->comm); /* call the origin system call */ orig_profile_task_exit(task); return ; } 6、在uninit_module中 if(old_do_exit_func && orig_profile_task_exit){ disable_write_protection(); patch_kernel_func(old_do_exit_func, (unsigned long)my_profile_task_exit, orig_profile_task_exit); enable_write_protection(); printk("profile_task_exit is unpatched!\n"); }其中disable_write_protection()、enable_write_protection()在这里介绍过 本方法的关键是在第四步,读者可以自行细细体味。 到处捕获进程退出代码就完成了,这里不止可以捕获进程退出,也可以捕获到线程的退出。 该示例代码运行结果截图如下: 关于在linux的基于x86_64的各内核版本里,如何hook应用程序的启动行为,可以看这篇文章。 感兴趣的话可以关注我的微信公众号【大胖聊编程】,我的公众号中有更多文章分享,也可以在公众号中联系到我,加好友一起交流学习。 |
今日新闻 |
点击排行 |
|
推荐新闻 |
图片新闻 |
|
专题文章 |
CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭 |