Syscall Hooking on Kali Linux (Kernel Version 5)

Syscall Hooking on Kali Linux (kernel version 5)

Perhaps the way you are wrapping the system call does not work. For example, on Linux 5.4.0-59-generic x86_64 architecture, a system call in the kernel is called through a common wrapper called do_syscall_64(). It passes the parameters through the pt_regs structure to the entry in sys_call_table[]:

__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
struct thread_info *ti;

enter_from_user_mode();
local_irq_enable();
ti = current_thread_info();
if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
nr = syscall_trace_enter(regs);

if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs); <-------- Call to the entry with pt_regs structure
#ifdef CONFIG_X86_X32_ABI
} else if (likely((nr & __X32_SYSCALL_BIT) &&
(nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
X32_NR_syscalls);
regs->ax = x32_sys_call_table[nr](regs);
#endif
}

syscall_return_slowpath(regs);
}

The pt_regs structure embeds the parameters passed to the system call by the user. So this may explain why you are crashing : the printk(..."bind was called") works as it does not access the parameters but after the call to the original system call entry does not comply with the expected parameters.

If you look at the source code of bind() system call in net/socket.c, it is defined as:

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
return __sys_bind(fd, umyaddr, addrlen);
}

The above macro SYSCALL_DEFINE3() expands into some wrappers which extract the parameters from the pt_regs structure.

So, here is as an example, some fixes in your module which works on my 5.4.0-60-generic Ubuntu x86_64:

#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <asm/ptrace.h>
#include <linux/socket.h>
#include <linux/kallsyms.h>

MODULE_LICENSE("Dual BSD/GPL");

typedef int (* syscall_wrapper)(struct pt_regs *);

unsigned long sys_call_table_addr;

#define SOCKETLOG "[SOCKETLOG]"

int enable_page_rw(void *ptr){
unsigned int level;
pte_t *pte = lookup_address((unsigned long) ptr, &level);
if(pte->pte &~_PAGE_RW){
pte->pte |=_PAGE_RW;
}
return 0;
}

int disable_page_rw(void *ptr){
unsigned int level;
pte_t *pte = lookup_address((unsigned long) ptr, &level);
pte->pte = pte->pte &~_PAGE_RW;
return 0;
}

syscall_wrapper original_bind;

//asmlinkage int log_bind(int sockfd, const struct sockaddr *addr, int addrlen) {
int log_bind(struct pt_regs *regs) {
printk(KERN_INFO SOCKETLOG "bind was called");
return (*original_bind)(regs);
}

static int __init socketlog_init(void) {

printk(KERN_INFO SOCKETLOG "socketlog module has been loaded\n");

sys_call_table_addr = kallsyms_lookup_name("sys_call_table");

printk(KERN_INFO SOCKETLOG "sys_call_table@%lx\n", sys_call_table_addr);

enable_page_rw((void *)sys_call_table_addr);
original_bind = ((syscall_wrapper *)sys_call_table_addr)[__NR_bind];
if (!original_bind) return -1;
((syscall_wrapper *)sys_call_table_addr)[__NR_bind] = log_bind;
disable_page_rw((void *)sys_call_table_addr);

printk(KERN_INFO SOCKETLOG "original_bind = %p", original_bind);
return 0;
}

static void __exit socketlog_exit(void) {
printk(KERN_INFO SOCKETLOG "socketlog module has been unloaded\n");

enable_page_rw((void *)sys_call_table_addr);
((syscall_wrapper *)sys_call_table_addr)[__NR_bind] = original_bind;
disable_page_rw((void *)sys_call_table_addr);
}

module_init(socketlog_init);
module_exit(socketlog_exit);

With a test:

$ sudo insmod ./bind_ovl.ko
$ dmesg
[ 2253.201888] [SOCKETLOG]socketlog module has been loaded
[ 2253.209486] [SOCKETLOG]sys_call_table@ffffffff88c013a0
[ 2253.209489] [SOCKETLOG]original_bind = 00000000f54304a9

After a reload of a WEB page for example, I get:

$ dmesg
[ 2136.946042] [SOCKETLOG]socketlog module has been unloaded
[ 2253.201888] [SOCKETLOG]socketlog module has been loaded
[ 2253.209486] [SOCKETLOG]sys_call_table@ffffffff88c013a0
[ 2253.209489] [SOCKETLOG]original_bind = 00000000f54304a9
[ 2281.716581] [SOCKETLOG]bind was called
[ 2295.607476] [SOCKETLOG]bind was called
[ 2301.947866] [SOCKETLOG]bind was called
[ 2304.088116] [SOCKETLOG]bind was called
[ 2309.599634] [SOCKETLOG]bind was called
[ 2310.946833] [SOCKETLOG]bind was called

After unloading the module:

$ sudo rmmod bind_ovl
$ dmesg
[...]
[ 2390.908456] [SOCKETLOG]bind was called
[ 2398.921475] [SOCKETLOG]bind was called
[ 2398.928855] [SOCKETLOG]socketlog module has been unloaded

You can of course enhance the overload by displaying the parameters passed to the system call. On x86_64, the system calls are passed at most 6 parameters through the processor registers. We can retrieve them in the pt_regs structure. The latter is defined in arch/x86/include/asm/ptrace.h as:

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 */
};

