directory

On RHEL/CentOS 8.x

On RHEL/CentOS 6.x 7.x

Execve System call execve

Relationship between stub_execve and sys_execve

Start the hook execve system call

(1) Function declaration

(2) Obtain the original system call address

(3) Replace the system call

(4) User-defined hook functions

(5) Restore the system call

On RHEL/CentOS 5.x

Write at the end


For Linux x86-64 platform, hook ordinary system call is a relatively simple thing, you can see the complete example of hook system call. But hooks for system calls such as execve, fork, and Clone are not so simple.

Note: This method only applies to Linux x86-64 platforms

The following is a detailed analysis of common kernel versions based on RHEL and its derived system CentOS. This article is synchronized to my wechat public account big fat chat about programming this article.

On RHEL/CentOS 8.x

Rhel /centos 8.x are based on the 4.18.0 kernel version, which is the same as the hook Openat system call on centos8.0. Please refer to Hook Syscall in RHEL/CentOS/OL 8.x (Kernel V4.17 once again).

The following is an example of the complete code:

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); / / parse the command line 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; / / replace custom execve write_cr0 (read_cr0 () | 0 x10000); 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; / / unloading, revert to the original system call, otherwise the system will collapse write_cr0 (read_cr0 () | 0 x10000); pr_info("%s removed.\n",__func__); } module_init(hello_init); module_exit(hello_exit);Copy the code

On centos8.0, the results are as follows:

On RHEL/CentOS 6.x 7.x

The execve system call for RHEL/CentOS 7.x(kernel version 3.10.0) 6.x(kernel version 2.6.32) implements the same principle, so the hook method is similar and complicated.

Centos 7.6 is used as the experimental environment.

[root@yglocal ~]# uname -r 3.10.0-957.el7.x86_64 [root@yglocal ~]# cat /etc/redhat-release CentOS Linux release 7.6.1810  (Core)Copy the code

Execve System call execve

First, let’s look at what’s in the system call symbol table with 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
Copy the code

As a rule of thumb, execve corresponds to the kernel system call sys_execve with the address ffffffADa4A090. Let’s write a simple program to verify:

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;
}
Copy the code

Test results:

Let’s see:

The system call address for __NR_execve is 0xffffFFFFadF75320, which is stub_execve, not sys_execve.

Arch \x86\um\sys_call_table_64. C: arch\x86\um\sys_call_table_64. C: arch\x86\um\sys_call_table_64. C: arch\x86\um\sys_call_table_64.

#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
Copy the code

And the 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_execveCopy the code

As you can see, sys_EXECve has been replaced with Stub_execve in the system call table

That is, when execve is called by the application layer, the system call to the kernel layer is actually stub_execve.

Relationship between stub_execve and sys_execve

The definition of stub_execve can be found in the kernel source, in the arch\x86\kernel\ entry_64.s file:

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)
Copy the code

The stack pointer register (RSP) was corrected before the call sys_EXECve was implemented in the kernel. The RSP was “inaccurate” before the call sys_EXECve was implemented. The RSP inaccuracy also means that the parameter addressing on the stack is inaccurate, so the my_execve_func we are replacing cannot simply use the parameters passed in directly.

Look again at the comment section in the header of this file:

Exec /fork, system call trace, signal, etc., need to save the whole stack frame. In other words, a layer of stub_execve is added before sys_EXECve in order to hold the full stack frame.

So the call relationship is clear:

execve —> stub_execve —> sys_execve

Start the hook execve system call

Since sys_EXECve is finally called from stub_execve assembly code, we can hook up: catch the exit method of the process in Linux, and change the offset of the call instruction to jump to the custom function.

Core implementation code:

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;
}
Copy the code

Specifically: From the stub_execve function entry, traverse the code segment to find the Call instruction (0xE8). Then calculate and compare whether the offset after the call instruction is the address of sys_execve. If so, prove that the instruction is call sys_execve. The new offset can then be recalculated to point to the entry of our custom my_hook_execve function, replacing the newly calculated offset. This completes our purpose and the code executes into our my_hook_execve function.

Let’s start coding the implementation.

(1) Function declaration

The function declaration is as follows:

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 *);
Copy the code

(2) Obtain the original system call address

Save the original stub_execve and sys_execve addresses:


old_stub_execve = (execve_t)sys_call_table_ptr[__NR_execve];
orig_execve_func = kallsyms_lookup_name("sys_execve");
Copy the code

Note: On rheL /centos 6.x(kernel version 2.6.32), kallsyms_lookup_name is not exported and cannot be used directly. You can use the kprobe method to obtain the address of 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;
}
Copy the code

You can then obtain the system call table address as follows:

if(get_kallsyms_lookup_name() < 0){ printk("[err] %s failed! \n", __FUNCTION__); return -1; } sys_call_table = orig_kallsyms_lookup_name("sys_call_table");Copy the code

Obtain the sys_execve address in the same way:

orig_execve_func = orig_kallsyms_lookup_name("sys_execve");
Copy the code

(3) Replace the system call

Change offset to point to custom 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)
Copy the code

(4) User-defined hook functions

My_hook_execve can normally read parameters, print out the application information, code implementation:

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);
}
Copy the code

(5) Restore the system call

Finally, replace the original system call in the uninstall KO function called by module_exit to ensure the normal operation of the system after our LKM is uninstalled:

write_cr0(read_cr0() & (~0x10000));
replace_kernel_func(stub_execve_func, (unsigned long)my_hook_execve, orig_execve_func);
write_cr0(read_cr0() | 0x10000);
Copy the code

On centos7.6 6.6, the results are as follows:

On RHEL/CentOS 5.x

On RHEL /centos 5.x, kernel version 2.6.18, none of the above methods are available, but you can write the assembly function my_stub_execve to handle stack balancing yourself. To replace sys_call_table[__NR_execve] pointing to the my_stub_execve function entry address, call my_hook_execve function inside my_stub_execve, write the file my_stub_execve. The code is as follows:

.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)
Copy the code

Replace stub_execve with my_stub_execve written by yourself:

write_cr0(read_cr0() & (~0x10000));
sys_call_table_ptr[__NR_execve] = (execve_t)my_stub_execve;
write_cr0(read_cr0() | 0x10000);
Copy the code

Where to uninstall the module, replace it back:

write_cr0(read_cr0() & (~0x10000));
sys_call_table[__NR_execve] = (execve_t)old_stub_execve;
write_cr0(read_cr0() | 0x10000);
Copy the code

You also need to define a global system call table pointer, which is required in assembly code (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];
}    
Copy the code

The function my_hook_execve () is called from orig_execve_func (). The function my_hook_execve () should return 0.

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;
}
Copy the code

Because orig_execve_func is called at the end of the my_stub_execve assembly code we wrote:

hook_stub_execve_ret:
    pop     %rbx
    jmp     *orig_sys_call_table(, %rax, 8)
Copy the code

Our my_hook_execve is just embedded between them.

The rest of the code is the same as in Centos 6.x 7.x.

On RHEL5.8, the running result is shown as follows:

Write at the end

To this point, the hook method for execve system call is all introduced, and the hook is advanced before: Linux capture process exit is a sister article, interested in this article, or follow my wechat public account big big chat programming, can also add friends to exchange and learn together.