inline hook syscall 详解

文章目录

  • 1. hook一般syscall
  • 2. hook stub syscall
    • 2.1 stub_xxx 原理
    • 2.2 方法1:hook `stub_xxx`
    • 2.3 方法2:hook `call sys_xxx`
  • 参考文档:

1. hook一般syscall

在安全、性能分析等领域,经常会需要对系统调用syscall进行hook。有些模块在kernel代码中已经预先hook,例如syscall trace event。

通常syscall使用sys_call_table[]数组来间接调用:

kernel\arch\x86\kernel\entry_64.S:

ENTRY(system_call)

	call *sys_call_table(,%rax,8)  # XXX:	 rip relative

sys_call_table[]数组中保存的是所有系统调用的函数指针:

#define __SYSCALL(nr, sym) [nr] = sym,

const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    #define __NR_read				0
    __SYSCALL(__NR_read, sys_read)
    #define __NR_write				1
    __SYSCALL(__NR_write, sys_write)
    #define __NR_open				2
    __SYSCALL(__NR_open, sys_open)
    #define __NR_close				3
    __SYSCALL(__NR_close, sys_close)

    ...
};

对于其他没有预置代码的模块来说,需要在运行的时候动态hook,通常我们使用inline hook。inline hook的好处是hook完以后,运行时零开销。

实例代码:

void syscallxxx_hook_init(void)
{
	unsigned long *sct;
    void ** g_syscall_table;

    g_syscall_table = (void **)kallsyms_lookup_name("sys_call_table");
		
	make_kernel_page_readwrite();
	preempt_disable();
	
    /* (1) 备份原有g_syscall_table[]数组中的函数指针 */
	orig_syscallxxx = (void *)g_syscall_table[__NR_syscallxxx];
    /* (2) 把g_syscall_table[]数组值改为新的函数指针 */
	sct[__NR_syscallxxx] = (unsigned long)new_syscallxxx;
	
	preempt_enable();
	make_kernel_page_readonly();
}

↓

asmlinkage long new_syscallxxx(...)
{
	long rc;
	
    /* (2.1) 做一些hook增加的事情 */
	rc = do_something(...);
	if (0 != rc)
	    return rc; 
	
    /* (2.2) 调用原有的syscall处理 */
	return orig_syscallxxx(....); 
}

这种hook方式在大部分情况下工作正常,但是某些特殊的系统调用会工作异常。

2. hook stub syscall

2.1 stub_xxx 原理

4.5版本及以下的内核中,x86架构对某些系统调用有特殊处理。我们可以在sys_call_table[]数组中看到的函数不是sys_xxx而是stub_xxx

const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {

    #define __NR_rt_sigreturn			15
    __SYSCALL(__NR_rt_sigreturn, stub_rt_sigreturn)

    #define __NR_clone				56
    __SYSCALL(__NR_clone, stub_clone)
    #define __NR_fork				57
    __SYSCALL(__NR_fork, stub_fork)
    #define __NR_vfork				58
    __SYSCALL(__NR_vfork, stub_vfork)
    #define __NR_execve				59
    __SYSCALL(__NR_execve, stub_execve)

    #define __NR_sigaltstack			131
    __SYSCALL(__NR_sigaltstack, stub_sigaltstack)

    #define __NR_iopl				172
    __SYSCALL(__NR_iopl, stub_iopl)
    ...
};

这有点出乎我们的意料,字面上理解是一些桩函数,我们看看其具体做了些什么:

kernel\arch\x86\kernel\entry_64.S:

/*
 * Certain special system calls that need to save a complete full stack frame.
 */
	.macro PTREGSCALL label,func,arg
ENTRY(\label)
	PARTIAL_FRAME 1 8		/* offset 8: return address */
	subq $REST_SKIP, %rsp
	CFI_ADJUST_CFA_OFFSET REST_SKIP
	call save_rest
	DEFAULT_FRAME -2 8		/* offset 8: return address */
	leaq 8(%rsp), \arg	/* pt_regs pointer */
	call \func              /* (1.1) 调用实际的系统调用sys_xxx()函数 */
	jmp ptregscall_common
	CFI_ENDPROC
END(\label)
	.endm

    /* (1) stub_clone/fork/vfork/sigaltstack/iopl 函数的定义 */
	PTREGSCALL stub_clone, sys_clone, %r8
	PTREGSCALL stub_fork, sys_fork, %rdi
	PTREGSCALL stub_vfork, sys_vfork, %rdi
	PTREGSCALL stub_sigaltstack, sys_sigaltstack, %rdx
	PTREGSCALL stub_iopl, sys_iopl, %rsi

ENTRY(ptregscall_common)
	DEFAULT_FRAME 1 8	/* offset 8: return address */
	RESTORE_TOP_OF_STACK %r11, 8
	movq_cfi_restore R15+8, r15
	movq_cfi_restore R14+8, r14
	movq_cfi_restore R13+8, r13
	movq_cfi_restore R12+8, r12
	movq_cfi_restore RBP+8, rbp
	movq_cfi_restore RBX+8, rbx
	ret $REST_SKIP		/* pop extended registers */
	CFI_ENDPROC
END(ptregscall_common)

    /* (2) stub_execve函数的定义 */
ENTRY(stub_execve)
	CFI_STARTPROC
	addq $8, %rsp
	PARTIAL_FRAME 0
	SAVE_REST
	FIXUP_TOP_OF_STACK %r11
	movq %rsp, %rcx
	call sys_execve             /* (2.1) 调用实际的系统调用sys_execve()函数 */
	RESTORE_TOP_OF_STACK %r11
	movq %rax,RAX(%rsp)
	RESTORE_REST
	jmp int_ret_from_sys_call
	CFI_ENDPROC
END(stub_execve)