The parameter passing convention for a system call is: param#0 to param#5 are respectively passed into the RDI, RSI, RDX, R10, R8 and R9 registers.

According to this rule, for bind() system call, the parameters are in the following registers:

  • RDI = int (socket descriptor)
  • RSI = struct sockaddr *addr
  • RDX = socklen_t addrlen

You can then enhance the log function with something like:

int log_bind(struct pt_regs *regs) {
printk(KERN_INFO SOCKETLOG "bind was called(%d, %p, %u)", (int)(regs->di), (void *)(regs->si), (unsigned int)(regs->dx));
return (*original_bind)(regs);
}

The traces from the module become more detailed:

[ 3259.589915] [SOCKETLOG]socketlog module has been loaded
[ 3259.594631] [SOCKETLOG]sys_call_table@ffffffff88c013a0
[ 3259.594634] [SOCKETLOG]original_bind = 00000000f54304a9
[ 3274.368906] [SOCKETLOG]bind was called(149, 0000000091c163d5, 12)
[ 3276.040330] [SOCKETLOG]bind was called(149, 0000000075b17cb4, 12)
[ 3278.203942] [SOCKETLOG]bind was called(188, 0000000091c163d5, 12)
[ 3287.014980] [SOCKETLOG]bind was called(214, 0000000075b17cb4, 12)
[ 3287.021167] [SOCKETLOG]bind was called(214, 0000000091c163d5, 12)
[ 3298.395713] [SOCKETLOG]bind was called(3, 000000008c2a9103, 12)
[ 3298.403249] [SOCKETLOG]socketlog module has been unloaded

System call hooking example arguments are incorrect

OP is probably using a kernel/architecture that uses "syscall wrappers" where the system call table contains a wrapper function that calls the real syscall function (possibly as an inline function call). The x86_64 architecture has used syscall wrappers since kernel version 4.17.

For x86_64 on kernel 4.17 or later, sys_call_table[__NR_open] points to __x64_sys_open (with prototype asmlinkage long __x64_sys_open(const struct pt_regs *regs)), which calls static function __se_sys_open (with prototype static long __se_sys_open(const __user *filename, int flags, umode_t mode)), which calls inline function __do_sys_open (with prototype static inline long __do_sys_open(const __user *filename, int flags, umode_t mode). Those will all be defined by the SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) macro call in "fs/open.c" and the function body that follows the macro call.

SYSCALL_DEFINE3 is defined in "include/linux/syscalls.h" and uses the SYSCALL_DEFINEx macro in the same file, which uses the __SYSCALL_DEFINEx macro. Since x86_64 defines CONFIG_ARCH_HAS_SYSCALL_WRAPPER, the __SYSCALL_DEFINEx macro is defined by #include <asm/syscall_wrapper.h>, which maps to "arch/x86/include/asm/syscall_wrapper.h".


For background on this change, see

  • LWN: use struct pt_regs based syscall calling for x86-64
  • LKML: [PATCH 000/109] remove in-kernel calls to syscalls https://lkml.org/lkml/2018/3/29/409

It seems the motivation is to only pass a pointer to pt_regs, instead of having a bunch of user-space values in registers down the call chain. (Perhaps to increase resistance to Spectre attacks by making gadgets less useful?)


Why open still worked, even though the wrapper didn't:

If OP is indeed using x86_64 kernel 4.17 or later, and replacing the sys_call_table[__NR_open] entry with a pointer to a function that uses a different prototype and calls the original function (pointed to by old_open) with the same parameters, that explains why the call to strncpy_from_user(user_msg, filename, sizeof(user_msg)) failed. Although declared as const char * __user filename, the filename pointer is actually pointing to the original struct pt_regs in kernel space.

In the subsequent call to old_open(filename, flags, mode), the first parameter filename is still pointing to the original struct pt_regs so the old function (which expects a single parameter of type struct pt_regs *) still works as expected.

i.e. the function passed on its first pointer arg unchanged, despite calling it a different type.

Linux x86-64 syscall hooking, path names garbled

As Tsyvarev alluded to, the manner in which syscall parameters are passed has changed for newer kernel versions (> 4.17) and requires that the intercepting trampoline have the following signature:

asmlinkage __type__ sys_foobar(const struct pt_regs * regs).

Individual parameters are then accessed via the respective registers, i.e (x86_64)

        arg1   arg2   arg3   arg4    arg5   arg6 
regs-> (di) (si) (dx) (r10) (r8) (r9)

So to get the path string for the access syscall, you would use regs->di.

asmlinkage int sys_access_trampoline(const struct pt_regs * regs)
{
char buffer[STR_MAX];
strncpy_from_user(buffer, regs->di, strnlen_user(path, PATH_MAX+1));
printk(buffer);
STUB_ORIGIN();
return sys_access(path, mode);
}

I now receive the correct ASCII path name.

Thank you to everyone for helping!

Hooking sys_execve() on Linux 3.x

You can't hook execve by modifying the system call table in a such a way as on x86_64 the sys_execve is called from the stub_execve. So the call chain is sys_call_table[NR_execve] -> stub_execve -> sys_execve -> do_execve ... Take a look at stub_execve on LXR.



Related Topics



Leave a reply



Submit