0%

Linux:Linux内核级安全开发基础(5)应用程序和内核模块的信息交互方式

Linux内核级安全开发基础

1.5 应用程序和内核模块的信息交互方式

  • 应用程序代码运行在用户态,用户态CPU工作在保护地址模式
  • 内核模块代码运行在系统态,系统态CPU工作在实地址模式

内核模块代码和应用程序代码分别运行在不同的地址空间上,二者不能相互访问和自由地传递数据

内核模块和程序之间的三种数据交换技术

  1. Netlink机制
  2. 创建设备文件
  3. 添加系统调用
  4. 创建proc文件结点
1.Netlink机制

Linux提供一种基于套接字接口的通信机制,即Netlink机制

在内核模块和上层应用程序之间分别建立Netlink协议类型的套接字,经过套接字初始化后,应用程序和内核模块就可以使用这对套接字进行数据传递

优点:

  1. 全双工异步通信,在内核实现Netlink接受队列,即可实现无阻塞的消息通信
  2. 具有“组播”功能,Netlink消息可发送到一个Netlink组地址,所有设定该组地址的进程都能收到该消息
  3. 在内核模块中添加一个Netlink套接字只需进行少量修改

由于这些优点,Netlink机制逐渐成为Linux系统中一种标准的通信机制,例如多种基于Linux开发的相关应用,如路由器、防火墙、IPSec等都采用Netlink机制实现内核和应用程序的通信

Netlink机制中,也有Netlink协议簇的概念,支持范围:031(016:Linux自身或知名应用+17~31供用户定义使用

Netlink机制的使用跟普通的套接字没有本质差别

  1. 创建套接字
  2. 在该套接字上完成数据的发送和接收

只是Netlink套接字通信时消息数据的构造相对复杂

2.创建设备文件

Linux为了屏蔽硬件细节,引入了设备文件这种方式,通过相应的设备驱动将具体设备抽象为设备文件(但网络设备抽象为接口),给应用程序提供一个统一的编程接口

Linux系统中设备的基本类型有:

  1. 字符设备:PC上的串口和键盘
  2. 块设备:硬盘和软盘
  3. 网络设备

Linux系统为每个设备分配了一个主设备号和一个次设备号:

  • 主设备号:标识设备对应的驱动程序
  • 次设备号:标识具体设备的实例

设备文件作为设备访问的入口点,应用程序利用该入口点就能像操作普通文件一样来操作设备

设备驱动程序实质是一组完成不同任务的函数集合(定义在file_operations结构体中,包含常见文件I/O函数的入口地址)

应用程序需要对设备进行操作时:访问设备对应的设备文件结点,内核将调用该设备的相关处理函数,就可以像读写文件一样从设备接收输入和将输出送到设备

实现内核模块和上层应用程序的通信:创建一个虚拟设备,通过驱动中的函数实现内核模块和应用程序之间的数据交互

编写一个字符设备驱动:

  1. 实现file_operations结构体中的需要用到的操作函数,比如write():通过应用程序配置的信息传递给内核模块,以实现相应的资源访问控制

  2. 实现初始化,以在内核中注册该设备

    设备驱动是以一个独立的内核模块形式存在的,在包含设备驱动的内核模块加载时。调用设备初始化函数完成设备注册:将驱动程序的file_operations和主设备号一起向内核进行注册

    字符设备的注册函数:

    1
    2
    3
    4
    5
    int register_chrdev(
    unsigned int major, //申请的主设备号,0:由系统动态分配
    const char name, //设备名
    struct file_operations fops //file_operations结构体
    )

    在包含设备驱动的内核模块卸载时,调用设备的卸载函数完成设备注销

    字符设备的卸载函数:

    1
    2
    3
    4
    5
    int unregister_chrdev(
    unsigned int major, //要注销的设备号
    const char name, //设备名
    struct file_operations fops //file_operations结构体
    )
3.添加系统调用

在使用系统调用提供的各类服务时,应用程序要和Linux内核交互数据,系统调用中的参数就是用来在应用程序和Linux内核间完成数据交互的。

所以,新添加一个带参数的系统调用就可以完成应用程序和内核模块之间的信息传递

实现新的系统调用的步骤:

  1. 为新添加的系统调用预先分配一个系统调用号
  2. 实现相应的系统调用函数
  3. 将该函数的入口地址写到系统调用入口地址表的对应表项中

新添加的系统调用分为:

  1. 静态实现:直接修改Linux操作系统源码

    1. 分配新的系统调用号
    2. 实现新的系统调用函数
    3. 修改系统调用入口地址表的源代码:在系统调用号对应表项写入新函数的入口地址

    因为修改了内核源码,需要对内核源码重新编译

  2. 动态实现:用内核模块的形式进行系统调用的扩展

    1. 分配新的系统调用号
    2. 实现新的系统调用函数
    3. 在模块初始化函数中,将新函数的入口地址写道系统调用表的对应表项

​ 该方法无需重启操作系统,也无需重新编译操作系统源代码