技术标签: kernel linux Linux hook hooks
对于linux x86-64平台,hook普通的系统调用是一件比较简单的事情,可以看hook系统调用完整实例讲解。但是对于execve、fork、clone等这些系统调用的hook却并没那么简单了。
注:本文方法只适用于Linux x86-64平台
其它CPU架构下的hook方法可以看这几篇文章:
Linux ARM64平台上Hook系统调用(以openat为例)_yg@hunter的博客-程序员宅基地
Linux MIPS64下hook系统调用(kylin server v10)_yg@hunter的博客-程序员宅基地
及hook的进阶方案,inline hook技术:
Linux下监控所有进程的退出事件(x86_64下hook系统调用do_exit)_yg@hunter的博客-程序员宅基地
下面我们针对基于RHEL及其衍生系统CentOS的常用内核版本来详细分析之。本文同步至我的微信公众号大胖聊编程的这篇文章。
目录
rhel/centos 8.x 都是基于4.18.0内核版本,跟在centos8.0上hook openat系统调用一样的方法,比较简单,具体可看hook syscall in RHEL/CentOS/OL 8.x (kernel v4.17 onwards)。
完整代码示例如下:
typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);
static sys_call_ptr_t *sys_call_table;
sys_call_ptr_t old_execve;
static asmlinkage long my_execve(const struct pt_regs *regs)
{
char __user *filename = (char *)regs->di;
char user_filename[MAX_FILE_NAME_LEN] = {0};
int len = 0;
len = strnlen_user(filename, MAX_FILE_NAME_LEN);
if(unlikely(len >= MAX_FILE_NAME_LEN)){
pr_info("len[%d] grater than %d.\n", len, MAX_FILE_NAME_LEN);
len = MAX_FILE_NAME_LEN-1;
}
long copied = strncpy_from_user(user_filename, filename, len);
pr_info("%s filename:[%s], copied:%d. len:%d.\n",__func__, user_filename, copied, len);
char **argv = (char **)regs->si;
get_user_cmdline(argv, user_filename, MAX_FILE_NAME_LEN); // 解析出命令行
pr_info("%s cmdline:[%s].\n",__func__, user_filename);
return old_execve(regs);
}
static int __init hello_init(void)
{
sys_call_table = (sys_call_ptr_t *)kallsyms_lookup_name("sys_call_table");
old_execve = sys_call_table[__NR_execve]; // 获取原系统调用地址
write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_execve] = my_execve; // 替换成自定义的execve
write_cr0(read_cr0() | 0x10000);
pr_info("%s inserted.\n",__func__);
return 0;
}
static void __exit hello_exit(void)
{
write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_execve] = old_execve; // 卸载时,还原回原始系统调用,否则系统会崩溃
write_cr0(read_cr0() | 0x10000);
pr_info("%s removed.\n",__func__);
}
module_init(hello_init);
module_exit(hello_exit);
在centos8.0上,运行效果如下:
RHEL/CentOS 7.x(内核版本3.10.0) 6.x(内核版本2.6.32)的execve系统调用实现原理一样,所以hook方法也类似,比较复杂,下面详细分析下。
下面以 centos 7.6 为实验环境:
[root@yglocal ~]# uname -r
3.10.0-957.el7.x86_64
[root@yglocal ~]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)
首先,我们来看看带有execve的在系统调用符号表中有哪些:
[root@yglocal ~]# grep execve /proc/kallsyms
ffffffffad935b20 t audit_log_execve_info
ffffffffada495a0 t do_execve_common.isra.24
ffffffffada49e20 T do_execve
ffffffffada4a090 T SyS_execve
ffffffffada4a090 T sys_execve
ffffffffada4a0c0 T compat_sys_execve
ffffffffadf75320 T stub_execve
ffffffffadf79450 T stub32_execve
ffffffffae4a42e0 d event_exit__execve
ffffffffae4a4380 d event_enter__execve
ffffffffae4a4420 d __syscall_meta__execve
ffffffffae4a4460 d args__execve
ffffffffae4a4480 d types__execve
ffffffffae70f4c0 t __event_exit__execve
ffffffffae70f4c8 t __event_enter__execve
ffffffffae710ac8 t __p_syscall_meta__execve
按经验来分析,execve对应到内核系统调用应该是sys_execve,地址ffffffffada4a090,我们写个程序简单验证下:
static int __init test_init(void)
{
sys_call_table = (sys_call_ptr_t *)kallsyms_lookup_name("sys_call_table");
old_execve = sys_call_table[__NR_execve];
printk("[info] %s. sys_call_table[__NR_execve]:0x%llx, __NR_execve:%d\n",
__func__, old_execve, __NR_execve);
printk("%s inserted.\n",__func__);
return 0;
}
测试结果:
我们对比下看看:
可以清楚的看到,系统调用表中__NR_execve对应的系统调用地址是0xffffffffadf75320,也即是stub_execve,而不是sys_execve,这是怎么回事呢?
在内核源码中搜索stub_execve,可以发现,在arch\x86\um\sys_call_table_64.c源码中:
#define stub_clone sys_clone
#define stub_fork sys_fork
#define stub_vfork sys_vfork
#define stub_execve sys_execve
#define stub_rt_sigreturn sys_rt_sigreturn
及arch\x86\syscalls\syscall_64.tbl中:
# 64-bit system call numbers and entry vectors
# The format is:
# <number> <abi> <name> <entry point>
# The abi is "common", "64" or "x32" for this file.
56 common clone stub_clone
57 common fork stub_fork
58 common vfork stub_vfork
59 64 execve stub_execve
可见,sys_execve在系统调用表中被替换成了stub_execve
也就是说,应用层调用execve时,到内核层系统调用实际上是stub_execve。
在内核源码中,可以找到stub_execve的定义,在arch\x86\kernel\entry_64.S文件中:
ENTRY(stub_execve)
CFI_STARTPROC
addq $8, %rsp
DEFAULT_FRAME 0
FIXUP_TOP_OF_STACK %r11
call sys_execve
UNWIND_END_OF_STACK
movq %rax,RAX(%rsp)
RESTORE_REST
jmp int_ret_from_sys_call
CFI_ENDPROC
END(stub_execve)
可以看出,在这段汇编代码中,call sys_execve之前都是栈平衡操作,跟普通系统调用约定不太一样,上来代码先对rsp(堆栈指针寄存器)进行了修正,说明内核在进入stub_execve之前,rsp本身就是"不准确"的,需要进行修正,而rsp不准确也意味着栈上参数寻址是不准确的,所以我们在进行替换的my_execve_func就不能简单的直接使用传递进来的参数。
再看看此文件头部的注释部分:
里面提到,正常的系统调用或中断不需要保存完整的栈帧,对应exec/fork、系统调用追踪、信号等这些是需要保存完整栈帧的。也就是说,这里在sys_execve之前,又加了一层stub_execve函数,为了保存完整栈帧。
所以调用关系明确了:
execve ---> stub_execve ---> sys_execve
既然stub_execve汇编代码里,最终还是调用了sys_execve,那么我们可以采用hook进阶:linux下捕获进程的退出中的方法,通过修改call指令的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){
int* offset = (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_addr, *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++ < 128);
return 0;
}
具体就是:从stub_execve函数入口,遍历其代码段,找到call指令(0xe8),然后通过计算,比较call指令后的offset是否就是sys_execve的地址,若是的话,则证明该条指令就是call sys_execve,然后就可以重新计算新的offset,让它指向自定义的my_hook_execve函数入口处,替换新计算出的offset,这样就达到了我们目的,代码会执行到我们的my_hook_execve函数中。
下面开始编码实现。
函数声明如下:
typedef asmlinkage long (*execve_t)(const char __user *filename, const char __user * const __user *argv,
const char __user *const __user *envp, struct pt_regs *);
asmlinkage long my_stub_execve(const char __user *filename, const char __user * const __user *argv,
const char __user *const __user *envp, struct pt_regs *);
保存原始stub_execve、sys_execve地址:
old_stub_execve = (execve_t)sys_call_table_ptr[__NR_execve];
orig_execve_func = kallsyms_lookup_name("sys_execve");
注意:在rhel/centos 6.x(内核版本2.6.32) 上kallsyms_lookup_name并未导出,不能直接使用,这里可以使用kprobe方法获取kallsyms_lookup_name函数地址:
#include <linux/kprobes.h>
static struct kprobe kp={
.symbol_name = "kallsyms_lookup_name",
};
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
static kallsyms_lookup_name_t orig_kallsyms_lookup_name = NULL;
int get_kallsyms_lookup_name(void)
{
int ret = register_kprobe(&kp);
if(ret < 0){
printk("[err] %s. register_kprobe failed, ret:%d\n", __FUNCTION__, ret);
return ret;
}
printk("[info] %s. kprobe at addr:%p, ret:%d\n", __FUNCTION__, kp.addr, ret);
orig_kallsyms_lookup_name = (kallsyms_lookup_name_t)(void*)kp.addr;
unregister_kprobe(&kp);
return ret;
}
之后可以这样获取系统调用表地址:
if(get_kallsyms_lookup_name() < 0){
printk("[err] %s failed!\n", __FUNCTION__);
return -1;
}
sys_call_table = orig_kallsyms_lookup_name("sys_call_table");
同样方法获取sys_execve地址:
orig_execve_func = orig_kallsyms_lookup_name("sys_execve");
修改offset,指向自定义my_hook_execve处:
write_cr0(read_cr0() & (~0x10000));
replace_kernel_func(stub_execve_func, orig_execve_func, (unsigned long)my_hook_execve);
write_cr0(read_cr0() | 0x10000)
my_hook_execve里,可以正常读取参数,打印出应用程序相关信息,代码实现:
asmlinkage long my_hook_execve(const char __user *filename, const char __user * const __user *argv,
const char __user *const __user *envp, struct pt_regs *regs)
{
long value = -1;
char absolutepath[360] = {0};
int ret_num = copy_from_user(absolutepath, filename, 358);
printk("[info] %s. tgid:%d, tgcomm:%s, pid:%d, comm:%s. filename:%s.\n", __FUNCTION__,
current->tgid, current->group_leader->comm, current->pid, current->comm, absolutepath);
return orig_execve_func(filename, argv, envp, regs);
}
最后在卸载ko卸载函数,即module_exit调用的函数里,替换回原系统调用,保证我们的lkm被卸载后,系统正常运行:
write_cr0(read_cr0() & (~0x10000));
replace_kernel_func(stub_execve_func, (unsigned long)my_hook_execve, orig_execve_func);
write_cr0(read_cr0() | 0x10000);
在centos7.6 6.6上,运行效果如下:
在rhel/centos 5.x上,内核版本2.6.18,上述方法都失效了,但是可以自己写汇编函数my_stub_execve,自己处理栈平衡,来替换sys_call_table[__NR_execve]指向为my_stub_execve函数入口地址,在my_stub_execve里面调用自己的my_hook_execve函数,写入文件my_stub_execve.S中,代码如下:
.text
.global my_stub_execve
my_stub_execve:
pushq %rbx
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %rax
pushq %r8
pushq %r9
pushq %r10
pushq %r11
call my_hook_execve
test %rax, %rax
movq %rax, %rbx
pop %r11
pop %r10
pop %r9
pop %r8
pop %rax
pop %rcx
pop %rdx
pop %rsi
pop %rdi
jz my_stub_execve_ret
movq %rbx, %rax
pop %rbx
ret
my_stub_execve_ret:
pop %rbx
jmp *orig_sys_call_table(, %rax, 8)
替换stub_execve为自己编写的my_stub_execve:
write_cr0(read_cr0() & (~0x10000));
sys_call_table_ptr[__NR_execve] = (execve_t)my_stub_execve;
write_cr0(read_cr0() | 0x10000);
卸载模块的地方,替换回去:
write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_execve] = (execve_t)old_stub_execve;
write_cr0(read_cr0() | 0x10000);
另外需要定义一个全局系统调用表指针,汇编代码中需要(orig_sys_call_table)
void *orig_sys_call_table[__NR_syscall_max];
int i = 0;
for( ; i < __NR_syscall_max - 1; i ++) {
orig_sys_call_table[i] = sys_call_table[i];
}
特别注意:此时my_hook_execv的最后不能在调用orig_execve_func了,直接return 0即可,my_hook_execve函数改成:
asmlinkage long my_hook_execve(const char __user *filename, const char __user * const __user *argv,
const char __user *const __user *envp, struct pt_regs *regs)
{
char tmp_buf[262] = {0};
int ret_num = copy_from_user(tmp_buf, filename, 260);
printk("[info] %s. tgid:%d, tgcomm:%s, pid:%d, comm:%s. filename:%s.\n", __FUNCTION__,
current->tgid, current->group_leader->comm, current->pid, current->comm, tmp_buf);
memset(tmp_buf, 0, 260);
get_user_cmdline(argv, tmp_buf, 260);
printk("[cmdline]:%s\n", tmp_buf);
return 0;
}
因为在我们写的my_stub_execve汇编代码里的最后会去调用orig_execve_func:
hook_stub_execve_ret:
pop %rbx
jmp *orig_sys_call_table(, %rax, 8)
我们的my_hook_execve只相当于嵌入到他们中间执行了。
其它部分的代码跟centos6.x 7.x上一样。
在rhel5.8上,运行结果图如下:
到此,对于execve系统调用的hook方法都介绍完了,跟之前hook进阶:linux下捕获进程的退出算是姊妹篇了,感兴趣的可以看看这篇文章,或者关注我的微信公众号大胖聊编程,也可以加好友一起交流学习。
在多媒体通信领域,MRCP(Media Resource Control Protocol)协议被广泛用于控制语音识别和合成等媒体资源。UniMRCP是一个开源的MRCP实现,提供了客户端和服务端的库。UmcFramework是一个基于UniMRCP客户端库的示例应用程序框架,它帮助开发者快速集成和测试MRCP客户端功能。本文将详细介绍如何使用UmcFramework和unimrcpclient.xml配置文件连接到多个SIP设置,以及如何用C代码进行示例说明。
文章浏览阅读3k次。报错:java.net.ProtocolException: Server redirected too many times (20)1.没有检查到cookie,一直循环重定向。解决:CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));URL url = new URL(url); ..._java.net.protocolexception: server redirected too many times (20)
文章浏览阅读4.1k次。问题这是部分报错信息2019-07-11 14:03:34.283 WARN [restartedMain][DirectJDKLog.java:175] - Failed to scan [file:/D:/repo/org/apache/derby/derby/10.14.2.0/derbyLocale_ja_JP.jar] from classloader hierarchyjava...._failed to scan from classloader hierarchy
文章浏览阅读2.8k次,点赞3次,收藏7次。在MATLAB中,ones函数用于创建一个指定大小的由1组成的矩阵或数组。_matlab中ones函数
文章浏览阅读3.9w次,点赞2次,收藏9次。 在使用电脑办公过程中,安装应用程序时难免遇到无法安装或者无法正常启动的问题,这对我们使用电脑带来了诸多不便。那遇到应用程序无法正常启动的问题要如何解决呢?相信大家肯定都是十分疑问的,每次都是只能忍痛重新安装软件。今天,小编就和大家探讨下应用程序无法正常启动的解决方法,帮助大家排忧解难。0xc000007b电脑图解1 第一种方案:SFC检查系统完整性来尝试修复丢失文件 1、打开电脑搜索输入cmd.exe,选择以管理员身份运行,跳出提示框时选择继续。0xc000007b电脑图解2_photoshop应用程序无法正常启动0xc000007b。请单击“确认”关闭应用程序。
文章浏览阅读396次。1、概念 REDO LOG是Oracle为确保已经提交的事务不会丢失而建立的一个机制。实际上REDO LOG的存在是为两种场景准备的:实例恢复(INSTANCE RECOVERY);介质恢复(MEDIA RECOVERY)。 实例恢复的目的是在数据库发生故障时,确保BUFFER CACHE中的数据不会丢失,不会造成数据库的..._oracle 实例恢复和介质恢复
文章浏览阅读418次。概述说明CAS内置了密码找回和密码修改的功能; 密码找回功能是,系统会吧密码重置的连接通过邮件或短信方式发送给用户,用户点击链接后就可以重置密码,cas还支持预留密码重置的问题,只有回答对了,才可以重置密码;系统可配置密码重置后,是否自动登录; 密码修改功能是,用户登录后输入新密码即可完成密码修改。安装步骤`1. 首先,搭建好cas sso server您需要按..._修改cas默认用户密码
文章浏览阅读141次。之前几章演示的熔断,降级 都是 RestTemplate + Ribbon 和RestTemplate + Hystrix ,但是在实际开发并不是这样,实际开发中都是 Feign 远程接口调用。Feign + Hystrix 演示: eruka(略)order 服务工程: pom.xml<?xml version="1.0" encoding="U..._this is order 服务工程
文章浏览阅读3.4k次,点赞35次,收藏43次。学习率是影响目标检测精度和速度的重要因素之一。合适的学习率调度策略可以加速模型的收敛和提高模型的精度。在YOLOv7算法中,可以使用基于余弦函数的学习率调度策略(Cosine Annealing Learning Rate Schedule)来调整学习率。
文章浏览阅读4k次,点赞4次,收藏9次。 linux中进程退出函数:exit()和_exit()的区别(1)_exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。(2)调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin, stdout, stderr ...). exit函数是在_exit..._linux结束进程可以用哪些函数,它们之间有何区别?
文章浏览阅读134次。select 5000/10000.0 --想变成0.5select 5500/10000.0 --想变成0.55select 5550/10000.0 --想变成0.555select 5555/10000.0 --想变成0.5555其结果分别为:0.5000000 0.5500000 0.5550000 0.5555000一、如果想去掉数字5后面多余的0 ,需要转化一下:selec..._sql server 去小数 0
文章浏览阅读3.1k次。例一:import { Injectable } from '@angular/core';import { Observable } from 'rxjs';import { User } from "./model/User";import { map } from 'rxjs/operators';import { Http, Response, Headers, RequestOp..._angular6,requestoptions改成了什么