/*
 * sigreturn is special because it needs to restore all registers on return.
 * This cannot be done with SYSRET, so use the IRET return path instead.
 */
    /* (3) stub_rt_sigreturn函数的定义 */
ENTRY(stub_rt_sigreturn)
	CFI_STARTPROC
	addq $8, %rsp
	PARTIAL_FRAME 0
	SAVE_REST
	movq %rsp,%rdi
	FIXUP_TOP_OF_STACK %r11
	call sys_rt_sigreturn       /* (3.1) 调用实际的系统调用sys_rt_sigreturn()函数 */
	movq %rax,RAX(%rsp) # fixme, this could be done at the higher layer
	RESTORE_REST
	jmp int_ret_from_sys_call
	CFI_ENDPROC
END(stub_rt_sigreturn)
  • 为什么系统要对这几个系统调用做stub_xxx的特殊处理?

注释中的一段话说明了大概原因:

/*
 * Certain special system calls that need to save a complete full stack frame.
 * 某些特殊的系统调用需要保存完整的完整堆栈帧。
 */

针对这类特殊的系统调用,我们有两种方法来进行hook。

2.2 方法1:hook stub_xxx

第一种方法我们还是继续替换sys_call_table[]数组中函数指针,但是要自己处理hook函数的栈平衡。

写一段自己的stub_new_syscallxxx函数来替换原有的stub_syscallxxx函数:

stub_new_syscallxxx:
    /**
     * (1.1) 保存寄存器状态, 保证之后调用原来的stub_syscallxxx的时候CPU执行环境一致
     * 其中rdi,rsi,rdx,rcx,rax,r8,r9,r10,r11保存sysenter的参数,rbx作为临时变量
     */
    pushq   %rbx
    pushq   %rdi
    pushq   %rsi
    pushq   %rdx
    pushq   %rcx
    pushq   %rax
    pushq   %r8
    pushq   %r9
    pushq   %r10
    pushq   %r11
 
	/* (1.2) 调用自己的hook函数 */
    call    new_syscallxxx
    test    %rax, %rax
    movq    %rax, %rbx
 
    /* (1.3) 恢复寄存器状态 */
    pop     %r11
    pop     %r10
    pop     %r9
    pop     %r8
    pop     %rax
    pop     %rcx
    pop     %rdx
    pop     %rsi
    pop     %rdi
 
    jz      new_syscallxxx_done
    
    /* (2.1) new_syscallxxx返回值为非0时 */
    movq    %rbx, %rax
    pop     %rbx
    ret   /* 这里不一定要jmp int_ret_from_sys_call,反正syscallxxx已经被我们拦截了 */
    
    /* (2.2) new_syscallxxx返回值为0时 */
new_syscallxxx_done:
    pop     %rbx
    jmp     *orig_sys_call_table(, %rax, 8) /* 调用原始的stub_syscallxxx */

这种方法要小心处理调用堆栈,在我们hook函数运行之前要小心的保护堆栈,在hook函数运行完成后要完全恢复堆栈。而且不方便实现post hook。

2.3 方法2:hook call sys_xxx

另一种方法我们替换stub_syscallxxx函数中的call sys_syscallxxx语句。例如:

ENTRY(stub_execve)
	CFI_STARTPROC
	addq $8, %rsp
	PARTIAL_FRAME 0
	SAVE_REST
	FIXUP_TOP_OF_STACK %r11
	movq %rsp, %rcx
	call sys_execve             // 替换call语句中的sys_execve为new_sys_execve
	RESTORE_TOP_OF_STACK %r11
	movq %rax,RAX(%rsp)
	RESTORE_REST
	jmp int_ret_from_sys_call
	CFI_ENDPROC
END(stub_execve)

查看原始指令码:

(gdb) disassemble /r stub_execve
Dump of assembler code for function stub_execve:
   0xffffffff8146f7e0 <+0>:     48 83 c4 08     add    $0x8,%rsp
   ...
   0xffffffff8146f847 <+103>:   e8 74 b2 b9 ff  callq  0xffffffff8100aac0   // call sys_execve
   ...
   0xffffffff8146f890 <+176>:   e9 77 fd ff ff  jmpq   0xffffffff8146f60c 
End of assembler dump.
(gdb) p sys_execve
$2 = {long (const char *, const char * const *, const char * const *, struct pt_regs *)} 0xffffffff8100aac0 

我们可以看到call sys_execve对应的命令码为e8 74 b2 b9 ff,其中:

  • e8对应call指令。
  • ffb9b274表示被调用函数和当前pc的偏移:
被call函数地址 - 当前地址 - 当前指令长度 = offset
0xffffffff8100aac0 - 0xffffffff8146f847 - 5 = 0xFFFFFFFFFFB9B274 & 0xFFFFFFFF = 0xFFB9B274

所以我们只要定义个参数完全一致的新函数new_sys_execve(),把sys_execve()的对应偏移ffb9b274替换成new_sys_execve()的相对偏移即可。

static asmlinkage long new_sys_execve(const char __user * filename,
				const char __user * const __user * argv,
				const char __user * const __user * envp, struct pt_regs *regs) {
	size_t exec_line_size;
	char * exec_str = NULL;
	char ** p_argv = (char **) argv;
    long ret = 0;

    /* (1) pre hook 点 */

	/* Finally, call the original sys_execve */
    /* (2) 调用原始系统调用 */
	ret = orig_sys_execve_fn(filename, argv, envp, regs);

    /* (3) post hook 点 */
    printk("orig_sys_execve_fn ret = %d\n", ret);

    return ret;
}

具体代码放在inlinehook_syscall_example。

参考文档:

1.x86平台inline hook原理和实现
2.execmon
3.Linux x64下hook系统调用execve的正确方法

你可能感兴趣的:(Trace,Security)