Linux Rootkits — Multiple ways to hook syscall(s)
Most of the rootkits used in the malware attacks we see are open source and have almost the same behavior(hiding and hooking) as that of a normal process running in the system. In terms of behavior, they have little to no difference than a normal process. In this brief blog we will explore some existing ways by which a rootkit hooks a syscall depending on various linux kernel versions.
The multiple ways we will be looking:
- Syscall table hijacking — The good old way
- Sys_close — The brute force method
- VFS hooking
- The ftrace helper method
Syscall Table hijacking — The good old way
The first method of hooking syscall we will discuss is via syscall table hijacking. For hijacking the syscall table and hook a syscall from it, requires write access to the table. The syscall table is a mapping between the syscall ID and the kernel address of its implementation. In order to modify the table, we will have to make write permission from read-only. This can be done via control registers in the processor which decides the behavior of the CPU.
The WP bit of the cr0 register is set to 0 which we just have to change to 1 in order to modify the syscall table.
Now, we can modify the syscall table easily. /proc/kallsyms in linux contains the mapping of the kernel syscalls including syscall table(or sys_call_table).
To locate the table via code we will use the syscall kallsyms_lookup_name() which is defined in kallsyms.h and used to get the address of functions. In Linux kernel 4.4, kallsyms_lookup_name() call is exported, using which we can locate sys_call_table ‘s address.
Once the address of the syscall table is identified, we could easily modify the entries in the table as shown in below images.
Using kallsyms_lookup_name to find sys_call_table()
Let’s load the rootkit into the system:
Rootkit got loaded successfully. Because we hooked read(), we got huge logs.
sys_close — The brute force method
The sys_close() syscall is exported in older kernels(4.4 in our case), using which we can scan the addresses for sys_call_table() in the kernel memory. This is a trivial method of finding the syscall table address via the exported function. Below snippet shows the scanning code via which we try to locate the sys_call_table.
The snippet code above is very simple. First we just store the address of sys_close in the offset variable and then we just scan the memory to match the __NR_close entry in the table with sys_close(). Once matched we return the address of the sys_call_table(sct above).
Now, we just have to replace the original read() syscall with ours.
The hook function prints the below message:
After compilation we load the rootkit and as we can see, hooking was successful.
I heard about this technique when I read this report. VFS(Virtual File System) in Linux is an abstract layer which is like the glue that enables system calls such as open(), read(), and write() to work regardless of the filesystem or underlying physical medium. Through VFS, one kernel can mount several different types of file systems.
VFS contains 4 primary objects:
- The superblock object (stored in a special sector on disk)
- The inode object
- The dentry object
- The file object
The below image shows the high level overview of VFS implementation in linux:
- Whenever a process gets started in the system, it has various file objects() of the opened files. Every file object structure contains a field known as file path(of type struct path) which contains the dentry.
- The dentry could be simply understood as if we have a path like /var/www/html. In the path,the var,www,html and /, all these are dentry objects. These objects are created on the fly by VFS. The dentry object is in itself a structure which contains the associated inode of the component (like var in above path).
- The inode object represents all the information needed by the kernel to manipulate a file or a directory.
- The inode operations structure(field inside inode object structure) contains various operations like create, symlink, mkdir etc. to be done.
The field of our interest in the inode operation structure is the lookup. We are going to hook this field/call, the below image shows the lookup hook and replace snippet.
We start by modifying the cr0 register in order to get our value replaced. Then as shown below, the target we chose would be /proc, so if any file gets looked up via lookup() in /proc by any process, our function gets called.
In the above image:
- fp above is the file object of /proc.
- fp file object struct contains f_path which is a dentry object.
- dentry struct contains d_inode which is pointing to inode structure of fp object.
- i_op inside inode structure is the actual operation structure which contains operations like create, symlink, mkdir,lookup. Kernel uses this struct to manipulate files and directories.
Once we get access to /proc lookup, we replace that lookup by our hook function.
We load the rootkit using insmod, as we can see below we are inside the hooked function. The dmesg log shows that the rootkit was loaded successfully and after modification of the cr0 register, we were successfully able to modify the lookup() operation.
NOTE: VFS hook tested in linux kernel 5.8
The ftrace helper method
This is a quite new technique that works in the latest kernels (linux kernel >= 5.7). As we know the kallsyms_lookup_name is no longer exported in newer kernels. Using kallsyms_lookup_name, in earlier rootkits we located the sys_call_table().
The ftrace helper library uses kallsyms_lookup_name via kprobe to resolve symbol addresses. This way we can leverage to hook the syscall via the ftrace library. The technique is explained here in more detail. We will just take a look at the snippets and the working of some structures from ftrace library.
As we can see below, the usage of kprobe for resolving kallsyms_lookup_name().
Hook macro for ftrace_hook struct.
In the above image, the name is syscall we want to hook(mkdir in our case),hook_mkdir is our hook function and orig_mkdir is the place where we want to save mkdir(for later use).
Using kallsysms_lookup_name to resolve mkdir() address.
After resolving mkdir(), its address gets saved in address field of the ftrace_hook struct. Another important field in the ftrace_hook struct is the ops field. ops struct in ftrace_hook struct contains .func field which can be assigned with the callback function whenever our target syscall gets called(sys_mkdir). Hence we assign .func with fh_ftrace_thunk(our callback) as shown:
Our callback function takes following parameters:
@ip This is the instruction pointer of the function that is being traced. (where the fentry or mcount is within the function)
@parent_ip This is the instruction pointer of the function that called the the function being traced (where the call of the function occurred).
@op This is a pointer to ftrace_ops that was used to register the callback. This can be used to pass data to the callback via the private pointer.
@regs If the FTRACE_OPS_FL_SAVE_REGS or FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED flags are set in the ftrace_ops structure, then this will be pointing to the pt_regs structure like it would be if an breakpoint was placed at the start of the function where ftrace was tracing. Otherwise it either contains garbage, or NULL.
NOTE: As shown above, .flags in ops structure has to be set with FTRACE_OPS_FL_SAVE_REGS,FTRACE_OPS_FL_RECURSION_SAFE,FTRACE_OPS_FL_IPMODIFY to save original regs value to be passed inside callback,turn off ftrace’s built-in recursion protection and to notify ftrace for rip modification respectively.
One important thing to note here is to enable tracing, register_ftrace_function(&ops) should be used.
Inside the callback function, we just replace the instruction pointer of orig_mkdir to our hook_mkdir function by modifying $rip.
Now, whenever mkdir is called in any process, our hook function, hook_mkdir() will be called which(as an example here) does the ps listing and saves the output in /tmp.
Let’s now load the rootkit into the system.
Rootkit loaded, ps ran, list got saved.
ftrace is very useful tool in linux, not only we can hook syscalls but also we can monitor and kill certain unnecessary modifications on our files. We will cover monitoring feature in later blogs.