Linux上监控应用程序启动 (hook execve系统调用)

对于linux x86-64平台,hook普通的系统调用是一件比较简单的事情,可以看hook系统调用完整实例讲解。但是对于execve、fork、clone等这些系统调用的hook却并没那么简单了。

注:本文方法只适用于Linux x86-64平台 

其它CPU架构下的hook方法可以看这几篇文章:

Linux ARM64平台上Hook系统调用(以openat为例)_yg@hunter的博客-CSDN博客

Linux MIPS64下hook系统调用(kylin server v10)_yg@hunter的博客-CSDN博客

及hook的进阶方案,inline hook技术:

Linux下监控所有进程的退出事件(x86_64下hook系统调用do_exit)_yg@hunter的博客-CSDN博客

下面我们针对基于RHEL及其衍生系统CentOS的常用内核版本来详细分析之。本文同步至我的微信公众号大胖聊编程这篇文章

目录

一、在RHEL/CentOS 8.x上

二、在RHEL/CentOS 6.x 7.x上

1、execve系统调用真面目

2、stub_execve跟sys_execve关系

3、着手hook execve系统调用

(1)函数声明

(2)获取原系统调用地址

(3)替换系统调用

(4)自定义hook函数

(5)还原系统调用

三、在RHEL/CentOS 5.x上

四、写在最后


一、在RHEL/CentOS 8.x上

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 6.x 7.x上

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)

1、execve系统调用真面目

首先,我们来看看带有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。

2、stub_execve跟sys_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

3、着手hook 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函数中。

下面开始编码实现。

(1)函数声明

函数声明如下:

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 *);

(2)获取原系统调用地址

保存原始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");

(3)替换系统调用

修改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)

(4)自定义hook函数

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);
}

(5)还原系统调用

最后在卸载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上

在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下捕获进程的退出算是姊妹篇了,感兴趣的可以看看这篇文章,或者关注我的微信公众号大胖聊编程,也可以加好友一起交流学习。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
### 回答1: Linux Hook 系统调用是指在 Linux 操作系统中,通过修改系统调用表来拦截和修改系统调用的行为。这种技术可以用于实现各种功能,如监控系统调用、实现安全策略、实现虚拟化等。Hook 系统调用的实现方式有多种,如使用内核模块、使用 LD_PRELOAD 环境变量、使用 ptrace 等。但是,Hook 系统调用也可能会带来一些安全风险,因此需要谨慎使用。 ### 回答2: Linux系统中的hook是一种通过修改系统内核来实现对系统应用程序行为的改变的技术手段。hook技术的实现方式有很多种,比如LD_PRELOAD,在程序运行前拦截系统调用,重定向到用户编写的函数中,实现对系统调用进行Hook;也可以修改内核源代码,在内核中添加对指定系统调用Hook函数,并重新编译部署内核。 在Linux系统中,Hook技术被广泛运用于安全防护、性能优化、监控记录等领域。安全防护方面,通过Hook技术可以拦截进程的系统调用,实现对安全攻击的检测和防范;在性能优化方面,Hook技术可以记录系统资源使用情况,进行优化分析;在监控记录方面,Hook技术可以通过修改系统调用返回值,实现对应用程序的状态监控和记录等功能。 Hook技术的实现需要一定的计算机专业知识和技能,同时也需要了解目标系统的内部运行机制和系统调用的使用方法。在使用时需要注意,Hook技术对系统稳定性和性能会产生影响,需要谨慎使用和调试。同时,在使用Hook技术时需要考虑到实现方式的兼容性、对日后维护的影响等问题,从而保证Hook技术的稳健性和长期有效性。 综上所述,在Linux系统中,Hook技术是一种应用广泛、有效性高的技术手段,可以实现对系统应用程序行为的灵活定制和改变,通过对系统调用的拦截和修改,实现安全防护、性能优化、监控记录等多种功能。不过,在使用时需要注意风险和影响,选择合适的实现方式,从而确保Hook技术的有效性和稳健性。 ### 回答3: Linux 系统调用是操作系统的一个重要组成部分,它提供了应用程序与操作系统之间通信的桥梁,使得应用程序可以利用操作系统的功能进行各种操作。然而,在某些情况下,用户可能需要修改某些系统调用的行为或者监控它们的使用情况,这就需要使用 Linux hook 系统调用Linux hook 系统调用,也称为系统调用钩子,是一种在系统调用进行时拦截和修改系统调用的技术。其主要作用是通过程序实现拦截和修改系统调用的行为,以达到特定的目的。比如,通过修改文件访问系统调用的行为,可以对文件进行加密、解密和防止恶意软件操作等。 Linux hook 系统调用的实现方式有很多种,其中最常见的方式就是使用内核模块实现。内核模块是一种动态加载到内核中的代码,可以通过内核提供的对模块的管理函数进行加载、卸载和修改等操作。利用内核模块,可以实现对指定系统调用的拦截与修改,具体实现方法是通过在模块中添加指定系统调用的替代函数来实现。 除此之外,还可以使用 LD_PRELOAD 环境变量或者 ptrace 系统调用来实现 Linux hook 系统调用。其中,LD_PRELOAD 环境变量是一种可以动态加载库文件的方法,通过在 LD_PRELOAD 中指定某个库文件,可以在程序运行时优先加载该库文件,从而实现对系统调用的拦截;而 ptrace 系统调用则是一种常用的系统调用监控方法,可以用来检测系统调用调用次数、参数和返回结果等信息。 总体来说,Linux hook 系统调用是一种非常实用的技术,可以用来实现各种监控和安全保障功能。但是,在使用时需要注意保证系统的稳定性和安全性,避免出现非预期的系统崩溃或安全漏洞。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值