13.3.3 构造函数verity_ctrdm-verity 的构造函数的代码逻辑与块设备关系不大。下面只分析 Device Mapper 层和dm-verity 层的代码逻辑。1.Device MapperDevice Mapper 架构为用户态程序提供的接口是一个名为 control 的设备文件全路径名为/dev/mapper/control主设备号为 10misc 设备从设备号为 236。用户态工具如 dmsetup会通过 ioctl 系统调用操作这个设备。下面看一下代码drivers/md/dm-ioctl.c static ioctl_fn lookup_ioctl(unsigned int cmd, int *ioctl_flags) { static struct { int cmd; int flags; ioctl_fn fn; } _ioctls[] { ... {DM_TABLE_LOAD_CMD, 0, table_load}, ... }; if (unlikely(cmd ARRAY_SIZE(_ioctls))) return NULL; *ioctl_flags _ioctls[cmd].flags; return _ioctls[cmd].fn; }命令 DM_TABLE_LOAD_CMD整数 9对应的函数是 table_load。table_load 会最终调用函数 dm_table_add_targetdm_table_add_target 会执行r tgt-type-ctr(tgt, argc, argv);调用某个具体类型的构造函数。2. dm_verityctr 是一个函数指针指向 dm_verity 的构造函数 verity_ctr。verity_ctr 本身逻辑比较简单它需要 10 个参数全部是必选参数。按照顺序分别是数据设备、哈希设备、数据块大小、哈希块大小、数据设备包含的块数、哈希设备起始块、哈希算法、根哈希值root hash、盐。如果最后的参数输入为“-”就意味着没有盐。参数“哈希设备起始块”规定哈希设备从起始块开始才存储前面提到的那棵存储哈希值的树至于起始块之前存储什么dm-verity 并没有规定。Android 4.4 利用起始块之前的空间存储了一个对哈希树的数字签名有兴趣的读者可以参考 http://nelenkov.blogspot.com/2014/05/using-kitkat- verified-boot.html。前面提到内核 dm-verity 部分不负责建立 hash device 中正确的数据结构。通常是利用用户态工具如 veritysetup来创建 hash device。所以需要注意的是构建 dm-verity 设备时输入的参数“盐”要和执行 veritysetup 时的输入的参数“盐”一致输入参数“根校验值”root hash要和 veritysetup 的输出一致。13.4 总结dm-verity 是 Device Mapper 的一种设备类型用来保证设备的完整性。它的巧妙性在于将对数据设备的完整性保护转化为对一个根哈希值的完整性保护。只要这个根哈希值没有被篡改任何对数据设备的篡改都可以检测出来。13.5 参考资料读者可参考以下资料 www.ibm.com/developerworks/cn/linux/l-devmapper/ Documentation/device-mapper/ lwn.net/Articles/459420 gitlab.com/cryptsetup/cryptsetup/wikis/DMVerity习题根哈希值不被篡改对于 dm-verity 设备至关重要。设计几种方案保护根哈希值思考一下这些方案的利弊。第四部分 审计和日志审计和日志的目的是记录系统关键行为。审计和日志就像城市街头的摄像头。摄像头本身并不能阻止违法和犯罪但是它能记录犯罪并为追踪犯罪提供方便。第 14 章 审计audit14.1 简介14.1.1 审计和日志audit 这个英文单词直译为“审计”英文释义为“an official inspection of an individual’s or organization’s accounts, typically by an independent body.”从上面释义中可以看到audit 有两个特征正式的审查official inspection和独立的个体independent body。在 Linux 系统中独立的个体就是内核正式的审查就是在内核中根据设定的规则生成审计消息。日志与审计有些类似都可以反映系统的行为。其区别在于日志基于一种自觉行为系统的多个守护进程daemon在执行过程中发送日志消息给日志服务进程后者将消息记录到日志文件中。系统守护进程可以多发、少发、不发甚至错发日志消息。14.1.2 概貌audit 的架构如图 14-1 所示。Linux 内核的 audit 子系统使用 netlink 套接字作为和用户态进程之间的接口。auditd 守护进程通过 netlink 套接字不断从内核 audit 子系统接收审计消息。auditd 进程会将审计消息发往两个去处一个是通过 AF_UNIX/AF_LOCAL 套接字发送给它的子进程 audispd后者依照配置做进一步的分发另一个是写入 audit.log。audit.log 的内容实在是过于庞杂为了让用户更容易理解audit 开发人员提供了 ausearch 和 aureport。前者用于在 audit.log 中寻找特定信息后者用于归纳 audit.log 中的信息。auditctl 用于向内核设定审计规则。autrace 的功能与 strace 的功能类似都是运行一条命令跟踪此命令所产生的进程的系统调用情况。autrace 的实现原理是在运行命令之前设定审计规则在执行结束时再将审计规则删除。在作者的计算机上执行的效果是rootubuntu-desktop:/tmp# autrace /bin/echo hello worldWaiting to execute: /bin/echohello worldCleaning up...Trace complete. You can locate the records with ausearch -i -p 4357 rootubuntu-desktop:/tmp#实际上autrace 会创建两条规则假设上例中 echo 产生的进程的进程号为 4212:LIST_RULES: entry,always pid4212 (0x1074) syscallall LIST_RULES: entry,always ppid4212 (0x1074) syscallall在上例中 echo 进程结束后autrace 会删除上述两条规则。同 strace 相比autrace 并不好用。因为 autrace 是一个需要特权的命令另外它的输出在 audit.log 文件中混杂在系统所有审计消息之中实在是不易辨析。图 14-1 中的 application 是指一些具有 CAP_AUDIT_WRITE 的进程。这些进程可以产生一些“审计消息”发给内核。按道理应该是内核审计用户态进程这里却是用户态进程发送“审计消息”给内核。这种所谓的“审计消息”实际上是日志消息。audit 子系统在实现中兼有审计和日志功能。audit 子系统转发 application 生成的日志消息给用户态守护进程 auditd。下面举几个通过 auditctl 设定规则的例子。检查 pid 为 20015 的进程的所有系统调用auditctl -a always,exit -S all -F pid20015检查 euid 为 1000 的所有进程的 openat 系统调用auditctl -a always,exit -S openat -F euid1000检查对文件/etc/shadow 的写和添加操作auditctl -a always,exit -F path/etc/shadow -F permwa14.2 架构14.2.1 四个消息来源前面讲过审计有两个特征正式的检查和独立的执行者。Linux 内核的 audit 子系统并不纯粹它还包含部分日志功能。下面列出审计消息的四个来源其中前三个来源实际上是日志属性的消息。1. 内核内核中的一些子系统会使用 audit 子系统提供的函数产生 audit 日志消息。audit 主要提供了3 个函数供其他子系统使用audit_log_start、audit_log、audit_log_end。下面以 SELinux 为例看一下调用过程security/selinux/ss/services.c static void security_dump_masked_av(struct context *scontext, struct context *tcontext, u16 tclass, u32 permissions, const char *reason) { … /* audit a message */ ab audit_log_start(current-audit_context, GFP_ATOMIC, AUDIT_SELINUX_ERR); if (!ab) goto out; audit_log_format(ab, opsecurity_compute_av reason%s scontext%s tcontext%s tclass%s perms, reason, scontext_name, tcontext_name, tclass_name); for (index 0; index 32; index) { u32 mask (1 index); if ((mask permissions) 0) continue; audit_log_format(ab, %s%s, need_comma ? , : , permission_names[index] ? permission_names[index] : ????); need_comma true; } audit_log_end(ab); … }audit_log_start准备一个bufferaudit_log_format向buffer中填充内容audit_log_end将buffer制作成 audit 消息发出。内核用 audit_log 系列函数生成的实际上是日志消息不是审计性质的消息。2. 用户态守护进程第二个来源是一些用户态守护进程。以 sshd 为例看几段代码udit-linux.c int linux_audit_record_event(int uid, const char *username, const char *hostname, const char *ip, const char *ttyn, int success) { int audit_fd, rc, saved_errno; audit_fd audit_open(); if (audit_fd 0) { if (errno EINVAL || errno EPROTONOSUPPORT || errno EAFNOSUPPORT ) return 1; /* No audit audit support in kernel */ else return 0; /* Must prevent login */ } rc audit_log_acct_message(audit_fd, AUDIT_USER_LOGIN, NULL, login, username ? username : (unknown), usernameNULL ? uid :-1, hostname, ip, ttyn, success); saved_errno errno; close(audit_fd); /* * Do not report error if the error is EPERM and sshd is run as non * root user. */ if ((rc EPERM) (getuid() ! 0 )) rc 0; errno saved_errno; return (rc0); } audit_open 和 audit_log_acct_message 都在 audit 的用户态库中实现。audit_open 的实现比较 简单 lib/netlink.c int audit_open(void) { int saved_errno; int fd socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT); … return fd; }audit_open 函数的核心就是获得一个 NETLINK_AUDIT 类型的 NETLINK 套接字。audit_log_acct_message 函数的核心是调用函数 audit_send_user_message, audit_send_user_ message 会调用函数 audit_send。audit_send 的核心是调用 Linux 系统调用 sendto也就是通过netlink 套接字发送消息给内核。简言之用户态守护进程 sshd 会申请一个 audit netlink 套接字然后向这个套接字中发送消息。netlink 套接字作为内核态和用户态的接口sshd 发送的消息会被内核接收。内核接收这些消息后不做任何处理再发还给 audit netlink 套接字但是套接字的端口号换成 auditd 相关的端口号。用户态的 auditd 进程会接收这些消息。这种设计有些怪异用户态产生的消息不直接发送给用户态的 auditd而发给内核由内核转发给用户态的 auditd。3. auditd第三个来源是 auditd 本身这个就不细说了。auditd 本身是一个用户态守护进程不同的是它不会把自身产生的 audit 消息传入内核而是直接发送给 audispd同时写入audit.log。4. 系统调用这是最重要的一个来源因为前三个来源都是日志不是审计只有这个来源才是审计。内核作为独立的个体做正式的检查它检查的对象是用户态进程检查点设置在系统调用的实现里面。先看系统调用入口中的 audit 钩子函数arch/x86/kernel/entry_64.S …… auditsys: movq %r10,%r9 /* 6th arg: 4th syscall arg */ movq %rdx,%r8 /* 5th arg: 3rd syscall arg */ movq %rsi,%rcx /* 4th arg: 2nd syscall arg */ movq %rdi,%rdx /* 3rd arg: 1st syscall arg */ movq %rax,%rsi /* 2nd arg: syscall number */ movl $AUDIT_ARCH_X86_64,%edi /* 1st arg: audit arch */ call __audit_syscall_entry LOAD_ARGS 0 /* reload call-clobbered registers */ jmp system_call_fastpath /* * Return fast path for syscall audit. Call __audit_syscall_exit() * directly and then jump back to the fast path with TIF_SYSCALL_AUDIT * masked off. */ sysret_audit: movq RAX-ARGOFFSET(%rsp),%rsi /* second arg, syscall return value */ cmpq $-MAX_ERRNO,%rsi /* is it -MAX_ERRNO? */ setbe %al /* 1 if so, 0 if not */ movzbl %al,%edi /* zero-extend that into %edi */ call __audit_syscall_exit movl $(_TIF_ALLWORK_MASK _TIF_SYSCALL_AUDIT),%edi jmp sysret_check …在系统调用的入口调用__audit_syscall_entry在出口调用__audit_syscall_exit。下面看一下这两个函数kernel/auditsc.c void __audit_syscall_entry(int arch, int major, unsigned long a1, unsigned long a2, unsigned long a3, unsigned long a4) { struct task_struct *tsk current; struct audit_context *context tsk-audit_context; … context-arch arch; context-major major; context-argv[0] a1; context-argv[1] a2; context-argv[2] a3; context-argv[3] a4; … if (!context-dummy state AUDIT_BUILD_CONTEXT) { context-prio 0; stateaudit_filter_syscall(tsk,context,audit_filter_list[AUDIT_ FILTER_ENTRY]); } … } void __audit_syscall_exit(int success, long return_code) { struct task_struct *tsk current; struct audit_context *context; if (success) success AUDITSC_SUCCESS; else success AUDITSC_FAILURE; context audit_get_context(tsk, success, return_code); if (!context) return; if (context-in_syscall context-current_state AUDIT_RECORD_CONTEXT) audit_log_exit(context, tsk); context-in_syscall 0; context-prio context-state AUDIT_RECORD_CONTEXT ? 0ULL : 0; if (!list_empty(context-killed_trees)) audit_kill_trees(context-killed_trees); audit_free_names(context); unroll_tree_refs(context, NULL, 0); audit_free_aux(context); context-aux NULL; context-aux_pids NULL; context-target_pid 0; context-target_sid 0; context-sockaddr_len 0; context-type 0; context-fds[0] -1; if (context-state ! AUDIT_RECORD_CONTEXT) { kfree(context-filterkey); context-filterkey NULL; } tsk-audit_context context; }除了这两个钩子函数还有许多 audit 钩子函数分布在系统调用的函数调用路径中。这些钩子函数都在做一件事将信息填入进程的 audit_context 结构中。下面看一下 audit_context 结构kernel/audit.h struct audit_context { … int major;/* syscall number */ unsigned long argv[4]; /* syscall arguments */ long return_code;/* syscall return code */ int return_valid; /* return code is valid */ … struct sockaddr_storage *sockaddr; size_t sockaddr_len; /* Save things to print about task_struct */ pid_t pid, ppid; kuid_t uid, euid, suid, fsuid; kgid_t gid, egid, sgid, fsgid; unsigned long personality; int arch; pid_t target_pid; kuid_t target_auid; kuid_t target_uid; unsigned int target_sessionid; u32 target_sid; char target_comm[TASK_COMM_LEN]; struct audit_names preallocated_names[AUDIT_NAMES]; int name_count; /* total records in names_list */ struct list_head names_list; /* struct audit_names-list anchor */ struct audit_tree_refs *trees, *first_trees; struct list_head killed_trees; int tree_count; union { struct { int nargs; long args[6]; } socketcall; struct { kuid_t uid; kgid_t gid; umode_t mode; u32 osid; int has_perm; uid_t perm_uid; gid_t perm_gid; umode_t perm_mode; unsigned long qbytes; } ipc; … }; … }在进程的 task_struct 中有一个指针指向一个 audit_context 实例。在 audit_context 中有一部分成员用于规则匹配一部分成员用于生成审计消息。文件和目录的处理比较复杂audit_context中有一些成员专门用于文件和目录的审计。