RSS Feed
更好更安全的互联网

从 0 开始学 Linux 驱动开发(一)

2019-01-08

作者:Hcamael@知道创宇404实验室

最近在搞IoT的时候,因为没有设备,模拟跑固件经常会缺/dev/xxx,所以我就开始想,我能不能自己写一个驱动,让固件能跑起来?因此,又给自己挖了一个很大坑,不管最后能不能达到我的初衷,能学到怎么开发Linux驱动,也算是有很大的收获了。

前言

我写的这个系列以实践为主,不怎么谈理论,理论可以自己去看书,我是通过《Linux Device Drivers》这本书学的驱动开发,Github上有这本书中讲解的实例的代码[1]

虽然我不想谈太多理论,但是关于驱动的基本概念还是要有的。Linux系统分为内核态和用户态,只有在内核态才能访问到硬件设备,而驱动可以算是内核态中提供出的API,供用户态的代码访问到硬件设备。

有了基本概念以后,我就产生了一系列的问题,而我就是通过我的这一系列的问题进行学习的驱动开发:

  1. 一切代码的学习都是从Hello World开始的,怎么写一个Hello World的程序?
  2. 驱动是如何在/dev下生成设备文件的?
  3. 驱动怎么访问实际的硬件?
  4. 因为我毕竟是搞安全的,我会在想,怎么获取系统驱动的代码?或者没有代码那能逆向驱动吗?驱动的二进制文件储存在哪?以后有机会可能还可以试试搞驱动安全。

Everything start from Hello World

提供我的Hello World代码[2]

Linux下的驱动是使用C语言进行开发的,但是和我们平常写的C语言也有不同,因为我们平常写的C语言使用的是Libc库,但是驱动是跑在内核中的程序,内核中却不存在libc库,所以要使用内核中的库函数。

比如printk可以类比为libc中的printf,这是在内核中定义的一个输出函数,但是我觉得更像Python里面logger函数,因为printk的输出结果是打印在内核的日志中,可以使用dmesg命令进行查看

驱动代码只有一个入口点和一个出口点,把驱动加载到内核中,会执行module_init函数定义的函数,在上面代码中就是hello_init函数。当驱动从内核被卸载时,会调用module_exit函数定义的函数,在上面代码中就是hello_exit函数。

上面的代码就很清晰了,当加载驱动时,输出Hello World,当卸载驱动时,输出Goodbye World

PS:MODULE_LICENSEMODULE_AUTHOR这两个不是很重要,我又不是专业开发驱动的,所以不用关注这两个

PSS: printk输出的结果要加一个换行,要不然不会刷新缓冲区

编译驱动

驱动需要通过make命令进行编译,Makefile如下所示:

一般情况下,内核的源码都存在与/usr/src/linux-headers-$(shell uname -r)/目录下

比如:

而我们需要的是编译好后的源码的目录,也就是/usr/src/linux-headers-4.4.0-135-generic/

驱动代码的头文件都需要从该目录下进行搜索

M=$(PWD)该参数表示,驱动编译的结果输出在当前目录下

最后通过命令obj-m := hello.o,表示把hello.o编译出hello.ko, 这个ko文件就是内核模块文件

加载驱动到内核

需要使用到的一些系统命令:

  • lsmod: 查看当前已经被加载的内核模块
  • insmod: 加载内核模块,需要root权限
  • rmmod: 移除模块

比如:

旧版的内核就是使用上面这样的方法进行内核的加载与移除,但是新版的Linux内核增加了对模块的验证,当前实际的情况如下:

从安全的角度考虑,现在的内核都是假设模块为不可信的,需要使用可信的证书对模块进行签名,才能加载模块

解决方法用两种:

  1. 进入BIOS,关闭UEFI的Secure Boot
  2. 向内核添加一个自签名证书,然后使用证书对驱动模块进行签名,参考[3]

查看结果

在/dev下增加设备文件

同样先提供一份代码,然后讲解这份实例代码[4]

知识点1 -- 驱动分类

驱动分为3类,字符设备、块设备和网口接口,上面代码举例的是字符设备,其他两种,之后再说。

如上图所示,brw-rw----权限栏,b开头的表示块设备(block),c开头的表示字符设备(char)

知识点2 -- 主次编号

主编号用来区分驱动,一般主编号相同的表示由同一个驱动程序控制。

一个驱动中能创建多个设备,用次编号来区分。

主编号和次编号一起,决定了一个驱动设备。

如上图所示,

设备sdasda1的主编号为8,一个此编号为0一个此编号为1

知识点3 -- 驱动是如何提供API的

在我的概念中,驱动提供的接口是/dev/xxx,在Linux下Everything is File,所以对驱动设备的操作其实就是对文件的操作,所以一个驱动就是用来定义,打开/读/写/......一个/dev/xxx将会发生啥,驱动提供的API也就是一系列的文件操作。

有哪些文件操作?都被定义在内核<linux/fs.h>[5]头文件中,file_operations结构体

上面我举例的代码中:

我声明了一个该结构体,并赋值,除了owner,其他成员的值都为函数指针

之后我在scull_setup_cdev函数中,使用cdev_add向每个驱动设备,注册该文件操作结构体

比如我对该驱动设备执行open操作,则会去执行scull_open函数,相当于hook了系统调用中的open函数

知识点4 -- 在/dev下生成相应的设备

对上面的代码进行编译,得到scull.ko,然后对其进行签名,最后使用insmod加载进内核中

查看是否成功加载:

虽然驱动已经加载成功了,但是并不会在/dev目录下创建设备文件,需要我们手动使用mknod进行设备链接:

总结

在该实例中,并没有涉及到对实际物理设备的操作,只是简单的使用kmalloc在内核空间申请一块内存。代码细节上的就不做具体讲解了,都可以通过查头文件或者用Google搜出来。

再这里分享一个我学习驱动开发的方法,首先看书把基础概念给弄懂,细节到需要用到的时候再去查。

比如,我不需要知道驱动一共能提供有哪些API(也就是file_operations结构都有啥),我只要知道一个概念,驱动提供的API都是一些文件操作,而文件操作,目前我只需要open, close, read, write,其他的等有需求,要用到的时候再去查。

参考

  1. https://github.com/jesstess/ldd4
  2. https://raw.githubusercontent.com/Hcamael/Linux_Driver_Study/master/hello.c
  3. https://jin-yang.github.io/post/kernel-modules.html
  4. https://raw.githubusercontent.com/Hcamael/Linux_Driver_Study/master/scull.c
  5. https://raw.githubusercontent.com/torvalds/linux/master/include/linux/fs.h

Paper

本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/779/

作者:Nanako | Categories:安全研究安全科普 | Tags:

发表评论