信息安全技术解析与开发实践

阅读笔记

上篇 技术解析篇

第1、2章技术对应第8、9章开发实践

第3、4、5章技术对应第10-13章开发实践

第6章技术对应第14、15章开发实践

第7章技术对应第16、17章开发实践

第一章 Linux内核级安全开发基础

  • 应用程序只能通过OS的服务接口——系统调用、API——访问文件、网络数据、设备等系统资源。

  • OS接收到请求时需要先进行访问控制判断。

  • 可以下载并修改Linux源码,以丰富资源访问监视与控制功能;或将其实现为一个单独的Linux内核模块,运行时动态加载即可。

操作系统体系结构概述

OS体系结构——单体式
  • 整个系统就是一堆 procedure 的集合,各个 procedure 可以调用任意其他 procedure,每一个 procedure 都有一个完好的接口(入口参数与返回值)
  • 运行效率高
  • 缺点:
    • 模块间没有清晰的调用关系
    • 系统不容易扩充
    • 系统运行时以一个单独的目标程序存在,其中的冗余模块占用系统资源
OS体系结构——微内核
  • 内核只存放最基本的内核OS功能,其他服务与应用在用户模式下运行
  • 优点:
    • 交互接口一致性
    • 可扩充
    • 可移植:所有与特定 CPU 有关的代码都在内核中

Linux 的动态内核模块机制

动态内核模块机制
  • Linux内核为单体式结构内核,但引入了支持内核模块动态加载的机制,通过内核模块扩展和动态加载实现 OS 功能的扩充
  • 基本内核+内核模块 = Linux 内核
  • 内核模块应当与所运行的内核在接口约定上保持一致
Linux 内核模块的加载和卸载
  • 通过 insmod 命令手工加载或通过请求加载
  • 请求加载的内核模块保存在 /lib/modules/2.6.18(内核版本为2.6.18)
  • 模块中定义的资源被OS以内核输出符号表来同一管理。加载时首先查找该表。若加载成功,模块的输出符号添加到该表
  • 通过 rmmod 命令卸载内核模块,但该内核模块不能被其他模块引用——B模块使用了A模块的输出符号
  • 支持自动卸载:引用的计数为0时才自动卸载。自动卸载由内核进程 kerneld 设置定时器到期完成

Linux内核模块开发方法

源代码组成
  • 动态内核模块包含:
    • 模块初始化:模块注册、数据与变量初始化。模块加载到内核时系统自动调用目标模块的初始化函数
    • 注销:资源释放,以注销函数形式出现
    • 主体功能:一组函数。需要时通过系统调用或其他功能模块调用
  • 内核模块编程无需 main 函数
外部符号引用
  • 应用程序的外部资源是库函数或系统调用,内核模块的外部资源为 Linux 基本内核或其他内核模块中定义的资源(函数、全局变量等)
  • ksyms 工具可列出当前系统所能使用的所有外部符号
编译与运行模式
  • 编译时要告知编译的是内核模块代码——在 gcc 或 cc 选项中添加 D_MODULE 生成适合在系统态运行的内核模块
  • 利用 Linux 内核提供的编译内核模块工程模板,以及 make 工具来编译
调试和信息输出
  • 定义格式化输出的全局函数 printk(),内核模块可调用该函数将进程信息输出到控制台或日志文件
  • 函数在 /linux/printk.h 中声明

Linux系统调用概述

  • OS实现系统调用对应的代码段时,不会也不允许调用C语言函数库

Linux 系统调用的实现

系统调用入口地址表
  • 按编号将每个系统调用的处理函数入口地址保存在一个内存数组中——系统调用入口地址表
  • 数组下标对应系统调用号,元素存储对应入口地址
  • 2.6.18版本中,入口地址表在源码文件 syscall_table.S
中断机制和系统调用实现
  • 根据中断源不同对中断编号,并设计不同的处理函数,并将处理函数入口地址保存在数组中(下标为编号)——中断向量表
  • 系统调用发生时通过执行机器码产生中断信号,即借助系统自陷实现
