要想在计算机里干点事,权限肯定是越高越好的。正常情况下,cpu硬件层面保证了运行在0环的操作系统和运行在3环的用户app互相隔离,3环app要想进入0环执行代码只能通过中断或系统调用的形式,执行最多代码的应该就是硬件的驱动了,常见的屏幕打印、磁盘读写、网卡/wifi收发数据都要执行硬件驱动。因为需要被保护(防止被恶意篡改),同时也需要在多个3环进程间互斥,所以驱动都是被操作系统加载到0环的,天然拥有和操作系统其他内核代码一样的权限,因此很多需要高权限运行的功能都是以驱动的形式落地的。这里先以window为例:
windows早期防护措施不够,杀毒或逆向外挂等软件都喜欢hook SSDT来监控3环的应用程序;SSDT被hook多了容易导致蓝屏,严重影响用户体验;微软直接怒了,搞了驱动签名来严控操作系统对驱动的加载(还有pathguard监控内核代码,一旦发现被更改,直接蓝屏);同时如果发现厂家还在通过驱动恶意hook SSDT,直接吊销签名执照,不再给驱动签名;但是部分厂家从正常的业务角度考虑确实需要hook SSDT咋办了? 比如杀毒软件、驱动保护等业务确实需要监控系统调用,如果一刀切不让hook了,怎么保证windows系统和3环应用的安全了?微软也没赶尽杀绝,提供了回调注册的接口ObRegisterCallbacks,让厂家注册自己的回调函数。每当系统调用被执行时,就调用用户注册的回调函数执行,由此达到和hook一样的效果!
1、回到linux,毕竟也是和windows齐名的os,也是基于x86硬件架构的os,原理和windows是一样的:驱动是以模块(module)的形式加载到内核0环的,代码和操作系统内核其他代码拥有同样的权限,自然也能读写内核其他代码,这就让通过驱动hook内核代码的方式水到渠成了!那么linux面临了和早期windows一样的问题:如果放任开发人员通过驱动随意hook系统调用,可能造成的恶果:(1)不同的hook程序可能会“互相踩踏”,导致hook失效,甚至错误修改内核代码;(2)hook的代码如果没能完整地还原被破坏的机器码,或则跳转回来的地址填写出错,都会导致内核执行出错。为了避免开发人员hook内核带来的出错,linux和windows一样提供了完整的内核驱动代码hook机制:kprobe,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是pre_handler、post_handler和fault_handler,其中pre_handler函数将在被探测指令被执行前回调,post_handler会在被探测指令执行完毕后回调(注意不是被探测函数),fault_handler会在内存访问出错时被调用;主要优势特点:
允许在同一个被探测位置注册多个kprobe,避免多个第三方程序同时hook同一段内核代码时发生“互相踩踏”,做到“有序hook”;
除了kernel/kprobes.c和arch/*/kernel/kprobes.c程序中用于实现kprobes自身的函数,外加do_page_fault和notifier_call_chain外,其他内核函数都能hook;do_page_fault和notifier_call_chain被调用太频繁,hook会降低操作系统的运行效率;并且这两个都是在中断的handler代码,需要尽快执行完毕;而且hook的业务意义也不大,所以没必要hook了!
同一个hook点的回调只会被执行一次,比如hook printk后在回调函数中再调用printk,回调函数只执行一次,避免无限嵌套递归,导致栈资源耗尽;
kprobes的注册和注销过程中不会使用mutex锁和动态的申请内存,避免占用mutex阻塞其他线程,或sleep让出cpu,导致回调长时间执行,严重影响原函数的效率;
2、kprobe工作hook流程总结如下:
当用户注册一个探测点后,kprobe首先备份被探测点的对应指令,然后将原始指令的入口点替换为断点指令,该指令是CPU架构相关的,如i386和x86_64是int3,arm是设置一个未定义指令(目前的x86_64架构支持一种跳转优化方案Jump Optimization,内核需开启CONFIG_OPTPROBES选项,该种方案使用跳转指令来代替断点指令);
当CPU流程执行到探测点的断点指令时(把原机器码用int 3替代),就触发了一个trap,在trap处理流程中会保存当前CPU的寄存器信息并调用对应的trap处理函数,该处理函数会设置kprobe的调用状态并调用用户注册的pre_handler回调函数,kprobe会向该函数传递注册的struct kprobe结构地址以及保存的CPU寄存器信息;
随后kprobe单步执行前面所拷贝的被探测指令,具体执行方式各个架构不尽相同,arm会在异常处理流程中使用模拟函数执行,而x86_64架构则会设置单步调试flag并回到异常触发前的流程中执行;
在单步执行完成后,kprobe执行用户注册的post_handler回调函数;
最后,执行流程回到被探测指令之后的正常流程继续执行。
和其他内核功能类似,kprobe功能的实现也是由结构体+函数构成了(这里顺便多说几句:C语言没有对象的语法,所以类的继承采用了结构体包含结构体的形式;也没有成员函数的语法,只能采用结构体包含指针函数的形式实现),结构体字段如下:
struct kprobe {
struct hlist_node hlist://被用于kprobe全局hash,索引值为被探测点的地址;
struct list_head list://用于链接同一被探测点的不同探测kprobe;
kprobe_opcode_t *addr://被探测点的地址;
const char *symbol_name://被探测函数的名字;
unsigned int offset://被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;
kprobe_pre_handler_t pre_handler://在被探测点指令执行之前调用的回调函数;
kprobe_post_handler_t post_handler://在被探测指令执行之后调用的回调函数;
kprobe_fault_handler_t fault_handler://在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数;
kprobe_break_handler_t break_handler://在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe;
kprobe_opcode_t opcode://保存的被探测点原始指令;
struct arch_specific_insn ainsn://被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);
u32 flags://状态标记。
}
最关键的字段:addr、symbol_name、offset、handler、opcode等;这里居然还有list,明显是用来连接其他hook点的!相关配套初始化、使用、卸载探测点的函数如下:
int register_kprobe(struct kprobe *kp) //向内核注册kprobe探测点
void unregister_kprobe(struct kprobe *kp) //卸载kprobe探测点
int register_kprobes(struct kprobe **kps, int num) //注册探测函数向量,包含多个探测点
void unregister_kprobes(struct kprobe **kps, int num) //卸载探测函数向量,包含多个探测点
int disable_kprobe(struct kprobe *kp) //临时暂停指定探测点的探测
int enable_kprobe(struct kprobe *kp) //恢复指定探测点的探测
3、由于需要hook内核代码,权限也必须是0环的,所以也要被加载到内核。windows下的api是driver_entry和driver_exit,linux类似,用的api是module_init和moud_exit;这里以字节跳动的Elkied工具为例,在driver\LKM\src\init.c文件中,加载驱动的module的代码如下:
static int __init do_kprobe_initcalls(void)
{
int ret = 0;
struct kprobe_initcall const *const *initcall_p;
for (initcall_p = __start_kprobe_initcall;
initcall_p < __stop_kprobe_initcall; initcall_p++) {
struct kprobe_initcall const *initcall = *initcall_p;
if (initcall->init) {
ret = initcall->init();
if (ret < 0)
goto exit;
}
}
return 0;
exit:
while (--initcall_p >= __start_kprobe_initcall) {
struct kprobe_initcall const *initcall = *initcall_p;
if (initcall->exit)
initcall->exit();
}
return ret;
}
static void do_kprobe_exitcalls(void)
{
struct kprobe_initcall const *const *initcall_p =
__stop_kprobe_initcall;
while (--initcall_p >= __start_kprobe_initcall) {
struct kprobe_initcall const *initcall = *initcall_p;
if (initcall->exit)
initcall->exit();
}
}
static int __init kprobes_init(void)
{
int ret;
ret = do_kprobe_initcalls();
if (ret < 0)
return ret;
return ret;
}
static void __exit kprobes_exit(void)
{
do_kprobe_exitcalls();
}
module_init(kprobes_init);
module_exit(kprobes_exit);
上面的代码只看到了通过驱动加载和初始化kprobe,但是具体hook了哪些系统调用了还不清楚,继续看driver\LKM\src\smith_hook.c代码就能发现端倪了:重要的系统调用都被hook了,https://github.com/bytedance/Elkeid/blob/main/driver/README-zh_CN.md 这里也有hook的系统调用列举;
struct kretprobe execve_kretprobe = {
.kp.symbol_name = P_GET_SYSCALL_NAME(execve),
.entry_handler = execve_entry_handler,
.data_size = sizeof(struct execve_data),
.handler = execve_handler,
};
struct kprobe call_usermodehelper_exec_kprobe = {
.symbol_name = "call_usermodehelper_exec",
.pre_handler = call_usermodehelper_exec_pre_handler,
};
struct kprobe rename_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(rename),
.pre_handler = rename_pre_handler,
};
struct kprobe renameat_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(renameat),
.pre_handler = renameat_pre_handler,
};
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,15,0)
struct kprobe renameat2_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(renameat2),
.pre_handler = renameat_pre_handler,
};
#endif
struct kprobe link_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(link),
.pre_handler = link_pre_handler,
};
struct kprobe linkat_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(linkat),
.pre_handler = linkat_pre_handler,
};
struct kprobe ptrace_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(ptrace),
.pre_handler = ptrace_pre_handler,
};
struct kretprobe udp_recvmsg_kretprobe = {
.kp.symbol_name = "udp_recvmsg",
.data_size = sizeof(struct udp_recvmsg_data),
.handler = udp_recvmsg_handler,
.entry_handler = udp_recvmsg_entry_handler,
};
#if IS_ENABLED(CONFIG_IPV6)
struct kretprobe udpv6_recvmsg_kretprobe = {
.kp.symbol_name = "udpv6_recvmsg",
.data_size = sizeof(struct udp_recvmsg_data),
.handler = udp_recvmsg_handler,
.entry_handler = udpv6_recvmsg_entry_handler,
};
struct kretprobe ip6_datagram_connect_kretprobe = {
.kp.symbol_name = "ip6_datagram_connect",
.data_size = sizeof(struct connect_data),
.handler = connect_handler,
.entry_handler = ip6_datagram_connect_entry_handler,
};
struct kretprobe tcp_v6_connect_kretprobe = {
.kp.symbol_name = "tcp_v6_connect",
.data_size = sizeof(struct connect_data),
.handler = connect_handler,
.entry_handler = tcp_v6_connect_entry_handler,
};
#endif
struct kretprobe ip4_datagram_connect_kretprobe = {
.kp.symbol_name = "ip4_datagram_connect",
.data_size = sizeof(struct connect_data),
.handler = connect_handler,
.entry_handler = ip4_datagram_connect_entry_handler,
};
struct kretprobe tcp_v4_connect_kretprobe = {
.kp.symbol_name = "tcp_v4_connect",
.data_size = sizeof(struct connect_data),
.handler = connect_handler,
.entry_handler = tcp_v4_connect_entry_handler,
};
struct kretprobe connect_syscall_kretprobe = {
.kp.symbol_name = P_GET_SYSCALL_NAME(connect),
.data_size = sizeof(struct connect_syscall_data),
.handler = connect_syscall_handler,
.entry_handler = connect_syscall_entry_handler,
};
struct kretprobe accept_kretprobe = {
.kp.symbol_name = P_GET_SYSCALL_NAME(accept),
.data_size = sizeof(struct accept_data),
.handler = accept_handler,
.entry_handler = accept_entry_handler,
};
struct kretprobe accept4_kretprobe = {
.kp.symbol_name = P_GET_SYSCALL_NAME(accept4),
.data_size = sizeof(struct accept_data),
.handler = accept_handler,
.entry_handler = accept4_entry_handler,
};
struct kprobe do_init_module_kprobe = {
.symbol_name = "do_init_module",
.pre_handler = do_init_module_pre_handler,
};
struct kretprobe update_cred_kretprobe = {
.kp.symbol_name = "commit_creds",
.data_size = sizeof(struct update_cred_data),
.handler = update_cred_handler,
.entry_handler = update_cred_entry_handler,
};
struct kprobe security_inode_create_kprobe = {
.symbol_name = "security_inode_create",
.pre_handler = security_inode_create_pre_handler,
};
struct kretprobe bind_kretprobe = {
.kp.symbol_name = P_GET_SYSCALL_NAME(bind),
.data_size = sizeof(struct bind_data),
.handler = bind_handler,
.entry_handler = bind_entry_handler,
};
struct kprobe mprotect_kprobe = {
.symbol_name = "security_file_mprotect",
.pre_handler = mprotect_pre_handler,
};
struct kprobe setsid_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(setsid),
.pre_handler = setsid_pre_handler,
};
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 17, 0)
struct kprobe memfd_create_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(memfd_create),
.pre_handler = memfd_create_kprobe_pre_handler,
};
#endif
struct kprobe prctl_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(prctl),
.pre_handler = prctl_pre_handler,
};
struct kprobe open_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(open),
.pre_handler = open_pre_handler,
};
struct kprobe kill_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(kill),
.pre_handler = kill_pre_handler,
};
struct kprobe tkill_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(tkill),
.pre_handler = tkill_pre_handler,
};
struct kprobe nanosleep_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(nanosleep),
.pre_handler = nanosleep_pre_handler,
};
struct kprobe exit_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(exit),
.pre_handler = exit_pre_handler,
};
struct kprobe exit_group_kprobe = {
.symbol_name = P_GET_SYSCALL_NAME(exit_group),
.pre_handler = exit_group_pre_handler,
};
struct kprobe security_path_rmdir_kprobe = {
.symbol_name = "security_path_rmdir",
.pre_handler = security_path_rmdir_pre_handler,
};
struct kprobe security_path_unlink_kprobe = {
.symbol_name = "security_path_unlink",
.pre_handler = security_path_unlink_pre_handler,
};
这里就看的很清楚了,下面找几个重点函数的回调分析;
(1)execve_handler:execve可以执行指定位置的文件,hook execve后可以监控操作系统运行的所有文件,属于大杀器级别的hook,整个操作系统在干啥看得一清二楚,特别适合监控病毒、木马的运行!
int execve_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
int sa_family = -1;
int dport = 0, sport = 0;
__be32 dip4;
__be32 sip4;
pid_t socket_pid = -1;
char *pname = DEFAULT_RET_STR;
char *tmp_stdin = DEFAULT_RET_STR;
char *tmp_stdout = DEFAULT_RET_STR;
char *buffer = NULL;
char *pname_buf = NULL;
char *pid_tree = NULL;
char *socket_pname = "-1";
char *socket_pname_buf = NULL;
char *tty_name = "-1";
char *exe_path = DEFAULT_RET_STR;
char *pgid_exe_path = "-1";
char *stdin_buf = NULL;
char *stdout_buf = NULL;
struct in6_addr dip6;
struct in6_addr sip6;
struct file *file;
struct execve_data *data;
struct tty_struct *tty;
data = (struct execve_data *)ri->data;
buffer = kzalloc(PATH_MAX, GFP_ATOMIC);
/*当前进程所执行文件的路径*/
exe_path = smith_get_exe_file(buffer, PATH_MAX);
tty = get_current_tty();//得到当前终端
if(tty && strlen(tty->name) > 0)
tty_name = tty->name;
//exe filter check and argv filter check
if (execve_exe_check(exe_path) || execve_argv_check(data->argv))
goto out;
/*得到当前进程的socket,从这里也可以看出是不是中了木马的反弹webshell*/
get_process_socket(&sip4, &sip6, &sport, &dip4, &dip6, &dport,
&socket_pname, &socket_pname_buf, &socket_pid,
&sa_family);
//if socket exist,get pid tree
if (sa_family == AF_INET6 || sa_family == AF_INET)
pid_tree = smith_get_pid_tree(PID_TREE_LIMIT);
else
pid_tree = smith_get_pid_tree(PID_TREE_LIMIT_LOW);
// get stdin
file = fget_raw(0);
if (file) {
stdin_buf = kzalloc(256, GFP_ATOMIC);
tmp_stdin = smith_d_path(&(file->f_path), stdin_buf, 256);
fput(file);
}
//get stdout
file = fget_raw(1);
if (file) {
stdout_buf = kzalloc(256, GFP_ATOMIC);
tmp_stdout = smith_d_path(&(file->f_path), stdout_buf, 256);
fput(file);
}
pname_buf = kzalloc(PATH_MAX, GFP_ATOMIC);
pname = smith_d_path(¤t->fs->pwd, pname_buf, PATH_MAX);
/*打印收集到的各种数据*/
if (sa_family == AF_INET) {
execve_print(pname,
exe_path, pgid_exe_path, data->argv,
tmp_stdin, tmp_stdout,
dip4, dport, sip4, sport,
pid_tree, tty_name, socket_pid, socket_pname,
data->ssh_connection, data->ld_preload,
regs_return_value(regs));
}
#if IS_ENABLED(CONFIG_IPV6)
else if (sa_family == AF_INET6) {
execve6_print(pname,
exe_path, pgid_exe_path, data->argv,
tmp_stdin, tmp_stdout,
&dip6, dport, &sip6, sport,
pid_tree, tty_name, socket_pid, socket_pname,
data->ssh_connection, data->ld_preload,
regs_return_value(regs));
}
#endif
else {
execve_nosocket_print(pname,
exe_path, pgid_exe_path, data->argv,
tmp_stdin, tmp_stdout,
pid_tree, tty_name,
data->ssh_connection, data->ld_preload,
regs_return_value(regs));
}
out:
if (pname_buf)
kfree(pname_buf);
if (stdin_buf)
kfree(stdin_buf);
if (stdout_buf)
kfree(stdout_buf);
if (buffer)
kfree(buffer);
if (data->free_argv)
kfree(data->argv);
if (pid_tree)
kfree(pid_tree);
if (data->free_ld_preload)
kfree(data->ld_preload);
if (data->free_ssh_connection)
kfree(data->ssh_connection);
if(tty)
tty_kref_put(tty);
return 0;
}
(2)mprotect_pre_handler:hook了security_file_mprotect的回调函数,这个函数可能不出名,她是在do_mprotect_pkey中被调用的,整个mprotect的调用链如下:
SYSCALL_DEFINE3(mprotect, .., start, .., len, .., prot)
-> do_mprotect_pkey(start, len, prot, pkey=-1)
-> mprotect_fixup(vma, .., start, end, newflags)
-> change_protection(vma, start, end, newprot, cp_flags)
-> change_protection_range(vma, addr, end, newprot, cp_flags)
-> change_p4d_range(vma, pgd, add, end, newprot, cp_flags)
-> change_pmd_range(vma, pud, addr, end, newprot, cp_flags)
-> change_pte_range(vma, pmd, addr, end, newprot, cp_flags)
从底层的change_pte_rang、change_pmd_range、change_p4d_range可以看出,mprotect函数最终改变的是页属性,这个也可以用来做反调试的:把关键的代码地址改为可读可执行,但是不可写,就没法下断点调试了;正常情况下,代码也只会被读,然后执行。如果被改写,肯定不是正常情况,所以可以hook这个链条上的方法来监控关键代码是否被调试了。如果页面不可写,强行更改数据会报SIGSEGV错,用ida调试x音的时候没少遇到这种弹窗吧?不过从公开的资料看,Elkeid貌似并没用在x音客户端的防护,只是在字节内部的生产环境,监控的是服务器操作系统的运行;回调函数代码如下:
int mprotect_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
int target_pid = -1;
unsigned long prot;
char *file_path = "-1";
char *file_buf = NULL;
char *vm_file_path = "-1";
char *vm_file_buff = NULL;
char *exe_path = "-1";
char *abs_buf = NULL;
char *pid_tree = NULL;
struct vm_area_struct *vma;
//only get PROT_EXEC mprotect info
//The memory can be used to store instructions which can then be executed. On most architectures,
//this flag implies that the memory can be read (as if PROT_READ had been specified).
prot = (unsigned long)p_regs_get_arg2(regs);
if (prot & PROT_EXEC) {
abs_buf = kzalloc(PATH_MAX, GFP_ATOMIC);
exe_path = smith_get_exe_file(abs_buf, PATH_MAX);//当前进程对应文件的路径
vma = (struct vm_area_struct *)p_regs_get_arg1(regs);
if (IS_ERR_OR_NULL(vma)) {
mprotect_print(exe_path, prot, "-1", -1, "-1", "-1");
} else {
rcu_read_lock();
if (!IS_ERR_OR_NULL(vma->vm_mm)) {
if (!IS_ERR_OR_NULL(&vma->vm_mm->exe_file)) {
if (get_file_rcu(vma->vm_mm->exe_file)) {
file_buf = kzalloc(PATH_MAX, GFP_ATOMIC);
file_path = smith_d_path(&vma->vm_mm->exe_file->f_path, file_buf, PATH_MAX);
fput(vma->vm_mm->exe_file);
}
}
#ifdef CONFIG_MEMCG
target_pid = vma->vm_mm->owner->pid;
#endif
}
if (!IS_ERR_OR_NULL(vma->vm_file)) {
if (get_file_rcu(vma->vm_file)) {
vm_file_buff =
kzalloc(PATH_MAX, GFP_ATOMIC);
vm_file_path = smith_d_path(&vma->vm_file->f_path, vm_file_buff, PATH_MAX);
fput(vma->vm_file);
}
}
rcu_read_unlock();
/*被修改内存属性的pid树,从这里可以看到关键进程是不是在被调试等*/
pid_tree = smith_get_pid_tree(PID_TREE_LIMIT);
mprotect_print(exe_path, prot, file_path, target_pid, vm_file_path, pid_tree);
}
if (pid_tree)
kfree(pid_tree);
if (file_buf)
kfree(file_buf);
if (abs_buf)
kfree(abs_buf);
if (vm_file_buff)
kfree(vm_file_buff);
}
return 0;
}
(3)ptrace:调试全靠它了,frida和ida这俩卧龙凤雏用的都是这个。要想监控自己的进程是不是被调试了,必须监控ptrace了!回调代码如下:
int ptrace_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
long request;
request = (long)p_get_arg1(regs);
//only get PTRACE_POKETEXT/PTRACE_POKEDATA ptrace
//Read a word at the address addr in the tracee's memory,
//returning the word as the result of the ptrace() call. Linux
//does not have separate text and data address spaces, so these linux居然没区分代码和数据空间
//two requests are currently equivalent. (data is ignored; but
//see NOTES.)
/*PTRACE_PEEKTEXT或PTRACE_PEEKDATA:从addr参数指示的地址开始读取一个WORD的长度的数据并通过返回值传递回来*/
if (request == PTRACE_POKETEXT || request == PTRACE_POKEDATA) {
long pid;
void *addr;
char *exe_path = DEFAULT_RET_STR;
char *buffer = NULL;
char *pid_tree = NULL;
pid = (long)p_get_arg2(regs);
addr = (void *)p_get_arg3(regs);
if (IS_ERR_OR_NULL(addr))
return 0;
buffer = kzalloc(PATH_MAX, GFP_ATOMIC);
/*得到当前文件路径*/
exe_path = smith_get_exe_file(buffer, PATH_MAX);
/*得到当前进程的进程树*/
pid_tree = smith_get_pid_tree(PID_TREE_LIMIT);
/*打印结果*/
ptrace_print(request, pid, addr, "-1", exe_path, pid_tree);
if(buffer)
kfree(buffer);
if(pid_tree)
kfree(pid_tree);
}
return 0;
}
不过和ida有点不同,frida使用ptrace attach到进程之后,往进程中注入一个frida-agent-32.so模块,此模块是frida和frida-server通信的重要模块,所以frida不会一直占用ptrace,注入模块完成后便detach,所以ptrace回调函数收集到的数据可能不会太多!
目前所有的handler仅仅是收集进程名称、进程id树、文件路径、文件名或其他相关信息,貌似并未才取任何反制措施。也难怪,这个是用在后台服务端的,不是客户端,没必要才取exit或其他反制措施.....
4、大家平时用frida hook native代码的时候,有onEnter和onLeave两个分支,分别是进入native函数和离开native函数时挂钩,前者一般用来查看或修改函数的参数,后者一般用来查看和修改函数返回值,其实在linux底层已经提供了相应的方式来达到同样的目的:jprobe和kretprobe,并且这两个都是基于kprobe实现的!
参考:
1、https://cloud.tencent.com/developer/article/1874723?from=15425%20%20%20L Linux内核调试技术——kprobe使用与实现
2、https://yaotingting.net/2021/05/09/mprotect-analysis/ Linux/mprotect源码分析
3、https://github.com/bytedance/Elkeid/blob/main/driver/README-zh_CN.md About Elkeid(AgentSmith-HIDS) Driver
4、https://zhuanlan.zhihu.com/p/347313289 systemtap介绍
离线