zoukankan      html  css  js  c++  java
  • Linux内核访问用户空间文件:get_fs()/set_fs()的使用

    测试环境:Ubuntu 14.04+Kernel 4.4.0-31

    关键词:KERNEL_DS、USER_DS、get_fs()、set_fs()、addr_limit、access_ok

    参考代码:https://elixir.bootlin.com/linux/v4.4/source

    内核空间和用户空间交换数据的方式有很多,比如用户空间发起的系统调用、proc、虚拟文件系统等。

    内核空间主动发起的有get_user/put_user、信号、netlink等。

    这里介绍get_user/put_user的使用以及背后的原理。

    1. 构造测试环境:Ubuntu下创建module

    要让内核空间主动发起,需要创建一个module,然后插入到内核中。

    从内核中发起创建kernel_file,并写入内容。

    最后从用户空间进行验证。

    1.1 测试源码

    首先,编写module源码:

    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>
    
    static char buf[] ="来自内核的访问
    ";
    static char buf1[32];
     
    int __init test_init(void)
    {
        struct file *fp;
        mm_segment_t fs;
        loff_t pos;
        printk("test enter
    ");
        fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644);
        if (IS_ERR(fp)){
            printk("create file error
    ");
            return -1;
        }
        fs =get_fs();
        set_fs(KERNEL_DS);
        pos =0;
        vfs_write(fp,buf, sizeof(buf), &pos);
        pos =0;
        vfs_read(fp,buf1, sizeof(buf), &pos);
        printk("Write contet=%s
    ",buf1);
        filp_close(fp,NULL);
        set_fs(fs);
        return 0;
    }
    void __exit test_exit(void)
    {
        printk("test exit
    ");
    }
     
    module_init(test_init);
    module_exit(test_exit);
     
    MODULE_LICENSE("GPL");

     编写Makefile文件:

    obj-m :=read_userspace.o                        #要生成的模块名     
    read_userspace-objs:= read_userspace_file.o     #生成这个模块名所需要的目标文件
    
    KDIR := /lib/modules/`uname -r`/build
    PWD := $(shell pwd)
    
    default:
        make -C $(KDIR) M=$(PWD) modules
    
    clean:
        rm -rf *.o *.cmd *.ko *.mod.c .tmp_versions Module.symvers modules.order

    1.2 编译

    执行make命令,就可以得到read_userspace.ko文件。

    1.3 测试

    sudo insmod read_userspace.ko-----------------插入模组

    sudo lsmod | grep read_userspace--------------验证是否插入成功

    sudo rmmod read_userspace----------------------移除模组

    测试结果如下,可以看出kernel_file是由root用户创建的。

    可以看出内容符合预期。

    3. 代码分析

        fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644);---------------------创建用户空间文件,获取文件句柄。
        if (IS_ERR(fp)){
            printk("create file error
    ");
            return -1;
        }
        fs =get_fs();----------------------------------------------------------------------------------------获取当前线程的thread_info->addr_limit。
        set_fs(KERNEL_DS);-----------------------------------------------------------------------------------将能访问的空间thread_info->addr_limit扩大到KERNEL_DS。
        pos =0;
        vfs_write(fp,buf, sizeof(buf), &pos);----------------------------------------------------------------调用vfs_write写内容
        pos =0;
        vfs_read(fp,buf1, sizeof(buf), &pos);----------------------------------------------------------------调用vfs_read读取内容
        printk("Write contet=%s
    ",buf1);
        filp_close(fp,NULL);---------------------------------------------------------------------------------关闭文件
        set_fs(fs);------------------------------------------------------------------------------------------将thread_info->addr_limit切换回原来值

    4. 原理

    4.1 set_fs和get_fs

    有下面代码可知KERNEL_DS范围很大,到0xffffffffffffffff

    而USER_DS范围较小,到0x7ffffffff000。

    由Linux内存分布图可知,KERNEL_DS意味着可以访问整个内存所有空间,USER_DS只能访问用户空间内存。

    通过set_fs可以改变thread_info->addr_limit的大小。

    /*
     * For historical reasons, the following macros are grossly misnamed:
     */
    #define KERNEL_DS    ((mm_segment_t) { ~0UL })        /* cf. access_ok() */
    #define USER_DS        ((mm_segment_t) { TASK_SIZE-1 })    /* cf. access_ok() */
    
    #define VERIFY_READ    0
    #define VERIFY_WRITE    1
    
    #define get_ds()  (KERNEL_DS)
    #define get_fs()  (current_thread_info()->addr_limit)
    #define set_fs(x) (current_thread_info()->addr_limit = (x))
    
    
    #define TASK_SIZE           DEFAULT_TASK_SIZE 

    4.2 vfs_write和vfs_read对addr_limit的检查

    将代码修改一下,不进行addr_limit扩大,看看结果如何。

    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>
    
    static char buf[] ="来自内核的访问
    ";
    static char buf1[32];
     
    int __init test_init(void)
    {
        struct file *fp;
        mm_segment_t fs;
        loff_t pos;
        int ret;
        
        printk("KERNEL_DS=0x%llx USER_DS=0x%llx get_fs()=0x%llx
    ", KERNEL_DS, USER_DS, get_fs());
        fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644);
        if (IS_ERR(fp)){
            printk("create file error
    ");
            return -1;
        }
        fs =get_fs();
        //set_fs(KERNEL_DS);
        pos =0;
        printk("fp=%p, buf=%p get_fs()=0x%llx
    ", fp, buf, get_fs());
        ret = vfs_write(fp,buf, sizeof(buf), &pos);
        printk("ret=%d
    ", ret);
        pos =0;
        printk("fp=%p, buf1=%p
    ", fp, buf1);
        ret = vfs_read(fp,buf1, sizeof(buf), &pos);
        printk("ret=%d Write contet=%s
    ", ret, buf1);
        filp_close(fp,NULL);
        //set_fs(fs);
        return 0;
    }
    void __exit test_exit(void)
    {
        printk("test exit
    ");
    }
     
    module_init(test_init);
    module_exit(test_exit);
     
    MODULE_LICENSE("GPL");

    执行结果如下,可以看出fp、buf、buf1都位于内核空间。而当前空间的get_fs()为0x7ffffffff000,这些地址都超出当前空间。

    所以vfs_read和vfs_write返回值都是-14,即“Bad address”。

    [49001.240705] KERNEL_DS=0xffffffffffffffff USER_DS=0x7ffffffff000 get_fs()=0x7ffffffff000
    [49001.240713] fp=ffff8800cae06900, buf=ffffffffc0305000 get_fs()=0x7ffffffff000
    [49001.240714] ret=-14
    [49001.240715] fp=ffff8800cae06900, buf1=ffffffffc03053c0
    [49001.240716] ret=-14 Write contet=
    [49013.464812] test exit

    简单看一下vfs_write和vfs_read,两者都调用access_ok对地址合法性进行检查,严禁addr大于当前get_fs()。

    此处buf和buf1都不满足条件,所以返回-EFAULT。

    #define __access_ok(addr, size, segment)                        
    ({                                            
        __chk_user_ptr(addr);                                
        (likely((unsigned long) (addr) <= (segment).seg)                
         && ((segment).seg == KERNEL_DS.seg                        
             || likely(REGION_OFFSET((unsigned long) (addr)) < RGN_MAP_LIMIT)));    
    })
    #define access_ok(type, addr, size)    __access_ok((addr), (size), get_fs())
    
    
    ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
    {
    ...
        if (unlikely(!access_ok(VERIFY_READ, buf, count)))
            return -EFAULT;
    ...
    }
    
    ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
    {
    ...
        if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
            return -EFAULT;
    ...
    }

    将测试代码红色部分打开,扩大addr_limit空间。

    可以看出当前thread_info->addr_limit变成了0xffffffffffffffff。

    所以vfs_write和vfs_read的access_ok检查得以通过,程序得到正确执行。

    [48937.547119] KERNEL_DS=0xffffffffffffffff USER_DS=0x7ffffffff000 get_fs()=0x7ffffffff000
    [48937.547138] fp=ffff8800c8300c00, buf=ffffffffc02f3000 get_fs()=0xffffffffffffffff
    [48937.547155] ret=23
    [48937.547158] fp=ffff8800c8300c00, buf1=ffffffffc02f33c0
    [48937.547164] ret=23 Write contet=xffffffe6xffffff9dxffffffa5xffffff9dxffffffa5xffffffe8xffffff87xffffffaaxffffff87xffffffaaxffffffe5xffffff86xffffff85xffffff86xffffff85xffffffe6xffffffa0xffffffb8xffffffa0xffffffb8xffffffe7xffffff9axffffff84xffffff9axffffff84xffffffe8xffffffaexffffffbfxffffffaexffffffbfxffffffe9xffffff97xffffffaexffffff97xffffffae
    [48937.547164] 
    [48940.600703] test exit

      

    5. 小结

    只有使用上面的方法,才能在内核中使用open,write等的系统调用。

    其实这样做的主要原因是open,write的参数在用户空间,在这些系统调用的实现里需要对参数进行检查,就是检查它的参数指针地址是不是用户空间的。

    系统调用本来是提供给用户空间的程序访问的,所以,对传递给它的参数(比如上面的buf、buf1),它默认会认为来自用户空间。

    在vfs_write()函数中,为了保护内核空间,一般会用get_fs()得到的值来和USER_DS进行比较,从而防止用户空间程序“蓄意”破坏内核空间。

    为了解决这个问题, set_fs(KERNEL_DS)将其能访问的空间限制扩大到KERNEL_DS,这样就可以在内核顺利使用系统调用了!

    内核使用系统调用参数肯定是内核空间,为了不让这些系统调用检查参数所以必须设置  set_fs(KERNEL_DS)才能使用该系统调用。

    vfs_write的流程可调用access_ok,而access_ok会判断访问的buf是否在0~addr_limit之间,如何是就ok;否则-EFAULT,这显然是为用户准备的检查。

    addr_limit一般设为USER_DS,在内核空间,buf肯定>USER_DS,必须修改addr_limit,这就是set_fs的由来。

  • 相关阅读:
    【C++ 系列笔记】03 C++ 面向对象进阶
    【C++ 系列笔记】02 C++ 面向对象基础
    【C++ 系列笔记】01 C++ 与 C
    【JavaScript】简单取随机数 ~~(Math.random() * number)
    英语测试
    Linux指令入门
    RE-攻防世界 T3 insanity
    PWN-攻防世界 level0
    ISCC不会的理论题
    kali linux配置ssh
  • 原文地址:https://www.cnblogs.com/arnoldlu/p/8879800.html
Copyright © 2011-2022 走看看