随着rhel8.0于去年5月初发布以来,开启了rhel8.x的时代,随后一段时间里centos、oracle linux也都发布了基于rhel的8.x系统。前段时间我就安装了个centos8.0,但是在编译运行之前写的hook内核的代码时,却发现之前的hook方法不奏效了。
一波谷歌之后,看到了这篇文章[PATCH 000/109] remove in-kernel calls to syscalls,如下图:
remove in-kernel calls to syscalls在64位的x86平台,4.17及以上的内核中,采用了新的调用约定,只使用struct pt_regs结构体指针这一个参数,即时解析出所需的参数,然后再传递到真正的系统调用处理函数中。
下面我们通过内核源码来探寻在centos8.0(基于4.18.0-80内核源码)系统中,如何hook系统调用。
本文实验环境:
[root@yglocalhost ~]# uname -r
4.18.0-80.el8.x86_64
[root@yglocalhost ~]# cat /etc/redhat-release
CentOS Linux release 8.0.1905 (Core)
本文以openat系统调用的hook为例切入,对于内核系统调用定义的详细解析见 linux系统调用内核源码解析(基于4.18.0-87版本内核)。
源码探秘
内核源码中openat系统调用的定义,在include/linux/syscalls.h文件中:
asmlinkage long sys_openat(int dfd, const char __user *filename, int flags,
umode_t mode);
01 系统调用原型
在fs\open.c文件中找到了系统调用openat的定义:
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
umode_t, mode){
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(dfd, filename, flags, mode);
}
将SYSCALL_DEFINE4(openat, xxx)宏展开后代码如下,详细展开过程看这篇:
SYSCALL_METADATA(_openat, 4, int, dfd, const char __user *, filename, int, flags, umode_t, mode)
asmlinkage long __x64_sys_openat(const struct pt_regs *regs);
ALLOW_ERROR_INJECTION(__x64_sys_openat, ERRNO);
static long __se_sys_openat(__MAP(4,__SC_LONG,__VA_ARGS__));
static inline long __do_sys_openat(__MAP(4,__SC_DECL,__VA_ARGS__));
asmlinkage long __x64_sys_openat(const struct pt_regs *regs)
{
return __se_sys_openat(SC_X86_64_REGS_TO_ARGS(4,__VA_ARGS__));
}
//__IA32_SYS_STUBx(x, name, __VA_ARGS__)
static long __se_sys_openat(__MAP(4,__SC_LONG,__VA_ARGS__))
{
long ret = __do_sys_openat(__MAP(4,__SC_CAST,__VA_ARGS__));
__MAP(4,__SC_TEST,__VA_ARGS__);
__PROTECT(4, ret,__MAP(4,__SC_ARGS,__VA_ARGS__));
return ret;
}
static inline long __do_sys_openat(int dfd, const char __user *filename, int flags,
umode_t mode){
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
所以,应用层到内核的openat系统调用实际上就是:
asmlinkage long __x64_sys_openat(const struct pt_regs *regs);
我们可以通过命令看到
[root@yglocal /]# grep __x64_sys_openat /proc/kallsyms
ffffffffbb0b60e0 T __x64_sys_openat
ffffffffbc796c50 t _eil_addr___x64_sys_openat
x86-64平台,在4.17及以上内核版本中,系统调用名称在之前基础上都加了"_x64"前缀。所以系统调用表里__NR_xxx下标所对应的系统调用内核地址就是指向了这里(__x64_sys_openat)。
现在就很明朗了,hook的系统调用,对应的原型都是这样的:
asmlinkage long __x64_sys_xxxx(const struct pt_regs *);
02 参数解析
我们再来看看,系统调用的唯一参数struct pt_regs结构体的具体定义(arch\x86\include\asm\ptrace.h ):
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_ax;
/* Return frame for iretq */
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
那么struct pt_regs怎么对应到系统调用具体的参数上呢,我们注意到__SYSCALL_DEFINEx宏展开后,传给__se_sys_openat函数的参数是SC_X86_64_REGS_TO_ARGS宏,该宏定义如下:
/* Mapping of registers to parameters for syscalls on x86-64 and x32 */
#define SC_X86_64_REGS_TO_ARGS(x, ...) \
__MAP(x,__SC_ARGS \
,,regs->di,,regs->si,,regs->dx \
,,regs->r10,,regs->r8,,regs->r9) \
那么系统调用的6个参数应该依次就是regs->di,regs->si,regs->dx,regs->r10,regs->r8,regs->r9。
另外在entry_64.S文件中也能找到相关说明(arch\x86\entry\entry_64.S):
entry_64.S
可以看出6个参数分别是在rdi、rsi、rdx、r10、r8、r9中。
自此,问题就都解决了,hook的系统调用原型及参数都明了了。
编码实现
1 待hook的系统调用原型
typedef asmlinkage long(*sys_call_ptr_t)(const struct pt_regs *);
对于openat,保存系统旧的openat地址,声明如下:
sys_call_ptr_t old_openat;
old_openat = sys_call_table[__NR_openat];
2 自定义hook函数
定义我们自己的my_openat处理函数,用于替换旧的openat
static asmlinkage long my_openat(const struct pt_regs *regs)
3 参数处理
通过前面分析,我们知道系统调用的最多6个参数分别对应struct pt_regs *regs中的6个寄存器,依次为:di、si、dx、r10、r8、r9。
再回过头来看看系统调用openat的声明:
asmlinkage long sys_openat(int dfd,const char__user *filename,int flags,umode_t mode);
所以,
第一个参数dfd在regs->di中
第二个参数filename在regs->si中
第三个参数flags在regs->dx中
第四个参数mode在regs->r10中
4 完整代码实现
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);
static sys_call_ptr_t *sys_call_table;
sys_call_ptr_t old_openat; //
void disable_write_protect(void)
{
write_cr0(read_cr0() & (~0x10000));
}
void enable_write_protect(void)
{
write_cr0(read_cr0() | 0x10000);
}
static asmlinkage long my_openat(const struct pt_regs *regs)
{
int dfd = regs->di;
char __user *filename = (char *)regs->si;
char user_filename[256] = {0};
int ret = raw_copy_from_user(user_filename, filename, sizeof(user_filename));
printk("%s. proc:%s, pid:%d, dfd:%d, filename:[%s], copy ret:%d\n", __func__,
current->group_leader->comm, current->tgid, dfd, user_filename, ret);
return old_openat(regs);
}
static int __init test_init(void)
{
sys_call_table = (sys_call_ptr_t *)kallsyms_lookup_name("sys_call_table");
old_openat = sys_call_table[__NR_openat];
printk("[info] %s. old_openat:0x%llx\n", __func__, old_openat);
disable_write_protect();
sys_call_table[__NR_openat] = my_openat;
enable_write_protect();
printk("%s inserted.\n",__func__);
return 0;
}
static void __exit test_exit(void)
{
disable_write_protect();
sys_call_table[__NR_openat] = old_openat;
enable_write_protect();
printk("%s removed.\n",__func__);
}
module_init(test_init);
module_exit(test_exit);
编译成ko后,insmod加载进内核,运行测试结果如下图:
在 centos8.0 上运行结果有关hook系统调用详细讲解,可以看hook调用完整实例。可以关注我的微信公众号大胖聊编程一起交流学习。
网友评论