Linux系统调用的实现过程
  • Linux实现自陷的机器指令:int x80,编译出的目标程序执行到此位置时,CPU 切换到系统态,并执行内核代码。内核代码查找中断向量表
  • 对应中断处理函数为系统调用总入口函数:
    • 保存寄存器现场
    • 从相应寄存器获得系统调用号
    • 由入口地址表获得入口地址
    • 调用处理函数并保存结果到相应寄存器
    • 完成系统调用,并返回结果给用户进程

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

  • 套接字为网络通信常用接口形式
  • 在内核模块和应用程序分别建立 Netlink 协议类型的套接字,初始化后即可通信
  • Netlink 协议簇,分别支持各种形式与用途的通信。支持范围为0-31,0-16支持 Linux 本身或知名应用,其余供用户定义使用
创建设备文件
添加系统调用
  • 新添加一个带参数的系统调用。参数即为应用程序与内核交换的数据
  • 分配系统调用号、实现处理函数、入口地址写入系统调用入口地址表对应位置
  • 实现方式:
    • 静态实现:直接修改 Linux 源码
    • 动态实现:运行过程时加载新的内核模块(过程同上)
  • 2.6版本后内核模块无法直接访问入口地址表

第二章 Linux内核级安全机制实现解析

  • 访问控制功能,安全日志功能
  • 需要Linux内核操作前告知内核模块要产生操作的信息
  • 系统调用重载/基于钩子函数的LSM(Linux Security Modules)机制

安全模块(LSM)机制

基于LSM的Linux内核级安全机制实现

Linux系统调用重载技术

  • 1.4与1.5是基础
概念
  • 修改系统调用入口地址表某一表项,将另一个新函数的入口地址写入该表项
  • 动态与静态两种方式实现
    • 动态:在内核模块初始化的过程中,从入口地址表中找到对应表项,写入新函数的入口地址
    • 静态:直接修改内核源码
系统调用重载实现技术
  • 间接获得入口地址表:

    • 获得中断向量表起始地址:访问 CPU 中的 IDT 寄存器

    • 获得系统调用总入口函数(x80中断处理函数)地址:上述地址+x80*8的偏移量

    • 在总入口函数代码段中查找入口地址表首地址:访问的汇编语句为

      1
      call * sys_call_table(, %eax, 4)

      根据系统调用号调用对应的系统调用处理函数。变量 sys_call_table即为系统调用入口地址表的起始地址,寄存器eax保存了当前要调用的系统调用号,call的指令码为十六进制的FF1485

    • 重载系统调用处理函数入口地址:根据系统调用编号,计算表项位置并写入新的处理函数入口地址

参数传递
  • OS有固定的参数约定,例如读文件:

    1
    int read(int fd, char *buf, int len)
  • 重载时形式参数要在数量、类型、次序上完全一致,形式参数的符号名可以不一致

基于系统调用重载的内核级安全机制实现

概述
  • 实际上是基于系统调用重载技术,来实现包含相应安全机制的内核模块,同样包含三个部分
    • 模块主体函数:Linux 将系统调用的实际参数传入,实现具体的安全机制
    • 模块初始化函数:将设计好的一组处理函数入口地址写入系统调用入口地址表相应位置
    • 模块注销函数:重新修改系统调用入口地址表,将重载后的表项恢复为原有的入口地址
  • 必须先实现原有的系统调用功能,再添加安全机制
  • 三类安全机制:
    • 访问监视类
    • 访问控制类
    • 数据转换类
访问监视类安全机制实现
  • 通过解析传递的参数(重载的系统调用函数),可以知道是做什么操作
  • 通过解析原处理函数(还要调用原处理函数以实现系统服务)的返回值,知道是否被成功执行
  • 以上形成日志
访问控制类安全机制实现
  • 根据参数得知操作,基于请求的上下文,按照既定的安全规则形成访问判决,执行不同措施
  • 要注意原处理函数返回值的含义——这是访问判决的依据
数据转换类安全机制实现
  • 例如文件的加解密
  • 加密在 write 操作之前进行,解密在读取之后的重载函数中进行

基于LSM的文件访问控制实现解析

基于系统调用重载的文化访问日志实现解析

