Modify the exception fixup table of page missing interrupt processing in Linux kernel

Recently, I made a low-level mistake while writing kernel modules:

  • Directly access the memory in user mode without using copy? To? User / copy? From? User!

In the view of the kernel, the virtual address provided by user state is untrustworthy, so it is very difficult to deal with the page missing interrupt when accessing user state memory in kernel state.

The way of Linux kernel is to provide an exception handling table, which uses proprietary functions to access user state memory. Similar to try catch block. For details, please refer to the implementation of copy to user / copy from user and the description of kernel document documentation / x86 / exception tables.txt.

Originally, let's see how to play this exception handling table.

First, we can write a piece of code to dump the exception handling table of the kernel:

// show_extable.c
#include <linux/module.h>
#include <linux/kallsyms.h>

int (*_lookup_symbol_name)(unsigned long, char *);
unsigned long (*_get_symbol_pos)(unsigned long, void *, void *);
unsigned long start_ex, end_ex;

int init_module(void)
{
	unsigned long i;
	unsigned long orig, fixup, originsn, fixinsn, offset, size;
	char name[128], fixname[128];

	_lookup_symbol_name = (void *)kallsyms_lookup_name("lookup_symbol_name");
	_get_symbol_pos = (void *)kallsyms_lookup_name("get_symbol_pos");
	start_ex = (unsigned long)kallsyms_lookup_name("__start___ex_table");
	end_ex = (unsigned long)kallsyms_lookup_name("__stop___ex_table");

	// Traverse from start to end according to sizeof of exception table entry.
	for(i = start_ex; i < end_ex; i += 2*sizeof(unsigned long)) {
		orig = i; // Take the insn field address of exception table entry.
		fixup = i + sizeof(unsigned int); // Take out the fixup field address.

		originsn = orig + *(unsigned int *)orig; // Find the absolute address according to the relative offset field
		originsn |= 0xffffffff00000000;
		fixinsn = fixup + *(unsigned int *)fixup;
		fixinsn |= 0xffffffff00000000;
		_get_symbol_pos(originsn, &size, &offset);
		_lookup_symbol_name(originsn, name);
		_lookup_symbol_name(fixinsn, fixname);
		printk("[%lx]%s+0x%lx/0x%lx [%lx]%s\n",
				originsn,
				name,
				offset,
				size,
				fixinsn,
				fixname);
	}

	return -1;
}
MODULE_LICENSE("GPL");

Let's look at the output:

# ___An exception occurred at sys ﹣ recvmsg + 0x253, jump to ffffff81649396 to handle the exception.
[ 7655.267616] [ffffffff8150d7a3]___sys_recvmsg+0x253/0x2b0 [ffffffff81649396]bad_to_user
...
# If there is an exception at the location of create 〝 elf 〝 tables + 0x3cf, skip to the address of ffffff81648a07 to execute exception handling.
[ 7655.267727] [ffffffff8163250e]create_elf_tables+0x3cf/0x509 [ffffffff81648a1b]bad_gs

Generally speaking, exception handling functions such as bad to user and bad from user directly return an error code of the user, such as Bad address, which is not a direct segment error of the direct user program, which is different from the user mode access illegal address sending SIGSEGV directly. For example:

#include <fcntl.h>
int main(int argc, char **argv)
{
	int fd;
	int ret;
	char *buf = (char *)0x56; // It's obviously an illegal address.

	fd = open("/proc/sys/net/nf_conntrack_max", O_RDWR | O_CREAT, S_IRWXU);
	perror("open");
	ret = read(fd, buf, 100);
	perror("read");
}

Implementation:

[root@localhost test]# ./a.out
open: Success
read: Bad address # There is no segment error, just a common one.

Can we modify its behavior to be consistent with the illegal address of user state access? Simply, replace bad to user. The code is as follows:

// fix_ex.c
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kallsyms.h>

int (*_lookup_symbol_name)(unsigned long, char *);
unsigned long (*_get_symbol_pos)(unsigned long, void *, void *);
unsigned long start_ex, end_ex;
void *_bad_from_user, *_bad_to_user;

void kill_user_from(void)
{
	printk("Manager! rush tighten beat electric discourse!\n");
	force_sig(SIGSEGV, current);
}

void kill_user_to(void)
{
	printk("Manager! rush tighten beat electric discourse! SB leather shoes\n");
	force_sig(SIGSEGV, current);
}

unsigned int old, new;

int (*_lookup_symbol_name)(unsigned long, char *);
unsigned long (*_get_symbol_pos)(unsigned long, void *, void *);

int hook_fixup(void *origfunc1, void *origfunc2, void *newfunc1, void *newfunc2)
{
	unsigned long i;
	unsigned long fixup, fixinsn;
	char fixname[128];


	for(i = start_ex; i < end_ex; i += 2*sizeof(unsigned long)) {
		fixup = i + sizeof(unsigned int);
		fixinsn = fixup + *(unsigned int *)fixup;
		fixinsn |= 0xffffffff00000000;
		_lookup_symbol_name(fixinsn, fixname);
		if (!strcmp(fixname, origfunc1) ||
			!strcmp(fixname, origfunc2)) {
			unsigned long new;
			unsigned int newfix;

			if (!strcmp(fixname, origfunc1)) {
				new = (unsigned long)newfunc1;
			} else {
				new = (unsigned long)newfunc2;
			}
			new -= fixup;
			newfix = (unsigned int)new;
			*(unsigned int *)fixup = newfix;
		}
	}

	return 0;
}

int init_module(void)
{
	_lookup_symbol_name = (void *)kallsyms_lookup_name("lookup_symbol_name");
	_get_symbol_pos = (void *)kallsyms_lookup_name("get_symbol_pos");
	_bad_from_user = (void *)kallsyms_lookup_name("bad_from_user");
	_bad_to_user = (void *)kallsyms_lookup_name("bad_to_user");
	start_ex = (unsigned long)kallsyms_lookup_name("__start___ex_table");
	end_ex = (unsigned long)kallsyms_lookup_name("__stop___ex_table");

	hook_fixup("bad_from_user", "bad_to_user", kill_user_from, kill_user_to);
	return 0;
}
void cleanup_module(void)
{
	hook_fixup("kill_user_from", "kill_user_to", _bad_from_user, _bad_to_user);
}

MODULE_LICENSE("GPL");

Compile, load and re execute our a.out:

[root@localhost test]# insmod ./fix_ex.ko
[root@localhost test]# ./a.out
open: Success
//Segment error
[root@localhost test]# dmesg
[ 8686.091738] Manager! rush tighten beat electric discourse! SB leather shoes
[root@localhost test]#

There was a paragraph error and a sentence was printed that asked the manager to call quickly.

In fact, my purpose is not like this. I really mean that Linux's exception handling list is another good place to hide dirt. We can hide some code in the hook function above, such as inline hook, and so on. Then what? Then quietly wait for the user state process bug to cause exception handling to be executed. Lengthen the timeline for code injection, making it harder for operations and managers to notice.

Separate the time points of code injection and module insertion to make things more chaotic.

However, pay attention to the hidden module or oneshot.

Wenzhou leather shoes in Zhejiang Province are wet, and they will not be fat if it rains and floods.

Tags: Linux

Posted on Wed, 13 May 2020 19:50:22 -0700 by khanfahd