精华内容
下载资源
问答
  • 修改内核 内存 属性 模块
    2021-05-10 16:40:11

    最近,我们在进行一项安全研究时,需要在任意进程中修改内存空间的保护标志。起初,我们发现这项任务看起来很简单,但在实际操作中,却发现困难重重,还好这些都不是什么大问题。在解决这些问题的过程中,我们还学到了一些新的东西,主要是关于 Linux 机制和内核开发的。在以下的详解中,我们会介绍我们所采取的三种方法以及每次寻求更好解决方案的原因。

    背景介绍

    在现代操作系统中,每个进程都有自己的虚拟地址空间(从虚拟地址到物理地址的映射)。此虚拟地址空间由内存页面(某些固定大小的连续内存块)组成,且每个页面都有保护标志,这些保护标志决定了允许对该页面的访问类型 ( 读取、写入和执行 ) 。不过,这种机制依赖于架构页表(architecture page table)。不过要注意的是,在 x64 的架构中,你不能只进行页面写入,即使你是特意从操作系统请求的,也都同时具有页面写入和可读的功能。

    在 Windows 中,你可以使用 API 函数 VirtualProtect 或 VirtualProtectEx 修改内存空间的保护。VirtualProtectEx 使我们的修改任务变得非常简单:因为它的第一个参数 hProcess 是 "要修改其内存保护的进程的句柄"。

    不过,在 Linux 中,修改过程就没有这么简单了,因为修改内存保护的 API 是系统调用 mprotect 或 pkey_mprotect 的结果,并且这两个函数始终在当前进程的地址空间上运行。现在让我们想办法解决一下如何在 x64 架构上的 Linux 中解决修改的问题,不过前提条件是,我们具有修改设备的 root 权限。

    方法一:代码注入

    如果 mprotect 总是在当前进程中运行,我们需要让目标进程从它自己的上下文中调用它。这时就要用到代码注入了,该方法可以通过许多不同的方式实现。我们可以选择使用 ptrace 机制实现它,该机制允许一个进程 " 观察和控制另一个进程的执行 ",包括修改目标进程的内存和寄存器的能力。这种机制用于调试器 ( 如 gdb ) 和跟踪实用程序 ( 如 strace ) ,使用 ptrace 注入代码所需的步骤如下 :

    1. 使用 ptrace 附加到目标进程,如果进程中有多个线程,那么最好停止所有其他线程;

    2. 找到一个可执行的内存空间(通过检查 / proc / PID / maps),并在这个空间编写操作码 syscall ( 十六进制 :0f05 ) ;

    3. 根据调用约定来修改寄存器,首先,将 rax 修改为 mprotect 的系统调用号(即 10);然后,前三个参数 ( 即起始地址、长度和所需的保护 ) 分别存储在 rdi、rsi 和 rdx 中;最后,将 rip 修改为步骤 2 中使用的地址;

    4. 继续这个过程,直到系统调用返回 ( ptrace 允许你跟踪系统调用的进入和退出 ) ;

    5. 恢复被修改的内存和寄存器,从进程中将其分离并恢复正常执行;

    这种方法是我们的采用的第一个也是最直观的方法,并且非常有效。不过在我们发现了 Linux 中的另一种完全破坏机制:利用 seccomp 进行破坏之后,该方法就不是我们的最优选择了。基本上,它是 Linux 内核中的一个安全工具,允许进程输入某种形式的 " 监狱 ",除了 read,write,_exit 和 sigreturn 之外,它不能进行任何系统调用。还有一个选项,可以指定任意的系统调用及针对它们的过滤参数。

    因此,如果进程启用了 seccomp 模式并且我们尝试将一个对 mprotect 的调用注入其中,那么内核将终止进程,因为该进程是不允许使用此系统调用的。因此,要对这些进程进行调用,就要采用方法二。

    方法二:在内核模块中模拟 mprotect 系统调用

    seccomp(全称 securecomputing mode)是 linuxkernel 从 2.6.23 版本开始所支持的一种安全机制。

    在 Linux 系统里,大量的系统调用直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。通过 seccomp,我们限制程序使用某些系统调用,这样可以减少系统的暴露面,同时是程序进入一种 " 安全 " 的状态。

    由于 Linux 中存在另一种完全破坏机制:利用 seccomp 进行破坏,因此这个方法肯定要在内核模式中进行。在 Linux 内核中,每个线程(包括用户线程和内核线程)都由一个名为 task_struct 的结构表示,并且当前线程 ( 任务 ) 可以通过 pointer current 访问。内核中 mprotect 的内部实现使用了 pointer current,因此我们的第一个想法是,只要将 mprotect 的代码复制粘贴到内核模块中,并将每次出现的 current 替换为指向目标线程 task_struct 的指针,不就可以了吗 ?

    接下来的事情你可能已经猜到了,就是复制 C 代码,不过复制过程并不是你想的那么简单,因为其中存在大量使用我们无法访问的未导出的函数、变量和宏。某些函数说明会在标头文件中导出,但是它们的实际地址不是由内核导出的。如果内核是用 linux 内核符号表 kallsyms 编译的,那么通过文件 / proc / kallsysm 导出所有内部符号,这个特定的问题就可以解决。因为 kallsyms 在进行源码调试时具有相当重要的作用,它可以描述所有不处在堆栈上的内核符号。linux 内核在编译的过程中,将内核中所有的符号(所有的内核函数以及已经装载的模块)及符号的地址以及符号的类型信息都保存在了 /proc/kallsyms 文件中。

    尽管存在这个特定问题,我们仍然试图实现 mprotect 调用。为此,我们特意编写一个内核模块,利用该模块获取目标 PID 和参数以进行 mprotect,并模仿其调用行为。首先,我们需要获取所需的内存映射对象,用它表示线程的地址空间:

    /* Find the task by the pid */ pid_struct = find_get_pid ( params.pid ) ; if ( !pid_struct ) return -ESRCH; task = get_pid_task ( pid_struct, PIDTYPE_PID ) ; if ( !task ) { ret = -ESRCH; goto out; } /* Get the mm of the task */ mm = get_task_mm ( task ) ; if ( !mm ) { ret = -ESRCH; goto out; } … … out: if ( mm ) mmput ( mm ) ; if ( task ) put_task_struct ( task ) ; if ( pid_struct ) put_pid ( pid_struct ) ;

    现在我们已经获得了内存映射对象,这大大方便了以后的操作。 Linux 内核实现了一个抽象层来管理内存空间,每个空间由结构 vm_area_struct 表示。为了找到正确的内存空间,我们使用函数 find_vma,该函数会根据所需地址搜索内存映射。

    vm_area_struct 包含字段 vm_flags,它以独立于架构的方式来表示内存空间的保护标志,vm_page_prot 也以独立于架构的方式来表示内存空间的保护标志。单独修改这些字段并不会真正影响页表(但会影响 /proc/PID/maps 的输出,我们已经尝试过了),详情请点击这里。

    在对内核代码进行了一些阅读和深入研究之后,我们发现要真正攻破内存空间的保护,最重要的工作是以下 3 方面 :

    1. 将字段 vm_flags 修改为所需的保护;

    2. 调用函数 vma_set_page_prot_func,再根据 vm_flags 字段来更新字段 vm_page_prot;

    3. 调用 change_protection_func 函数来实际修改页表中的保护位;

    虽然以上的那段代码很有效,但其中也存在着很多问题。首先,我们只实现了 mprotect 的基本部分,但原始函数的基本功能却比我们能开发的要多得多,例如,通过保护标志分离和连接内存空间。其次,我们使用了两个内核函数(vma_set_page_prot_func 和 change_protection_func),这些函数不是由内核导出的。此时,我们可以使用 kallsyms 来调用它们,但是这很容易出现问题,因为将来我们可能会修改它们的名称,或者将内存空间的整个内部实现进行修改。不过,我们想要一个更通用的解决方案,即不考虑内部结构的方案,此时,就有了方法三。

    方法三:使用目标进程的内存映射

    方法三与第一种方法非常相似,即都要目标进程的上下文中执行代码。虽然,这两个方法都可以在我们自己的线程中执行代码,但在方法三中,我们使用的是目标进程的 " 内存上下文 ",这意味着,我们要使用内存中的地址空间。

    我们通过几个 API 函数就可以在内核模式下修改地址空间,其中就用到了 use_mm。正如 use_mm 的介绍中明确指出的那样 " 此例程仅会被用于从内核线程上下文中进行调用 "。由于这些线程是在内核中创建的,不需要任何用户地址空间,因此可以修改它们的地址空间(地址空间内的内核区域在每个任务中都以相同的方式映射)。

    在内核线程中运行代码的一种简单方法就是通过内核的运行队列接口(queue interface),它允许你使用特定例程和特定参数来进行进程调用。我们的工作例程也非常简单,它会获取所需进程的内存映射对象和 mprotect 的参数,并执行以下操作(do_mprotect_pkey 是内核中实现 mprotect 和 pkey_mprotect 系统调用的内部函数):

    use_mm ( suprotect_work->mm ) ; suprotect_work->ret_value = do_mprotect_pkey ( suprotect_work->start, suprotect_work->len, suprotect_work->prot, -1 ) ; unuse_mm ( suprotect_work->mm ) ;

    当我们的内核模块在某个进程(通过一个特殊的 IOCTL)获得修改保护的请求时,该请求首先会找到所需的内存映射对象(正如我们在前面的方法中所解释的那样),然后再使用正确的参数来调用进程。

    不过这个解决方案仍有一个小问题,即函数 do_mprotect_pkey_func 不会由内核导出,需要使用 kallsyms 获取。与第一个解决方案不同,这个解决方案中的内部函数不太容易被修改,因为该函数与系统调用 pkey_mprotect 有关,而且我们也不用处理内部结构,因此我们只能将其称为 " 小问题 "。

    我们希望你在这篇文章中找到一些有趣的信息和技巧,学会如何在任意进程中修改内存保护属性。如果你有兴趣,可以在github中找到这个概念验证内核模块的源代码。

    更多相关内容
  • 关于linux内核的学习,可以参考华清远见《Linux内核修炼培训班》的课程大纲。 初次接触Linux内核,好仔细阅读顶层目录的readme文件,它是Linux内核的概述和编译命令说明。readme的说明侧重于X86等通用的平台,对于...

    很多粉丝在群里提问,如何把一个模块文件编译到内核中或者独立变异成ko文件。本文给大家详解讲解。

    1. 内核目录

    Linux内核源代码非常庞大,随着版本的发展不断增加。它使用目录树结构,并且使用Makefile组织配置、编译。

    初次接触Linux内核,好仔细阅读顶层目录的readme文件,它是Linux内核的概述和编译命令说明。readme的说明侧重于X86等通用的平台,对于某些特殊的体系结构,可能有些特殊的说明。

    顶层目录的Makefile是整个内核配置编译的核心文件,负责组织目录树中子目录的编译管理,还可以设置体系结构和版本号等。

    内核源码的顶层有许多子目录,分别组织存放各种内核子系统或者文件。
    具体的目录说明如下表所示。

    目录内容
    arch/体系结构相关的代码,如arch/i386、arch/arm、arch/ppc
    crypto常用加密和散列算法(如AES、SHA等),以及一些压缩和CRC校验算法
    drivers/各种设备驱动程序,如drivers/char、drivers/block……
    documentation/内核文档
    fs/文件系统,如fs/ext3、fs/jffs2……
    include/内核头文件:include/asm是体系结构相关的头文件,它是include/asm-arm、include/asm-i386等目录的链接;include/linux是Linux内核基本的头文件
    init/Linux初始化,如main.c
    ipc/进程间通信的代码
    kernel/Linux内核核心代码(这部分比较小)
    lib/各种库子程序,如zlib、crc32
    mm/内存管理代码
    net/网络支持代码,主要是网络协议
    sound声音驱动的支持
    scripts/内部或者外部使用的脚本
    usr/用户的代码

    2. 编译工具

    1. make mrproper: 清除内核生成的配置文件与目标文件等,一般在第一次编译时使用

    2. 导入默认配置信息(在内核根目录中)

    a) make xxx_deconfig
    b) cp arch/arm/configs/xx_deconfig  .config
    生成默认配置文件
    
    1. 配置命令
    make xxxxconfig  修改配置文件
    make xconfig (图形界面 qt库)
    make menuconfig (常用 libncurses库)
    sudo apt-get install libncurses5-dev
    make config (精简)
    
    1. 编译内核
    make uImage ---生成内核镜像  /arch/arm/boot/uImage
    
    1. 编译设备树
    make dtbs ---生成设备树文件  /arch/arm/boot/dtb/xxxxxx.dtb
    
    1. 编译生成模块文件
    make modules ---把配置值选成M的代码编译生成模块文件。(.ko)  放在对应的源码目录下。
    

    3. 内核编译

    现在很多基于Linux的产品开发,通常厂家都会提供集成开发环境SDK。builroot使我们搭建环境变得更加方便,但是作为初学者我们还是要掌握如何独立编译内核源码。

    0) 前提条件

    必须先安装交叉编译工具链,关于交叉编译工具链的安装可以参考
    linux环境搭建-ubuntu16.04安装

    在这里我们使用的是arm-none-linux-gnueabi-gcc。

    1)下载内核源码

    下载地址:
    https://mirrors.edge.kernel.org/pub/linux/kernel/

    我们下载Linux-3.14内核(可以是更高的版本)至/home/peng目录。

    或者直接点击下面链接
    https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/linux-3.14.10.tar.xz

    解开压缩包,并进入内核源码目录,具体过程如下:

    $ tar  xvf  linux-3.14.tar.xz
    $ cd  linux-3.14
    

    2)修改内核目录树根下的Makefile,指明交叉编译器:

       $ vim Makefile
    

    找到ARCH和CROSS_COMPILE,
    修改:

    ARCH		?= $(SUBARCH)
    CROSS_COMPILE	?= $(CONFIG_CROSS_COMPILE:"%"=%)
    

    ARCH		?= arm
    CROSS_COMPILE	?= arm-none-linux-gnueabi-
    

    4)配置内核产生.config文件:

    导入默认配置

    $ make  exynos_defconfig
    

    这里我们假定要编译的内核最终在三星的板子上运行,soc名字是exynos,三星公司其实已经将自己的配置文件放置在
    ./arch/arm/configs/exynos_defconfig

    执行这个命令,最终会在内核根目录下生成.config文件,

    我们编译内核就完全依赖这个文件。
    该文件是exynos开发板所需要的一些内核模块宏定义和参数设置,这些值是厂商给的一个初始配置。
    实际项目开发中,需要在这个配置文件基础之上再重新移植自己需要的对应的驱动模块。

    5)配置内核模块

    输入内核配置命令,进行内核选项的选择,命令如下:

    $ make menuconfig
    

    命令执行成功以后,会看到如下图所示的界面。其实我们在图1.5中看到过同样功能的界面,那个图也是内核选项配置界面,只不过那个界面在X-window下才能执行。

    内核选项配置界面其中:

    1. 子菜单—>

    表示有子菜单,按下回车可以进入子菜单。

    1. 中括号[]
      在每一个选项前都有个括号,有的是中括号,有的是尖括号,还有的是圆括号。


    [] 表示该选项只有两种选项,中括号中要么是空,要么是“*”;

    用空格键可以做出选择。

    1. 尖括号<>

      <>选择相应的配置时,有3种选择,它们代表的含义分别如下。
    *:将该功能编译进内核。
    ● 空:不将该功能编译进内核。
    ● M:将该功能编译成可以在需要时动态插入到内核中的模块。
    
    1. 模块配置圆括号()
      而圆括号的内容是要你在所提供的几个选项中选择一项。

    如果使用的是make xconfig,使用鼠标就可以选择对应的选项。如果使用的是make menuconfig,则需要使用回车键进行选取。

    在编译内核的过程中,麻烦的事情就是配置这步工作了。初次接触Linux内核的开发者往往弄不清楚该如何选取这些选项。

    实际上,在配置时,大部分选项可以使用其默认值,只有小部分需要根据用户不同的需要选择。

    选择的原则是将与内核其他部分关系较远且不经常使用的部分功能代码编译成为可加载模块,这有利于减小内核的长度,减少内核消耗的内存,简化该功能相应的环境改变时对内核的影响;不需要的功能就不要选;与内核关系紧密而且经常使用的部分功能代码直接编译到内核中。

    6)编译内核:

    root@ubuntu:/home/peng/linux-3.14# make uImage
    

    uImage

    如果按照默认的配置,没有改动的话,编译后系统会在arch/arm/boot目录下生成一个uImage文件,这个文件就是刚刚生成的。

    7)下载Linux内核

    因为不同的板子对应的uboot版本都不一样,所以下载程序的uboot命令也会有所差异,关于验证,本文暂不讨论。

    4. 独立驱动程序的编译

    1. 编译成独立模块

    假定我们有以下驱动程序,要编译成可以加载到开发板的独立ko文件

    hello.c

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kdev_t.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    //#include <io/uaccess.h>
    #include <linux/device.h>
    #include <asm/io.h>
    #include <asm/uaccess.h>
    
    static int major = 237;
    static int minor = 0;
    static dev_t devno;
    struct device *class_dev = NULL;
    struct class *cls;
    
    
    static int hello_open (struct inode *inode, struct file *filep)
    {
    	printk("hello_open()\n");
    	return 0;
    }
    static int hello_release (struct inode *inode, struct file *filep)
    {
    	printk("hello_release()\n");
    
    	return 0;
    }
    
    #define KMAX_LEN 32
    char kbuf[KMAX_LEN+1] = "kernel";
    
    
    //read(fd,buff,40);
    
    static ssize_t hello_read (struct file *filep, char __user *buf, size_t size, loff_t *pos)
    {
    	int error;
    
    	
    	if(size > strlen(kbuf))
    	{
    		size = strlen(kbuf);
    	}
    
    	if(copy_to_user(buf,kbuf, size))
    	{
    		error = -EFAULT;
    		return error;
    	}
    
    	return size;
    }
    //write(fd,buff,40);
    static ssize_t hello_write (struct file *filep, const char __user *buf, size_t size, loff_t *pos)
    {
    	int error;
    
    	if(size > KMAX_LEN)
    	{
    		size = KMAX_LEN;
    	}
    	memset(kbuf,0,sizeof(kbuf));
    	if(copy_from_user(kbuf, buf, size))
    	{
    		error = -EFAULT;
    		return error;
    	}
    	printk("%s\n",kbuf);
    	return size;
    }
    
    
    static struct file_operations hello_ops = 
    {
    	.open = hello_open,
    	.release = hello_release,
    	.read = hello_read,
    	.write = hello_write,
    };
    static int hello_init(void)
    {
    	int result;
    	
    	printk("hello_init \n");
    	result = register_chrdev( major, "hello", &hello_ops);
    	if(result < 0)
    	{
    		printk("register_chrdev fail \n");
    		return result;
    	}
    	cls = class_create(THIS_MODULE, "hellocls");
    	if (IS_ERR(cls)) {
    		printk(KERN_ERR "class_create() failed for cls\n");
    		result = PTR_ERR(cls);
    		goto out_err_1;
    	}
    	devno = MKDEV(major, minor);
    	
    	class_dev = device_create(cls, NULL, devno, NULL, "hellodev");
    	if (IS_ERR(class_dev)) {
    		result = PTR_ERR(class_dev);
    		goto out_err_2;
    	}
    	
    	return 0;
    
    out_err_2:
    	class_destroy(cls);
    out_err_1:
    	unregister_chrdev(major,"hello");
    	return 	result;
    }
    static void hello_exit(void)
    {
    	printk("hello_exit \n");
    	device_destroy(cls, devno);
    	class_destroy(cls);
    	unregister_chrdev(major,"hello");
    	return;
    }
    module_init(hello_init);
    module_exit(hello_exit);
    MODULE_LICENSE("GPL");
    //proc/devices
    
    

    注意我们需要编写Makefile如下:

    ifneq ($(KERNELRELEASE),)
    obj-m:=hello.o
    else
    KDIR :=/home/peng/linux-3.14
    PWD  :=$(shell pwd)
    all:
    	make -C $(KDIR) M=$(PWD) modules
    clean:
    	rm -f *.ko *.o *.mod.o *.symvers *.cmd  *.mod.c *.order
    endif
    

    关于Makefile的详解,大家可以参考我们之前的文章
    手把手教Linux驱动1-模块化编程
    其中内核路径:

    KDIR :=/home/peng/linux-3.14
    

    必须是我们刚才编译过的内核源码根目录。

    编译时,程序可以放到其他目录下:


    用file命令查看文件属性,是基于ARM的。该模块文件就是与前面编译的内核配套的驱动模块,如果开发板的内核版本与上面编译的版本号一致,那么该模块文件就可以在开发板上insmod。

    2. 编译到内核

    步骤:

    • 1)拷贝文件
      如果要将刚才的驱动程序直接编译到内核,那么我们必须把hello.c拷贝到内核的某个目录下。

    字符设备可以考虑放到以下目录:

    linux-3.14/drivers/char
    

    • 2)修改Makefile
    root@ubuntu:/home/peng/linux-3.14/drivers/char# vim Makefile 
    

    修改如下:

    该行内容是根据宏CONFIG_HELLO来决定是否编译hello.c这个文件。

    • 3)修改Kconfig

    7 HELLO 取前面步骤CONFIG_HELLO下划线后面的字符串
    8 tristate 表示该模块最终有3个选项 空 * M
    9 表示该模块依赖的模块,如果ARCH_EXYNOS4模块没有被选中,那么HELLO模块也不会被编译到内核
    10 帮助信息

    • 4) 重新配置
      执行
    make menuconfig
    

    进入配置页面,

    输入 / 可以根据关键字查找模块所在位置。


    我们添加的模块文件的位置:

    根据路径

    -> Device Drivers 
       -> Character devices
    

    找到我们刚才的模块配置路径

    此处是尖括号,因为我们设置的属性是tristate
    移动到Help处,可以看到前面我们填充的帮助信息

    我们可以按下空格键设置为*,编译到内核中。

    选择Save,

    然后再点击2次Exit,就可以退出。

    • 5)重新编译内核
    root@ubuntu:/home/peng/linux-3.14# make uImage
    

    这样,我们的模块编译到了新生成的内核模块文件中。

    3. 补充

    前面一节其实最终目的是生成CONFIG_HELLO=y 这个定义信息,并把该信息保存到内核根目录的.config文件中。

    其实我们如果不修改Kconfig,直接在.config中增加这个宏定义也是可以的。

    今天内容就到这里,还等什么?抓紧操练起来吧。

    文中用到的虚拟机,叫交叉编译工具,还有源代码,可以关注公众号,后台回复 ubuntu,即可获得。

    展开全文
  • (36)内核空间与内核模块,遍历内核模块链表

    千次阅读 热门讨论 2020-10-18 23:13:04
    一、内核空间 每个进程的低2G都是独立的,而高2G是共享的。 我们可以做一个小实验,在一个进程的高2G定义申请一块内存,去另一个进程里,用相同的线性地址读取,会发现是同一块物理内存。 驱动A #include <ntddk...

    一、内核空间

    每个进程的低2G都是独立的,而高2G是共享的。

    在这里插入图片描述

    我们可以做一个小实验,在一个进程的高2G定义申请一块内存,去另一个进程里,用相同的线性地址读取,会发现是同一块物理内存。

    驱动A

    #include <ntddk.h>
    
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path);
    VOID DriverUnload(PDRIVER_OBJECT driver);
    
    
    // 高2G申请一块内存
    UINT32 g_H2GValue = 0;
    
    // 入口函数,相当于main
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
    {
    	g_H2GValue = 0x20201018;
    	DbgPrint("[%p]: %08X\n", &g_H2GValue, g_H2GValue);
    	driver->DriverUnload = DriverUnload;
    	return STATUS_SUCCESS;
    }
    
    // 卸载函数
    VOID DriverUnload(PDRIVER_OBJECT driver)
    {
    	DbgPrint("驱动卸载成功\n");
    }
    

    在这里插入图片描述

    驱动B

    #include <ntddk.h>
    
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path);
    VOID DriverUnload(PDRIVER_OBJECT driver);
    
    // 入口函数,相当于main
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
    {
    	PUINT32 pUint32 = (PUINT32)0xF88DB01C; // 驱动A变量的线性地址,这个值是驱动A打印的
    	DbgPrint("驱动B读取驱动A的变量值: %08X\n", *pUint32);
    	driver->DriverUnload = DriverUnload;
    	return STATUS_SUCCESS;
    }
    
    // 卸载函数
    VOID DriverUnload(PDRIVER_OBJECT driver)
    {
    	DbgPrint("驱动卸载成功\n");
    }
    

    在这里插入图片描述

    二、内核模块,驱动名字的由来

    在这里插入图片描述

    高2G里有许多模块,操作系统内核(如101012分页的ntoskrnl.exe)也在其中。接下来的课后试验我们会编程遍历高2G模块。

    内核模块一般是.sys,也可以是其他格式,他们都遵循PE格式。

    我们经常说“驱动”,这个名字的来源其实是内核程序入口函数的参数。

    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path);
    

    PDRIVER_OBJECT 驱动对象,就是驱动这个名字的由来。

    三、PDRIVER_OBJECT 驱动对象

    我们可以在windbg中查看 _DRIVER_OBJECT 结构体:

    kd> dt _DRIVER_OBJECT
    ntdll!_DRIVER_OBJECT
       +0x000 Type             : Int2B
       +0x002 Size             : Int2B
       +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT
       +0x008 Flags            : Uint4B
       +0x00c DriverStart      : Ptr32 Void
       +0x010 DriverSize       : Uint4B
       +0x014 DriverSection    : Ptr32 Void
       +0x018 DriverExtension  : Ptr32 _DRIVER_EXTENSION
       +0x01c DriverName       : _UNICODE_STRING
       +0x024 HardwareDatabase : Ptr32 _UNICODE_STRING
       +0x028 FastIoDispatch   : Ptr32 _FAST_IO_DISPATCH
       +0x02c DriverInit       : Ptr32     long 
       +0x030 DriverStartIo    : Ptr32     void 
       +0x034 DriverUnload     : Ptr32     void 
       +0x038 MajorFunction    : [28] Ptr32     long 
    

    挑几个比较重要的属性来说明:

    DriverStart:驱动在内存中的基址
    DriverSize:驱动在内存中的大小
    DriverSection:内核模块链表基址(这个待会详细说)
    DriverName:驱动名

    这样看起来干巴巴的,干脆我们写一个驱动,看看它里面这个结构的数据是长什么样的:

    #include <ntddk.h>
    
    // 卸载函数
    VOID DriverUnload(PDRIVER_OBJECT driver)
    {
    	DbgPrint("驱动程序停止运行了.\r\n");	
    }
    
    // 入口函数,相当于main
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
    {	
    	DbgPrint("PDRIVER_OBJECT: %p %wZ\n",driver,reg_path);
    	// 设置一个卸载函数,便于退出
    	driver->DriverUnload = DriverUnload;
    	return STATUS_SUCCESS;
    }
    

    在这里插入图片描述

    在windbg中查看这个驱动进程的 _DRIVER_OBJECT 结构体:

    kd> dt _DRIVER_OBJECT 81ECC880 
    ntdll!_DRIVER_OBJECT
       +0x000 Type             : 0n4
       +0x002 Size             : 0n168
       +0x004 DeviceObject     : (null) 
       +0x008 Flags            : 0x12
       +0x00c DriverStart      : 0xf8910000 Void
       +0x010 DriverSize       : 0x6000
       +0x014 DriverSection    : 0x81d65498 Void
       +0x018 DriverExtension  : 0x81ecc928 _DRIVER_EXTENSION
       +0x01c DriverName       : _UNICODE_STRING "\Driver\内核编程基础"
       +0x024 HardwareDatabase : 0x80690a90 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
       +0x028 FastIoDispatch   : (null) 
       +0x02c DriverInit       : 0xf8911020     long  _empty_!DriverEntry+0
       +0x030 DriverStartIo    : (null) 
       +0x034 DriverUnload     : 0xf8911000     void  _empty_!DriverUnload+0
       +0x038 MajorFunction    : [28] 0x804fb87e     long  nt!IopInvalidDeviceRequest+0
    

    可以看到,除了刚才说的几个属性,HardwareDatabase 其实就是入口函数第二个参数,我们用工具注册驱动时,就是在注册表里做了修改,用的就是这个字符串。

    接下来,着重介绍 DriverSection 属性。

    四、DriverSection / 内核模块链表

    在windbg 中查看 DriverSection 属性,类型是 void,它实际上是 _LDR_DATA_TABLE_ENTRY 类型。这个结构体我在上一篇《3环PEB断链》中介绍了,它是一个链表的项。

    kd> dt _LDR_DATA_TABLE_ENTRY
    ntdll!_LDR_DATA_TABLE_ENTRY
       +0x000 InLoadOrderLinks : _LIST_ENTRY
       +0x008 InMemoryOrderLinks : _LIST_ENTRY
       +0x010 InInitializationOrderLinks : _LIST_ENTRY
       +0x018 DllBase          : Ptr32 Void
       +0x01c EntryPoint       : Ptr32 Void
       +0x020 SizeOfImage      : Uint4B
       +0x024 FullDllName      : _UNICODE_STRING
       +0x02c BaseDllName      : _UNICODE_STRING
       +0x034 Flags            : Uint4B
       +0x038 LoadCount        : Uint2B
       +0x03a TlsIndex         : Uint2B
       +0x03c HashLinks        : _LIST_ENTRY
       +0x03c SectionPointer   : Ptr32 Void
       +0x040 CheckSum         : Uint4B
       +0x044 TimeDateStamp    : Uint4B
       +0x044 LoadedImports    : Ptr32 Void
       +0x048 EntryPointActivationContext : Ptr32 Void
       +0x04c PatchInformation : Ptr32 Void
    

    和3环有点区别,在0环中InMemoryOrderLinks 和 InInitializationOrderLinks 是没用的,只需要关注第一个链表 InLoadOrderLinks。_LIST_ENTRY 这个结构体存了两个地址,指向前一个节点和下一个节点:

    kd> dt _LIST_ENTRY
    ntdll!_LIST_ENTRY
       +0x000 Flink            : Ptr32 _LIST_ENTRY
       +0x004 Blink            : Ptr32 _LIST_ENTRY
    

    这里多一句嘴,我们在Windows中见到过很多"ENTRY"了,PDE PTE,还有这里的LIST_ENTRY,这个ENTRY其实就是“项”的意思。

    通过这个 InLoadOrderLinks,我们可以遍历整个高2G的模块了。InLoadOrderLinks.Flink 指向的就是下一个 _LDR_DATA_TABLE_ENTRY。下面给出遍历内核模块链表的代码:

    #include <ntddk.h>
    
    typedef struct _LDR_DATA_TABLE_ENTRY
    {
    	LIST_ENTRY InLoadOrderLinks;
    	LIST_ENTRY InMemoryOrderLinks;
    	LIST_ENTRY InInitializationOrderLinks;
    	PVOID DllBase;
    	PVOID EntryPoint;
    	UINT32 SizeOfImage;
    	UNICODE_STRING FullDllName;
    	UNICODE_STRING BaseDllName;
    	UINT32 Flags;
    	UINT16 LoadCount;
    	UINT16 TlsIndex;
    	LIST_ENTRY HashLinks;
    	PVOID SectionPointer;
    	UINT32 CheckSum;
    	UINT32 TimeDateStamp;
    	PVOID LoadedImports;
    	PVOID EntryPointActivationContext;
    	PVOID PatchInformation;
    } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
    
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path);
    VOID DriverUnload(PDRIVER_OBJECT driver);
    
    // 入口函数,相当于main
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
    {
    	PLDR_DATA_TABLE_ENTRY pLdteHead; // 内核模块链表头
    	PLDR_DATA_TABLE_ENTRY pLdteCur; // 遍历指针
    	
    	pLdteHead = (PLDR_DATA_TABLE_ENTRY)driver->DriverSection;
    	pLdteCur = pLdteHead;
    	do 
    	{
    		PLDR_DATA_TABLE_ENTRY pLdte = CONTAINING_RECORD(pLdteCur, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
    		DbgPrint("DllBase: %p, SizeOfImage: %08X %wZ\n", pLdteCur->DllBase, pLdteCur->SizeOfImage, &(pLdteCur->FullDllName));
    		pLdteCur = (PLDR_DATA_TABLE_ENTRY)pLdteCur->InLoadOrderLinks.Flink;
    	} while (pLdteHead != pLdteCur);
    	
    	driver->DriverUnload = DriverUnload;
    	return STATUS_SUCCESS;
    }
    
    // 卸载函数
    VOID DriverUnload(PDRIVER_OBJECT driver)
    {
    	DbgPrint("驱动卸载成功\n");
    }
    

    注意,windbg 无法打印带中文的 UNICODE_STRING,建议把项目名取成全英文。
    还有一点,这里打印的内核模块名是写死的 ntoskrnl.exe,即使系统当前是 2-9-9-12分页,这是XP的BUG。
    https://bbs.pediy.com/thread-97717.htm

    在这里插入图片描述

    五、定位未导出函数 PspTerminateProcess

    课上老师和同学给出了几种办法,接下来分别介绍。比如,我们想找 PspTerminateProcess 这个函数,这是一个未导出函数,用来杀进程的。

    1.windbg+pdb

    在有内核PDB的情况下,用windbg可以直接找到该函数:

    kd> u PspTerminateProcess l40
    nt!PspTerminateProcess:
    8062f050 8bff            mov     edi,edi
    8062f052 55              push    ebp
    8062f053 8bec            mov     ebp,esp
    8062f055 56              push    esi
    8062f056 64a124010000    mov     eax,dword ptr fs:[00000124h]
    8062f05c 8b7508          mov     esi,dword ptr [ebp+8]
    8062f05f 3b7044          cmp     esi,dword ptr [eax+44h]
    8062f062 7507            jne     nt!PspTerminateProcess+0x1b (8062f06b)
    8062f064 b80d0000c0      mov     eax,0C000000Dh
    8062f069 eb5a            jmp     nt!PspTerminateProcess+0x75 (8062f0c5)
    8062f06b 57              push    edi
    8062f06c 8dbe48020000    lea     edi,[esi+248h]
    8062f072 f6470120        test    byte ptr [edi+1],20h
    8062f076 7412            je      nt!PspTerminateProcess+0x3a (8062f08a)
    8062f078 8d8674010000    lea     eax,[esi+174h]
    8062f07e 50              push    eax
    8062f07f 56              push    esi
    8062f080 68caf06280      push    offset nt!NtTerminateProcess+0x14c (8062f0ca)
    8062f085 e800feffff      call    nt!PspCatchCriticalBreak (8062ee8a)
    8062f08a 6a08            push    8
    8062f08c 58              pop     eax
    8062f08d f00907          lock or dword ptr [edi],eax
    8062f090 6a00            push    0
    8062f092 56              push    esi
    8062f093 e854faf4ff      call    nt!PsGetNextProcessThread (8057eaec)
    8062f098 8bf8            mov     edi,eax
    8062f09a 85ff            test    edi,edi
    8062f09c 741e            je      nt!PspTerminateProcess+0x6c (8062f0bc)
    8062f09e ff750c          push    dword ptr [ebp+0Ch]
    8062f0a1 57              push    edi
    8062f0a2 e824d3f4ff      call    nt!PspTerminateThreadByPointer (8057c3cb)
    8062f0a7 57              push    edi
    8062f0a8 56              push    esi
    8062f0a9 e83efaf4ff      call    nt!PsGetNextProcessThread (8057eaec)
    8062f0ae 8bf8            mov     edi,eax
    8062f0b0 85ff            test    edi,edi
    8062f0b2 75ea            jne     nt!PspTerminateProcess+0x4e (8062f09e)
    8062f0b4 3986bc000000    cmp     dword ptr [esi+0BCh],eax
    8062f0ba 7406            je      nt!PspTerminateProcess+0x72 (8062f0c2)
    8062f0bc 56              push    esi
    8062f0bd e882c1ffff      call    nt!ObClearProcessHandleTable (8062b244)
    8062f0c2 33c0            xor     eax,eax
    8062f0c4 5f              pop     edi
    8062f0c5 5e              pop     esi
    8062f0c6 5d              pop     ebp
    8062f0c7 c20800          ret     8
    
    

    8062f050 就是函数头,然而这个值由于重定位,可能会变的,所以我们就要用其他办法,确保每次都能找到这个函数。

    2.通过已导出函数

    第二种办法是根据已导出函数找未导出函数,在驱动里找已导出函数使用的函数是 MmGetSystemRoutineAddress 。我们通过IDA交叉引用,并没有找到调用 PspTerminateProcess 的导出函数。

    在这里插入图片描述

    所以,这种办法在这里无法使用。

    3.模块基址+偏移

    虽然模块基址会变,但是函数相对基址的偏移是不变的,通过这个规律也可以找到想要的函数。
    PspTerminateProcess 相对内核基址的偏移 = 8062f050 - 804D8000 = 157050
    只要找到内核基址,加上 0x157050 就是 PspTerminateProcess 的地址。

    这种方法我就不贴代码了,因为原理比较简单。

    4.特征码匹配(最常用)

    特征码提取时,要避免使用全局变量等和重定位有关的指令,也要避免提取这种所有函数都有的指令。

    8062f050 8bff            mov     edi,edi
    8062f052 55              push    ebp
    8062f053 8bec            mov     ebp,esp
    

    看看函数头部的汇编:

    kd> u PspTerminateProcess l10
    nt!PspTerminateProcess:
    8062f050 8bff            mov     edi,edi
    8062f052 55              push    ebp
    8062f053 8bec            mov     ebp,esp
    8062f055 56              push    esi
    8062f056 64a124010000    mov     eax,dword ptr fs:[00000124h]
    8062f05c 8b7508          mov     esi,dword ptr [ebp+8]
    8062f05f 3b7044          cmp     esi,dword ptr [eax+44h]
    8062f062 7507            jne     nt!PspTerminateProcess+0x1b (8062f06b)
    8062f064 b80d0000c0      mov     eax,0C000000Dh
    8062f069 eb5a            jmp     nt!PspTerminateProcess+0x75 (8062f0c5)
    8062f06b 57              push    edi
    8062f06c 8dbe48020000    lea     edi,[esi+248h]
    8062f072 f6470120        test    byte ptr [edi+1],20h
    8062f076 7412            je      nt!PspTerminateProcess+0x3a (8062f08a)
    8062f078 8d8674010000    lea     eax,[esi+174h]
    8062f07e 50              push    eax
    

    选取这部分作为特征码:

    8062f056 64a124010000    mov     eax,dword ptr fs:[00000124h]
    8062f05c 8b7508          mov     esi,dword ptr [ebp+8]
    8062f05f 3b7044          cmp     esi,dword ptr [eax+44h]
    8062f062 7507            jne     nt!PspTerminateProcess+0x1b (8062f06b)
    8062f064 b80d0000c0      mov     eax,0C000000Dh
    8062f069 eb5a            jmp     nt!PspTerminateProcess+0x75 (8062f0c5)
    8062f06b 57              push    edi
    8062f06c 8dbe48020000    lea     edi,[esi+248h]
    8062f072 f6470120        test    byte ptr [edi+1],20h
    8062f076 7412            je      nt!PspTerminateProcess+0x3a (8062f08a)
    8062f078 8d8674010000    lea     eax,[esi+174h]
    

    用dd打印一下:

    在这里插入图片描述

    接下来编程只需要找这段作为特征码匹配即可。代码如下:

    #include <ntddk.h>
    
    typedef struct _LDR_DATA_TABLE_ENTRY
    {
    	LIST_ENTRY InLoadOrderLinks;
    	LIST_ENTRY InMemoryOrderLinks;
    	LIST_ENTRY InInitializationOrderLinks;
    	PVOID DllBase;
    	PVOID EntryPoint;
    	UINT32 SizeOfImage;
    	UNICODE_STRING FullDllName;
    	UNICODE_STRING BaseDllName;
    	UINT32 Flags;
    	UINT16 LoadCount;
    	UINT16 TlsIndex;
    	LIST_ENTRY HashLinks;
    	PVOID SectionPointer;
    	UINT32 CheckSum;
    	UINT32 TimeDateStamp;
    	PVOID LoadedImports;
    	PVOID EntryPointActivationContext;
    	PVOID PatchInformation;
    } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
    
    // 函数声明
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path);
    VOID GetKernelBase(PDRIVER_OBJECT driver, PVOID *pKrnlBase, PUINT32 uKrnlImageSize);
    PVOID MemorySearch(PVOID bytecode, UINT32 bytecodeLen, PVOID pBeginAddress, PVOID pEndAddress);
    VOID DriverUnload(PDRIVER_OBJECT driver);
    typedef NTSTATUS (*_PspTerminateProcess)(PEPROCESS pEprocess, NTSTATUS ExitCode);
    _PspTerminateProcess PspTerminateProcess;
    
    // 入口函数
    NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
    {
    	UINT32 bytecode[] = {
    		0x0124a164, 0x758b0000, 0x44703b08, 0x0db80775,
    		0xebc00000, 0xbe8d575a, 0x00000248, 0x200147f6,
    		0x868d1274, 0x00000174
    	};
    	PVOID pKrnlBase; // 内核基址
    	UINT32 uKrnlImageSize; // 内核大小
    	PEPROCESS pEprocess; // 要关闭的进程的EPROCESS
    
    	// 获取内核模块基址和大小
    	GetKernelBase(driver, &pKrnlBase, &uKrnlImageSize);
    	DbgPrint("内核基址: %p,大小: %X\n", pKrnlBase, uKrnlImageSize);
    	// 获取 PspTerminateProcess 函数地址
    	PspTerminateProcess = (_PspTerminateProcess)((UINT32)MemorySearch( \
    		bytecode,sizeof(bytecode),pKrnlBase,(PVOID)((UINT32)pKrnlBase+uKrnlImageSize)) - 6);
    	DbgPrint("PspTerminateProcess: %p\n", PspTerminateProcess);
    	// 根据PID获取EPROCESS
    	PsLookupProcessByProcessId((HANDLE)1796,&pEprocess); // 记事本PID是1796
    	// 调用 PspTerminateProcess 关闭进程
    	PspTerminateProcess(pEprocess, 0);
    	DbgPrint("记事本进程被 PspTerminateProcess 函数关闭了.\n");
    	driver->DriverUnload = DriverUnload;
    	return STATUS_SUCCESS;
    }
    
    // 获取内核基址,大小
    VOID GetKernelBase(PDRIVER_OBJECT driver, PVOID *pKrnlBase, PUINT32 uKrnlImageSize)
    {
    	PLDR_DATA_TABLE_ENTRY pLdteHead; // 内核模块链表头
    	PLDR_DATA_TABLE_ENTRY pLdteCur; // 遍历指针
    	UNICODE_STRING usKrnlBaseDllName; // 内核模块名
    
    	RtlInitUnicodeString(&usKrnlBaseDllName,L"ntoskrnl.exe");
    	pLdteHead = (PLDR_DATA_TABLE_ENTRY)driver->DriverSection;
    	pLdteCur = pLdteHead;
    	do 
    	{
    		PLDR_DATA_TABLE_ENTRY pLdte = CONTAINING_RECORD(pLdteCur, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
    		//DbgPrint("DllBase: %p, SizeOfImage: %08X %wZ\n", pLdteCur->DllBase, pLdteCur->SizeOfImage, &(pLdteCur->FullDllName));
    		if (RtlCompareUnicodeString(&pLdteCur->BaseDllName, &usKrnlBaseDllName, TRUE) == 0)
    		{
    			*pKrnlBase = pLdteCur->DllBase;
    			*uKrnlImageSize = pLdteCur->SizeOfImage;
    			return;
    		}
    		pLdteCur = (PLDR_DATA_TABLE_ENTRY)pLdteCur->InLoadOrderLinks.Flink;
    	} while (pLdteHead != pLdteCur);
    	return;
    }
    
    // 特征码搜索
    PVOID MemorySearch(PVOID bytecode, UINT32 bytecodeLen, PVOID pBeginAddress, PVOID pEndAddress)
    {
    	PVOID pCur = pBeginAddress;
    	while (pCur != pEndAddress)
    	{
    		if (RtlCompareMemory(bytecode,pCur,bytecodeLen) == bytecodeLen)
    		{
    			return pCur;
    		}
    		((UINT32)pCur)++;
    	}
    	return 0;
    }
    
    // 卸载驱动
    VOID DriverUnload(PDRIVER_OBJECT driver)
    {
    	DbgPrint("驱动卸载成功\n");
    }
    

    驱动运行前:

    在这里插入图片描述

    驱动运行后:

    在这里插入图片描述

    展开全文
  • Linux内核模块基础--内核模块

    千次阅读 2020-03-24 00:58:36
    1. 内核简单模块的编写 通过命令date可以获取当前系统时间,如下面示例。 下面我们通过编写一个简单的内核模块直接获取当前系统时间。 1.1模块源码编写 在Linux内核源码中,定义了一个struct timeval结构体,...

    1. 内核简单模块的编写

    通过命令date可以获取当前系统时间,如下面示例。

    下面我们通过编写一个简单的内核模块直接获取当前系统时间。

    1.1模块源码编写

    在Linux内核源码中,定义了一个struct timeval结构体,结构体中有两个成员变量tv_sec,tv_usec,分别保存当前系统时间的秒和毫秒,time_t和suseconds_t类型变量在x86架构中,均为long型,变量类型定义在文件include/linux/time.h中。

    00018: struct timeval {

    00019time_t tv_sec/ * seconds */

    00020suseconds_t tv_usec; / * microseconds */

    00021: };

    00022:

     

    模块源码如下:

    00001:

    00002: #include <linux/ module.h>

    00003: #include <linux/ time.h>

    00004:

    00005: static char modname[] = "time";

    00006:

    00007: extern struct timespec xtime;

    00008:

    00009: int init_module( void )

    00010: {

    00011struct timeval tv;

    00012printk( "Installing %s module.", modname );

    00013do_gettimeofday(&tv);

    00014printk("\njiffies:%lu, tv.tv_sec:%lu, tv.tv_nsec:%lu ", jiffies, tv.tv_sec, tv.tv_usec);

    00015:

    00016return 0;

    00017: }

    00018:

    00019:

    00020: void cleanup_module( void )

    00021: {

    00022printk( "\nRemoving %s module.", modname );

    00023: }

    00024:

    00025: MODULE_LICENSE("GPL");

    00026:

     

    1.2Makefile

    创建一个Makefile,执行make,即可编译生成内核模块,生成后缀名为.ko的文件。

    Makefile内容:

    注意:在default:后面的$(MAKE) … … 和rm –r … …两行前面必须是Tab键,不能为空格或其他字符,否则执行make时,会报告“Makefile:10: *** missing separator. Stop.”错误。

    1.3模块加载

    执行make,编译生成模块.ko文件后,就可以通过insmod命令来加载模块。

    通过lsmod命令可以查看驱动是否成功加载到内核中。

    通过insmod命令加载刚编译成功的time.ko模块后,似乎系统没有反应,也没看到打印信息。而事实上,内核模块的打印信息一般不会打印在终端上。驱动的打印都在内核日志中,我们可以使用dmesg命令查看内核日志信息。

    内核模块time.ko获取到的当前系统时间为1289489871秒,与执行date ‘+%s’命令获取到的值一致。

    2内核模块版本与符号表

    在编写和使用内核模块过程中,会发现在某个内核版本上编译的模块只能在当前内核版本中使用。若模块版本号与当前内核版本号不匹配导致就会无法加载,提示“insmod: error inserting 'time.ko': -1 Invalid module format”,内核会打印类似信息“time: version magic '2.6.32.12-0.7-default SMP mod_unload modversions ' should be '2.6.18-92.el5 SMP mod_unload gcc-4.1'”。

    2.1内核模块版本号

    查看内核模块版本信息的命令为modinfo,如查看刚才我们编写的time.ko。

    模块的版本号在“vermagic“一项,当前系统中使用的模块版本号都是相同的。

    模块版本号是哪里决定的?我们是否可以更改?我们是否可以在当前系统中,编译其他内核版本的模块?

    2.1.1模块版本号的确定

    在make编译模块时,通过-C参数制定内核源码头文件位置。前面我们编译time模块内核源码头文件位置为/lib/modules/2.6.32.12-0.7-default/build。

    make -C /lib/modules/2.6.32.12-0.7-default/build SUBDIRS=/root/programming modules

    我们来分析模块版本号的确定

    vermagic: 2.6.32.12-0.7-default SMP mod_unload modversions

    在RHEL5系统中,模块版本号vermagic由include/linux/vermagic.h和include/linux/utsrelease.h两个文件的内容来决定,即vermagic就为VERMAGIC_STRING

    文件include/linux/utsrelease.h的内容如下:

    文件include/linux/vermagic.h的内容如下:

    而在SLES11.1内核2.6.32.12-0.7-default的模块版本号VERMAGIC_STRING由scripts/mod/modpost可执行文件确定。

    2.1.2模块版本号的修改

    前面分析了模块版本号由VERMAGIC_STRING确定产生,若我们需要修改模块版本号或希望在当前内核版本中编译其他内核的模块(注意gcc大版本和CPU架构i686/x86_64保持一致),只需要修改控制模块的VERMAGIC_STRING值即可。

    如我们将示例中的time模块在RHEL5.2内核中编译RHEL5.3内核模块,然后在RHEL5.3系统上可以加载。

    2.1.3编译非当前内核版本模块

    在模块版本号的修改介绍的方法中,仅适合内核版本(OS发行版本)差别不大的情况下,可以方便修改某块版本。本小节介绍如何编译非当前内核版本模块。

    步骤:

    1. 将待编译特定内核源码开发包拷贝到当前系统中某个目录下

    如我们打算在SLES11.1 x86_64系统中编译RHEL5.5 x86_64内核模块,应先将RHEL5.5 x86_64内核开发包拷贝到SLES11.1系统中。

    1. 修改Makefile,将KDIR指向待编译内核开发包目录

    在Makefile中,设置KDIR变量为指定内核源码目录位置。

    3、执行make编译模块

    执行后,生成内核模块。可以使用modinfo命令来查看新生成模块的版本号。如

    #modinfo /root/programming/time.ko

     

    编译非当前内核版本模块后,加载再次提示“Invalid module format”时,通过dmesg命令查看加载失败原因。

    如上面的提示,我们直接include/linux/vermagic.h文件即可,将gcc版本信息值改为固定值gcc-4.1即可。

    2.2内核符号表及使用

    加载模块时,insmod使用公共内核符号表解析模块中未定义的符号。公共符号表中包含了所有的全局内核项(即函数和变量)的地址,内核符号表的内容全部在文件/proc/kallsyms中,可以通过cat等命令查看。内核和模块将函数、变量导出后,就成为内核符号表的一部分。

    在我们编写的内核模块中,可以使用内核或其他模块定义的函数和变量,如本章示例的获取时间模块中,就调用了内核函数do_gettimeofday()。

     

    内核中有两个宏用来导出函数和变量:

    EXPORT_SYMBOL(symbolname)

    将函数或变量导出到所有模块

    EXPORT_SYMBOL_GPL(symbolname)

    将函数或变量仅导出到GPL模块

    我们也可以在自己的模块中导出部分函数或变量,这样其他模块就可以访问这部分函数、变量。C语言用户态程序编程中,我们常会使用在其他C文件或lib库中定位的函数和变量,内核符号表和这有相似之处。

    系统中所有内核和模块导出的变量和函数,就成了内核符号表,在/proc/kallsyms文件中。

    内核符号表中,第一列为函数或变量的在内核中的地址,第二列为符号的类型,第三列为符号名,第四列为符号所属的模块。若第四列为空,则表示该符号属于内核代码。

    内核符号属性

    符号属性

    含义

    b

    符号在未初始化数据区(BSS)

    c

    普通符号,是未初始化区域

    d

    符号在初始化数据区

    g

    符号针对小object,在初始化数据区

    i

    非直接引用其他符号的符号

    n

    调试符号

    r

    符号在只读数据区

    s

    符号针对小object,在未初始化数据区

    t

    符号在代码段

    u

    符号未定义

     

    若符号在内核中是全局性的,则属性为大写字母,如T、U等。其他符号属性含义,请参考命令nm的帮助信息。

    00273: / * Only label it "global" if it is exported. */

    00274: static void upcase_if_global(struct kallsym_iter *iter)

    00275: {

    00276if (is_exported(iter- >name, iter- >owner))

    00277iter- >type += 'A' - 'a';

    00278: }

    00279:

     

    若打算使用内核中的符号,在模块中增加函数或变量说明即可。如:

    00091: extern struct timespec xtime;

     

    3模块版本控制

    Linux内核版本在不变升级,内核提供的API或符号可能也随之变化。这对内核模块开发来说,是一个比较麻烦的问题,通常要适应不同的内核版本,或者只针对具体某些内核版本开发。

    内核为了确保模块的函数接口与内核借口一致,采用了模块版本控制。版本控制最简单的办法就是为了内核和模块都设置一个常量,该常量会随着接口变化而不断增加。加载模块时,内核会检查模块提供的常量是否和内核版本常量相等,若不相等则拒绝加载。

    采用常量的办法进行版本控制,方法简单,但不够灵活。如内核部分接口变化后,版本常量就会增加。但若某模块使用的这些接口并没有变化,也会导致驱动无法加载。基于这个原因,最恰当的方法是将单个内核API的变化考虑进去。实际的模块和内核实现无关,模块和内核关系密切的是API接口。

    3.1checksum方法

    CRC checksum原理是使用函数的参数来计算校验码,若校验码不相等,加载模块失败。

    我们来看一下内核执行模块加载的函数load_module()(文件kernel/module.c)。1767行会调用check_modstruct_version()函数来检查struct_module符号的CRC校验码。若校验码不相等,则提示“disagrees about version of symbol struct_module”。如

    hwinc_kernel_driver: disagrees about version of symbol struct_module

    Found checksum B6AF205C vs module F3D5F8AF

     

    01600: static struct module *load_module(void __user *umod,

    01601unsigned long len,

    01602const char __user *uargs)

    01603: {

    01604Elf_Ehdr *hdr;

    01605Elf_Shdr *sechdrs;

    … …

    01766/ * Check module struct version now, before we try to use module. */

    01767if (! check_modstruct_version(sechdrs, versindex, mod)) {

    01768err = - ENOEXEC;

    01769goto free_hdr;

    01770}

    01771:

    01772modmagic = get_modinfo(sechdrs, infoindex, "vermagic");

    01773/ * This is allowed: modprobe - - force will invalidate it. */

    01774if (! modmagic) {

    01775add_taint_module(mod,TAINT_FORCED_MODULE);

    01776printk(KERN_WARNING "%s: no version magic, tainting

    kernel.\n",

    01777mod- >name);

    01778else if (! same_magic(modmagic, vermagic )) {

    01779printk(KERN_ERR "%s: version magic '%s' should be '%s'\n",

    01780mod- >name, modmagic, vermagic );

    01781err = - ENOEXEC;

    01782goto free_hdr;

    01783}

     

    在编译内核模块时,会生成*.mod.c文件,该文件中包含了模块中各个符号的校验码。校验码的生成,由scripts/genksyms/genksyms计算生成。

    注意:scripts/genksyms/genksyms文件是在内核源码目录或内核开发包目录中,如/usr/src/linux-2.6.32.12-0.7-obj/x86_64/default/scripts/genksyms/genksyms

    3.2vermagic

    查看内核版本模块信息时,会看到vermagic一项。模块在装载时,load_module()函数会比较(如前面代码的1772行)当前运行内核的vermagic和当前要加载的模块的vermagic比较,如果不同,则禁止加载模块。

    Vermagic的的确定请参考章节2.1.1。

    3.3内核模块版本控制使能与关闭

    我们常遇到内核提示“disagrees about version of symbol struct_module”,而导致模块无法加载的情况。

    3.3.1内核中关闭/使能

    若选择关闭内核的模块版本控制功能,则会避免出现这种情况。模块版本控制选项在内核源码配置文件.config中,注释掉CONFIG_MODVERSIONS就取消了模块版本控制。

    CONFIG_MODVERSIONS=y

    重新编译内核,重启即可。

    3.3.2模块中关闭/使能版本控制

    如下面的实例。虽然模块的vermagic和内核一致,但struct_module的版本号不一致。我们可以不修改当前内核,重新编译模块即可解决问题。

    若去掉模块版本控制后,加载驱动导致系统死机。建议解决办法:使用待运行内核的.config配置文件覆盖模块编译指向的内核开发包(源码).config文件。

    .config配置文件的获取:(1)可以拷贝/proc/config.gz,然后解压缩,拷贝为.config;(2)若/proc/config.gz不存在,可以使用/boot/目录下对应的内核配置文件;(3)或向内核提供者获取.config配置文件。

    此时我们可以修改内核开发包中的模块版本控制选项,修改文件.config(在内核源码或开发包根目录下),注释掉或删除CONFIG_MODVERSIONS选项,重新编译模块即可去除模块的版本控制。

    CONFIG_MODULES=y

    CONFIG_OBSOLETE_MODPARM=y

    #CONFIG_MODVERSIONS=y

    CONFIG_MODULE_SIG=y

    4内核模块参数

    在用户执行系统命令或其他程序时,可以使用参数。内核模块也可以使用参数。

    参数必须使用宏module_param()声明,该宏定义在include/linux/moduleparam.h文件中。module_param()需要三个参数:参数名称、类型、sysfs文件系统入口项的访问权限掩码。

    模块参数的定义必须放在任何函数之外。如本章获取系统时间的模块示例,我们增加province和population两个参数(参数仅作示范,和系统时间无任何关系)。

    00001: #include <linux/ module.h>

    00002: #include <linux/ time.h>

    00003: #include <linux/ moduleparam.h>

    00004:

    00005: static char modname[] = "time";

    00006:

    00007: static char *province = "Guangdong";

    00008: module_param(provincecharp0);

    00009: static int population = 10000;

    00010: module_param(populationint0);

    00011:

    00012: int init_module( void )

    00013: {

    00014struct timeval tv;

    00015printk( "Installing %s module.", modname );

    00016do_gettimeofday(&tv);

    00017printk("\njiffies:%lu, tv.tv_sec:%lu, tv.tv_nsec:%lu ",

    00018jiffies, tv.tv_sec, tv.tv_usec);

    00019:

    00020printk("\nProvince:%s, Population:%d \n", province , population );

    00021:

    00022return 0;

    00023: }

    00024:

    00025:

    00026: void cleanup_module( void )

    00027: {

    00028printk( "\nRemoving %s module.", modname );

    00029: }

    00030:

    00031: MODULE_LICENSE("GPL");

    00032:

     

    加载模块time后,内核打印信息:

    内核模块支持的参数类型如下:

    bool

    invbool

    charp:字符串指针。内核会为用户提供的字符串自动分配内存。

    int

    long

    short

    uint:unsigned int

    ulong:unsigned long

    ushort:unsigned short

    5模块入口/出口函数及其他

    每个内核模块都要有初始化(入口)函数和清除(出口)函数,清除函数负责在模块被移除前注销接口并向系统返回所有资源。

    在本章time模块的示例中,并没有像用户态C程序一样有main()入口函数。time模块入口函数为init_module(),而出口函数为cleanup_module()。

    在复杂的模块中,我们可以指定模块的入口/出口函数名称。通过module_init()和module_exit()函数分别指定。如LSISAS1068E驱动mptsas中的入口/出口函数:

    04828: module_init(mptsas_init);

    04829: module_exit(mptsas_exit);

    在模块中,我们还可以添加作者信息、模块描述、模块版本等信息。

    MODULE_AUTHOR():模块作者信息

    MODULE_DESCRIPTION():模块描述

    MODULE_LICENSE():模块协议

    MODULE_VERSION():模块版本

    如:

    00070: #define my_NAME "Fusion MPT SCSI Host driver"

    00071: #define my_VERSION MPT_LINUX_VERSION_COMMON

    00072: #define MYNAM "mptscsih"

    00073:

    00074: MODULE_AUTHOR(MODULEAUTHOR);

    00075: MODULE_DESCRIPTION(my_NAME);

    00076: MODULE_LICENSE("GPL");

    00077: MODULE_VERSION(my_VERSION);

     

    6内核模块与用户程序区别

    6.1用户空间与内核空间

    内核空间具有最高权限,可以访问所有CPU寄存器和其他所有资源。

    • 内核空间可以访问所有的CPU指令和所有的内存空间、I/O空间。

    • 用户空间只能访问有限的资源,若需要特殊权限,可以通过系统调用获取相应的资源。

    • 用户空间允许页面中断,而内核空间则不允许。

    • 用户空间是0-3G的地址范围,内核空间是3G-4G的地址范围。

    • 内核空间和用户空间是针对线性地址空间的。

    • 所有内核进(线)程共用一个地址空间,而用户进程都有各自的地址空间。

    ​​​​​​​                                                               Linux 32位系统用户空间与内核空间

    6.2内核模块与应用程序的对比

    • 内核模块具有独立的地址空间

    模块运行在内核空间中。应用程序运行在用户空间中。系统软件受到保护,不允许用户程序访问。内核空间和用户空间有各自独立的内存地址空间。

    • 内核模块具有更高的执行特权

    运行在内核空间中的代码要比运行在用户空间中的代码具有更大的特权。

    • 内核模块不按顺序执行

    用户程序通常按顺序执行并且从头到尾地执行单独的任务。内核模块并不按顺序执行,它注册自己是为了服务将来的请求。

    • 内核模块可以被中断

    在同一时刻,可能有许多进程同时向驱动程序发出请求。中断程序可以在驱动程序正在响应系统调用时,向驱动程序发出请求。在对称多处理器(SMP)系统中,驱动程序可能在多个 CPU 上并发地执行。

    • 内核模块必须是可抢占的

    • 内核模块能够共享数据

    一个应用程序的不同线程常常不会共享数据。与之相对应的是,组成驱动程序的数据结构和例程被所有使用驱动程序的线程所共享。驱动程序必须能够处理由多个请求导致的竞争问题。

    • 错误处理

    应用程序的错误导致Segmentation Fault,而内核模块的错误影响整个系统,甚至使内核

    7常见问题处理

    1、头文件引用

    在用户程序和内核模块时,可能都会使用头文件 #include <linux/time.h>,但两者文件所在的位置是不同的。

    用户态用户程序使用的time.h,在gcc库文件中,一般位置是/usr/include/linux/time.h或/usr/include/sys/time.h。

    内核模块使用的time.h,在内核源码头文件中,一般位置是<内核版本>/include/linux/time.h,如/usr/src/kernels/2.6.18-128.el5-x86_64/include/linux/time.h。

     

    2、提示内核build目录不存在

    在编写好内核模块后,执行make时,有的系统会提示类似“make: *** /lib/modules/2.6.18-128.el5xen/build: No such file or directory. Stop.”错误信息。原因在于内核源码开发包没有安装。

    解决办法:安装当前内核版本的源码开发包。

    3、模块加载提示Invalid Module Format”

    解决步骤:

    1. 执行dmesg命令,查看模块提示Invalid Module Format的详细原因

    2. 根据提示信息,结合本章提到内核模块版本号与修改一些,修复相应的错误。

    4、模块加载提示“Symbol not found”

    解决步骤:

    1. 执行dmesg命令,查看模块哪些符号在当前系统中不存在。

    2. 执行modinfo命令,查看当前模块依赖关系,并检查依赖的模块是否已加载到系统中。

    4、模块加载提示“disagrees about version of symbol struct_module

    请参考“模块版本控制”一节。

    5、是否有办法将模块加载到非当前内核版本中,而不重新编译模块?

    在内核版本相近和CPU架构相同的情况下,如2.6.18-92.e15 i686和2.6.18-194.e15 i686内核,可以直接二进制编辑模块,修改模块的版本信息,这样就可以加载到非当前内核版本中了。

     

    展开全文
  • Linux内核模块详解

    万次阅读 多人点赞 2019-08-26 09:22:36
    内核模块 实验目的 内核模块是Linux操作系统中一个比较独特的机制。通过这一章学习,希望能够理解Linux提出内核模块这个机制的意义;理解并掌握Linux实现内核模块机制的基本技术路线;运用Linux提供的工具和命令,...
  • 内核模块的加载

    千次阅读 2015-04-27 17:49:46
    一般linux中有两个程序可以添加内核模块,modprobe和insmod,前者考虑到了各个模块之间可能出现的依赖关系,被依赖的模块会被自动载入,而insmod只是简单的尝试载入当前的模块。二者最终都是通过linux系统调用sys_...
  • linux内核模块的参数传递

    千次阅读 2019-06-26 22:33:02
    模块安装时候可以给模块中的变量传递数值,安装后,模块中变量的值就是安装时所传入的值 ,没有传递的变量,则使用代码中默认值。 带参数的模块安装后会生成目录: /sys/module/模块名/parameters/ 示例:如果安装 ...
  • 目录(一)模块化编程简介(二)安装卸载模块命令.(三)将自定义功能添加到内核三种方法(1)修改Kconfig和Makefile(2)直接修改功能对应目录下的Makefile文件(3)在内核目录中,将功能编译成模块文件,后期加载...
  • linux编写一个简单的内核模块

    千次阅读 2019-12-13 07:40:43
    编写一个简单的内核模块 (一)实验目的 Linux 操作系统的内核是单一体系结构(monolithic kernel)的,也就是说,整个内核是一个单独的非常大的程序。这样,系统的速度和性能都很好,但是可扩展性和维护性就相对比较...
  • linux虚拟内存和物理内存 虚拟地址空间Linux整体架构图Linux虚拟内存系统内存管理分页式内存管理分段式内存管理段页式内存管理 虚拟地址空间 参考: ... 地址空间:非负整数地址的有序集合,如{0,1,2,...}\{0,1,2,...\...
  • 我这里有个疑惑,如果是 insmod 命令修改内核模块的 refcnt 值,可能有两种方式: 通过其它系统调用完成 通过操作 sys 下的文件完成 第一种方式可以通过 strace insmod 命令查看,没有发现可疑的其它系统调用,...
  • VAD 虚拟内存

    千次阅读 2022-04-20 18:29:40
    } 首先找到程序VAD一开始的状态 申请一块内存之后 再重新修改属性 可以发现commit变为1,但是属性还是没改 打开CE 看内存 发现还是修改了的 VAD里面的结构为MMVAD 私有内存一般是MMVAD_SHORT 映射内存一般是MMVAD_...
  • 密钥在内核代码中称为key,因为key是由用户态进程创建,由内核管理,其实体存储在内核申请的内存中,所以密钥管理需要实施配额管理。密钥有对称密钥和非对称密钥两大类,每类密钥又有很多种。密钥种类不同,payload...
  • 内存寻址 、硬件中的分段与分页 、Linux内存管理 页与内存管理区 、kmalloc()和vmalloc()
  • sysfs是一种基于内存的虚拟文件系统,该文件系统将内核信息以文件的方式提供给用户程序使用,可以更方便地对系统设备进行管理。sysfs把连接在系统上的设备和总线组织成为一个分级的文件,由用户空间存取,并能向用户...
  • 使用内核模块的方式添加系统调用 ...在内核模块中实现系统调用函数,修改映射在内存中的系统调用表,把一个空闲的系统调用表项指向自己写的模块中的函数。具体步骤如下: a. 找系统调用表在内存中的位置;
  • Linux内核模块简介

    千次阅读 2016-08-27 10:22:12
    Linux内核模块简介
  • 1.4 高端内存和低端内存 1.5 内存模型 2 物理内存的管理 2.1 物理内存的组织:节点和管理区(内存域)简介 2.1.1 简介 2.1.2 相关函数 2.2 伙伴系统 2.2.1 简介 2.2.2 伙伴系统实现原理 2.2.3 /proc/buddyinfo...
  • Linux驱动开发1-内核入门之hello模块

    万次阅读 2018-03-21 19:59:36
    一、Linux内核简介 现在我们从一个比较高的高度来审视一下 GNU/Linux 操作系统的体系结构。如下图所示,最上面是用户(或应用程序)空间,这是用户应用程序执行的地方。用户空间之下是内核空间,Linux 内核正是位于...
  • 深入理解linux内核架构(内存管理)

    千次阅读 2017-02-20 15:43:52
    1:在内核使用高端内存页之前,必须使用下文讨论的kmap和kunmap函数将其映射到内存虚拟地址空间中。 2:UMA计算机(一致内存访问,uniform memory access)将可用内存以连续方式组织起来。 3:NUMA计算机(非一致性...
  • 腾讯一面:内存满了,会发生什么?

    千次阅读 多人点赞 2022-05-24 10:18:01
    作者:小林coding ... 大家好,我是小林。 前几天有位读者留言说,面腾讯时,被问了两个内存管理...第二,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在.
  • 调试linux内核模块

    千次阅读 2012-10-01 14:42:50
     最近几天学习Linux-2.6平台上的设备驱动,所以要建立内核内核模块的调试平台.虽然网上有很多相关教程,但多是基于2.6.26以前的通过补丁安装的,过程非常复杂,而且问题比较多.linux从 2.6.26开始已经集成了kgdb,只...
  • 摘要:本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法。力求从外到内、水到渠成地引导网友分析Linux的内存管理与使用。在本章最后,...
  • Linux内核态和用户态共享内存

    千次阅读 2016-10-24 14:32:49
    Linux内核态和用户态共享内存 1、mmap系统调用(功能)  void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset )  内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存...
  • - 内存管理信息在 task_struct 的 mm_struct 中 - task_size 指定用户态虚拟地址大小 - 32 位系统:3G 用户态, 1G 内核态 - 64 位系统(只利用 48 bit 地址): 128T 用户态; 128T 内核态 - 用户态地址空间布局和管理 -...
  • 而进程,文件,驱动等等部件在内存中是以内核对象的形式独立存在的。而通常形形色色的rootkit会尝试修改操作系统的内部数据结构来达到隐藏自身的目的.安全研究员要想在内存取证中做到游刃有余,就需要对windows各种内存...
  • 内核与宏内核比较

    千次阅读 2021-05-11 01:19:08
    内核与宏内核比较内核体系结构---微内核与宏内核比较内核按照体系结构分为两类:微内核(microkernel)与宏内核(macrokernel). 微内核的系统有WindowNT,Minix,Mach,etc.宏内核的系统有Unix,Linux,etc.通过比较Minix和...
  • Linux内核模块编程-系统调用拦截

    千次阅读 2015-11-16 22:15:48
    系统调用拦截本文实验基于Centos6.5 内核...通过学习系统调用的内核实现我们发现其实系统调用的地址是放在sys_call_table中通过系统调用号定位到具体的系统调用地址,然后开始调用,那么通过编写内核模块修改sys_c
  • 01-第一个内核模块程序

    千次阅读 2019-09-06 15:33:34
    01-第一个内核模块程序代码实现Makefile 代码实现 demo.c #include <linux/module.h> /* 函数名可以任意指定又带来了一个新的问题,那就是有可能和内核中的已有函数重 名,为了避免重名而带来的...
  • 内核里使用内存 内存使用,无非就是申请、复制、设置、释放。在 C 语言里,它们对应的函数是:malloc、memcpy、memset、free;在内核编程里,他们分别对应 ExAllocatePool、RtlMoveMemory、 RtlFillMemory、...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 46,371
精华内容 18,548
热门标签
关键字:

修改内核 内存 属性 模块