Linux 的日志系统
  • 日志记录系统中发生的操作
  • 实现系统审计和监测功能
  • 三类日志:
    • 系统层日志:Linux内核产生,记录系统异常状况、启动过程、设备变更情况。记录在/var/log/messages
    • 应用层日志:管理工具执行一些操作时调用库函数接口
    • 第三方应用软件也会向 Linux 日志子系统汇报日志
  • Linux 日志子系统只能被动记录应用发来的日志信息
基于系统调用重载的文件访问日志
  • 当应用程序进行任何形式的文件操作时,均会对行为进行记录追踪
  • 还要实现一个文件访问日志管理程序,借助 Netlink 机制从内核的日志缓冲区读出临时存放的日志记录
  • 管理程序预处理:
    • 相似日志合并
    • 不同信息类型的转换:日志信息中的UID(用户标识)转换为用户名,将PID(进程标识符)转换为可执行程序名
      image-20201011170529259

下篇 开发实践篇

第九章 基于系统调用重载的文件访问日志原型实现

原型系统总体设计

  • 此日志原型系统较为简单,只对特定目录下的文件访问操作进行记录,且只记录程序名、用户名、访问方式(读/写)、访问文件名和具体的访问时间
  • 两个部分
    • 日志信息搜集的 Linux 内核模块
    • 日志应用应用程序

内核日志模块的实现(注意 Linux 版本)

外部函数与结构
结构体 task_struct,fs_struct 和变量 current
  • task_struct

    • PCB 对应的结构体

    • 在内核源代码目录下的include\linux\sched.h

      1
      2
      3
      4
      5
      6
      struct task_struct {
      pid_t pid; //进程对用的进程标识符
      uid_t uid, euid, suid, fsuid; //进程对应的用户标识符
      char comn[TASK_COMM_LEN]; //进程对应的可执行文件名,不包含路径
      struct fs_struct * fs; //进程相关的文件系统
      };
  • current

    • 直接访问变量current,查找执行该系统调用进程的 PCB
    • 全局指针变量,指向正在执行进程的 PCB(current->pidcurrent->uid等)
  • fs_struct

    • 进程的文件系统,可用于获得文件绝对路径名

      1
      2
      3
      4
      5
      6
      7
      struct fs_struct {
      atomic_t count;
      rwlock_t lock;
      int umask;
      struct dentry * root, * pwd, * altroot; //pwd指向struct dentry的目录项结构,通过current->fs->pwd获得进程当前目录的目录项结构。具体参见8.3.1
      struct vfsmount * rootmnt, *pwdmnt, * altrootmnt;
      };
结构体 struct sk_buff,struct nlmsghdr,struct iovec
  • 用于将传递的数据构造为 Netlink 数据包
  • sk_buff位于include/linux/skbuff.h,是 OS 实现网络协议的结构,存储报文所有相关信息
    • 成员len:报文数据载荷长度
    • 成员data:报文数据载荷(相对 IP 层次)
外部函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct sock * netlink_kernel_create(int unit, //netlink协议类型号
unsigned in groups, //套接字的组标识,设置为0
void (* input)(struct sock *sk, int len), //回调函数的地址指针
struct module * module //指定使用套接字的范围
); //创建一个Netlink类型的套接字接口,返回一个套接字接口指针

int netlink_unicast(struct sock * ssk, //netlink_kernel_create 返回的套接字
struct sk_buff * skb, //存放要发送的消息
_u32 pid, //接收消息的进程ID
int nonblock //函数是否阻塞,为1则函数在没有接收缓存时立刻返回,为0则没有缓存时睡眠
); //向指定进程发送 Netlink 消息

struct sk_buff *skb_dequeue(struct sk_buff_head *list
); //取得Netlink接收队列上的消息,返回结构中的data指针指向实际的Netlink消息

kmalloc(); //内存分配
memset(); //缓存区初始化
strpcy(); //字符串复制
strncpy(); //字符串比较
头文件、全局变量及声明
头文件与宏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <linux/module.h> //内核开发必须的头文件
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/syscalls.h>
#include <linux/file.h>
#include <linux/fs.h> //文件相关
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/unistd.h>
#include <net/sock.h>
#include <net/netlink.h> //netlink机制必需的头文件

#define TASK_COMM_LEN 16 //进程对应可执行文件名长度
#define Netlink_TEST 29 //用于传递信息的 Netlink 协议号
#define AUDITPATH "/root/TestAudit" //监控目录
#define MAX_LENGTH 256 //文件名最大长度
全局变量
1
2
3
4
void ** sys_call_table; //指向系统调用入口表的地址
amlinkage long (* origin_open)(const char * pathname, int flags, mode_t mode); //保存原来的open系统调用处理函数地址
static u32 pid = 0; //与内核模块进行 netlink 通信的日志应用程序进程标识符
static struct sock * nl_sk = NULL; //通信的套接字
函数声明
1
2
module_init(audit_init); //内核模块初始化  参数为函数名,用于入口重载
module_exit(audit_exit); //内核模块注销 参数为函数名,用于恢复原来的系统调用
结构定义
1
2
3
4
5
6
struct idt_descriptor {
unsigned short off_low; //中断处理函数低16位地址
unsigned short sel;
unsigned char none, flags;
unsigned short off_high; //中断处理函数高16位地址
}; //获得中断处理函数入口地址(0x80号中断,处理系统调用)
函数组成和功能设计
内核模块初始化
  • static int __init audit_init(void)
    • 内核模块初始化函数,在加载到内核中被自动调用
    • 调用get_sys_call_table()获得入口地址表首地址
    • hacked_openat(struct pt_regs *regs)替换原有的入口地址,原有地址保存在全局变量orig_open
    • 调用netlink_init()初始化套接字接口
  • void * get_sys_call_table(void)
    • 调用get_system_call()获得调用函数入口地址
  • void * get_system_call(void)
  • unsigned int clear_and_return_cr0(void)
    • 清除控制寄存器CR0的写保护位
  • void netlink_init(void)
    • 创建基于 Netlink 的 SOCKET 接口
    • 数据接收钩子函数设置为函数nl_data_ready(),并将此套接字接口保存在全局变量nl_sk
内核注销
  • void netlink_release(void)
    • 释放分配的 Netlink 资源
  • static void _exit audit_exit(void)
    • 内核模块卸载时自动调用
    • 将入口地址恢复为原来的系统调用函数入口地址
    • 恢复写保护位
    • 释放接口资源
操作信息收集
  • asmlinkage long hacked_openat(struct pt_regs *regs)
    • 此函数入口地址在初始化时注册到入口地址表,重载open
    • 先调用原来的open,之后调用函数AuditOpen()完成日志信息收集和记录
  • int AuditOpen(const char * pathname, int flags, int ret)
    • 完成日志信息的收集
    • 调用get_fullname()获得打开文件全部路径名
    • 信息封装在一个缓冲区中
    • 调用netlink_sendmsg()将日志内容发送出去
  • void get_fullname(const char * pathname, char * fullname)
    • 获得打开文件全部路径名
    • current->fs->pwd获得当前目录对应的目录项结构
    • 获得目录项对应的路径名(struct dentry
    • 拼合得到路径名
    • 需要注意参数传入的文件名可能包含向上级目录的回退,如 ../TestAudit 形式
  • int netlink_sendmsg(cosnt void * buffer, unsigned int size)
    • 调用netlink_unicast()buffer中的数据发送到创建的 SOCKET 接口中
    • 应用层从接口读出日志数据
函数的实现
  • 参见代码仓库FileAudit

内核应用程序的实现

库函数与结构体
  • void signal(int signo, void (*func)(int))

    • 为信号设置指定的处理函数
    • 第一个参数指定哪一类信号
    • 第二个参数指定具体处理函数
  • struct passwd * getpwuid(uid_t uid)

    • 获得对应用户id的账户信息,返回指向账户信息结构的指针

      1
      2
      3
      4
      5
      6
      7
      8
      9
      struct passwd(){
      char * pw_name; //用户账户名
      char * pw_passwd; //账户口令
      uid_t pw_uid; //用户标识符
      gid_t pw_gid; //用户组标识
      char * pw_gecos; //用户全名
      char * pw_dir; //用户主目录
      char * pw_shell; //用户的shell程序
      };
  • int socket(int domain, int type, int protocol)

    • 创建一个套接字接口
    • 创建 Netlink 套接字接口时:PF_Netlink, SOCK_RAW, ProtocolNum
  • ssize_t sendmsg(int sock, const struct msghdr * msg, int flags)

    • 向指定套接字发送数据

    • 参数二为发送消息头的结构体

    • 参数三为发送的一些控制标志和组合

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      struct msghdr{
      void * msg_name; //消息名称
      int msg_namelen; //消息名称长度
      struct iovec * msg_iov; //指向消息内容的结构体
      _kernel_size_t msg_iovlen; //消息数量
      void * msg_control; //控制消息
      _kernel_size_t msg_controllen; //消息发送的长度
      unsigned msg_flags; //消息发送的标志
      };

      struct iovec{
      void _user * iov_base; //指向数据包缓冲区
      _kernel_size_t iov_len; //数据包长度
      };
  • ssize_t recvmsg(int sock, const struct msghdr * msg, int flags)

头文件与全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <fcntl.h>
#include <asm/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <signal.h>
#include <pwd.h>

#define TM_FMT "%Y-%m-%d %H:%M:%S" //时间格式化串

#define NETLINK_TEST 29 //协议通信号
#define MAX_PAYLOAD 1024 //最大载荷长度
int sock_fd; //套接字标识符
struct msghdr msg; //构造套接字的发送消息
struct nlmsghdr *nlh = NULL; //消息头
struct sockaddr_nl src_addr, dest_addr; //源地址和目标地址
struct iovec iov; //消息内容的结构体
FILE *logfile; //日志文件的FILE结构体指针
函数组成与功能设计
  • void main(int argc, char ** argv)
    • 控制运行总流程
    • 打开日志文件,初始化接口,调用sendpid()将进程标识符发到内核日志模块,循环接收日志消息,调用Log()格式化日志消息后写入日志文件
  • void sendpid(unsigned int pid)
    • 进程标识符封装到缓冲区,调用 SOCKET 接口将标识符发到内核
  • void killdeal_func()
    • 进程终止时自动调用,关闭打开的日志文件内
  • void Log(char * commandname, int uid, int pid, char * filepath, int flags, int ret)
    • 日志记录格式化到日志文件中
    • 格式:用户名 + 程序名 + 访问时间 + 访问文件名+ 访问类型 + 是否访问成功
函数的实现
  • 参见代码仓库FileAudit

编译、运行与测试

编译方法
  • 应用编译

    1
    gcc -o auditdaemon auditaemon.c
  • 内核模块编译

    1
    make

    makefile文件的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    obj-m:=AuditModule.o  #使用目标文件AuditModule.o建立模块AuditModule.ko
    AuditModule-objs :=sdthook.o syscalltable.o netlinkp.o #指明AuditModule.o以来的目标文件,这些文件由同名的.c文件编译
    KDIR := /lib/modules/$(shell uname -r)/build #编译内核需要的资源所在路径
    PWD := $(shell pwd)
    default:
    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules #$(PWD)指明了模块文件所在的路径,modules指明了编译的是内核模块
    clean:
    $(RM) -rf .*.cmd *.mod.c *.o *.ko .tmp*
测试
  • 加载与查看模块

    1
    2
    insmod AuditModule.ko
    lsmod | grep -e AuditModule
  • 卸载内核模块

    1
    rmmod AuditModule

扩展开发实践

基于系统调动重载的系统级资源访问审计
  • 日志范围扩展
    • 资源访问类操作:文件访问类操作。各类文件的创建、打开、读、写、执行
    • 通信类操作:一个进程与外界的交互。进程间的数据通信、进程间的控制通信、网络类通信——日志覆盖重点
    • 管理类操作:重启、关机、内核模块加载、设备添加等
  • 日志项确定
    • 举例:write操作
    • 尽量避免记录动态日志项
    • 记录能够理解的静态项:文件名替代文件描述符
  • 日志处理的扩展
    • 合并日志信息等
    • 日志查看与检索手段
  • 日志系统的灵活性扩展:
    • 管理员能对日志系统自行配置
    • 指定重要目录进行文件访问日志记录
基于系统调用重载的访问控制类开发实践
基于系统调用重载的加密型文件保险箱
基于系统调用重载的日志原型系统的移植