# 配置 FreeBSD 环境
# 1. 下载 Oracle VirtualBox
# 2. 下载 FreeBSD 的镜像
去这个链接,下载 FreeBSD-14.0-RELEASE-amd64-disc1.iso
# 3. 把镜像导入到 Oracle VirtualBox
# 4. 配置
https://docs.freebsd.org/en/books/handbook/mac/
Chapter 9
Chapter 18. Mandatory Access Control
# 2. 开始实现功能
常量名 | 含义 |
---|---|
EXTATTR_NAMESPACE_USER |
用户命名空间,用户进程可以读写 |
EXTATTR_NAMESPACE_SYSTEM |
系统命名空间,只有内核或特权进程可访问 |
你可以用命令给某个文件添加扩展属性:
setextattr user secure 1 somefile.txt |
这表示:
- 命名空间是
user
- 属性名是
secure
- 属性值是
1
vn_extattr_get(vp, IO_NODELOCKED, EXTATTR_NAMESPACE_USER, "secure", ...) |
表示
“我要读取 vnode vp 上属于用户空间的名为 secure 的扩展属性”。
vnode
是 FreeBSD(以及其他 BSD 类 Unix 系统)内核中的一个重要数据结构,用于统一表示文件系统中的所有对象,包括文件、目录、符号链接、设备节点等。
# 📦 简单理解:vnode 是 “虚拟文件节点”
“vnode” 是 “virtual node” 的缩写,是 VFS(Virtual File System,虚拟文件系统) 的核心概念之一。
你可以把 vnode 看成内核中的一个 “文件描述对象”,但不是用户进程 open()
得到的那个文件描述符,而是内核层面上的抽象表示。
# 🧩 vnode 是干什么用的?
它将不同类型、不同格式的文件系统(UFS, ZFS, NFS 等)统一抽象成一个通用的接口。
文件系统对象 | 对应的 vnode 作用 |
---|---|
普通文件 | 提供读写接口、属性接口等 |
目录 | 提供遍历、添加 / 删除子项接口 |
设备文件 | 提供对设备的统一访问方式 |
套接字、FIFO | 也有 vnode 表示它们 |
# 📄 vnode 的结构(简化)
在 FreeBSD 内核中,一个 struct vnode
通常包含如下信息:
struct vnode {
enum vtype v_type; // 类型:VREG(普通文件)、VDIR(目录)等
struct mount *v_mount; // 所属文件系统
struct vop_vector *v_op; // 指向一组操作函数指针
void *v_data; // 指向具体文件系统的私有数据
...
};
# 📚 vnode 与文件访问的流程
当你在用户态执行:
open("/etc/passwd", O_RDONLY);
系统调用最终会:
- 解析路径 → 得到对应的 vnode
- 检查权限 → 通过 vnode 的属性
- 建立文件描述符 → 关联到该 vnode
- 返回给用户进程使用的 fd
也就是说,每个打开的文件,内核都通过 vnode 来表示和操作。
# 🔐 vnode 在 MAC 模块中的作用
在 MAC(Mandatory Access Control)模块中,很多 hook 函数以 vnode
为参数,比如:
int secure_vnode_check_open(struct ucred *cred, struct vnode *vp, ...)
这里的 vp
(vnode pointer)就表示你试图打开的那个文件或目录,MAC 模块可以通过它:
- 检查扩展属性(是否有
user.secure
) - 判断类型(文件?目录?设备?)
- 拿到挂载点信息、具体路径等
# ✅ 总结
名称 | vnode(virtual node) |
---|---|
是什么 | 内核中表示文件 / 目录的对象 |
属于 | VFS(虚拟文件系统)层 |
作用 | 跨文件系统访问统一接口 |
使用场景 | 内核内部访问文件、权限检查、挂载点解析等 |
MAC 中用法 | 拿 vnode 判断是否需要限制访问 |
# 2. 基础部件
步骤一
编写一个 MAC 模块,当用户打开文件时:
- 系统调用
open()
会触发我们的 hook - 我们打印一条日志,比如:`[mac_log] Process 1234 opened a file
创建一个 C 文件,比如 mac_log.c
:
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/vnode.h>
#include <security/mac_framework.h>
#include <security/mac_policy.h>
#include <sys/proc.h>
static int
mac_log_vnode_check_open(struct ucred *cred, struct vnode *vp,
struct label *vnodelabel, accmode_t accmode)
{
struct proc *p = curproc;
printf("[mac_log] Process %d opened a file\n", p->p_pid);
return 0; // 允许打开
}
static struct mac_policy_ops mac_log_ops = {
.mpo_vnode_check_open = mac_log_vnode_check_open,
};
MAC_POLICY_SET(&mac_log_ops, mac_log, "Simple open logger", MPC_LOADTIME_FLAG_UNLOADOK, NULL);
Makefile
# 步骤二:创建 Makefile
创建一个 Makefile 文件用于编译内核模块:
KMOD = mac_log | |
SRCS = mac_log.c | |
.include <bsd.kmod.mk> |
我要编译一个名字叫
mac_log
的内核模块,它的源代码文件是mac_log.c
。
FreeBSD 会使用你提供的信息,再加上系统自带的编译规则,自动完成:
- 编译
.c
→.o
- 生成
.ko
模块(KLD 文件) - 处理依赖
KMOD = mac_log |
KMOD 是 “Kernel Module” 的缩写。
它的值是你要生成的内核模块的名字(不带 .ko)。
这一行告诉 Makefile:我最后想生成一个 mac_log.ko 文件。
SRCS = mac_log.c
这是源码文件的列表,告诉系统我们要编译的是 mac_log.c。
如果你有多个源文件,比如 a.c b.c,就写成:
- MALLOC_DEFINE
MALLOC_DEFINE(M_TAINT, "mac_secure_taint", "tainted-process list");
它的作用是:
在内核中定义一个新的 内存分配标签(memory allocation type),名字叫
M_TAINT
,描述为"mac_secure_taint"
和"tainted-process list"
。
之后你就可以这样分配内存:
te = malloc(sizeof(*te), M_TAINT, M_WAITOK | M_ZERO);
也可以这样释放:
free(te, M_TAINT);
# 🧩 二、语法结构解析
MALLOC_DEFINE(name, shortdesc, longdesc)
# 参数说明:
参数 | 解释 |
---|---|
name |
一个 C 宏变量(如 M_TAINT ),以后 malloc() 和 free() 会用这个作为分类标签 |
shortdesc |
用于内核统计或调试的简短说明 |
longdesc |
更详细的描述,用于内核分析工具显示 |
# 🔍 三、这个宏是怎么定义的?
这个宏定义在头文件 <sys/malloc.h>
中:
#define MALLOC_DEFINE(type, shortdesc, longdesc) \
struct malloc_type type[1] = { \
{ NULL, 0, shortdesc, longdesc } \
}
它背后实际是定义了一个结构体数组:
struct malloc_type M_TAINT[1] = {
{ NULL, 0, "mac_secure_taint", "tainted-process list" }
};
这个 malloc_type
会被 FreeBSD 内核的 malloc 管理器识别,用于:
- 统计这个类型分配了多少内存
- 给内核工具如
vmstat -m
提供内存分析报告
# 内核链表
struct taint_entry { | |
LIST_ENTRY(taint_entry) link; | |
pid_t pid; | |
}; |
LIST_ENTRY(taint_entry) link;
相当于
struct { | |
struct taint_entry *le_next; // 指向下一个元素 | |
struct taint_entry **le_prev; // 指向前一个元素的指针 | |
} link; |
看似简单,其实它背后结合了 FreeBSD(BSD 风格)内核编程中非常重要的一套数据结构:内核链表(BSD 链表宏)。
static struct rwlock taint_lock;
FreeBSD 内核使用 struct rwlock 实现 读写锁,用于并发同步。
🔒 为什么需要加锁?
因为多个 CPU / 线程可能会同时:
向 taint_head 插入节点(LIST_INSERT_HEAD)
遍历它(LIST_FOREACH)
删除节点(LIST_REMOVE)
不加锁会造成数据竞争,甚至内核崩溃。
🔐 为什么用 rwlock 而不是 mutex?
因为:
读多写少场景,读写锁更高效
多个 reader 可以同时持有读锁(rw_rlock)
写的时候需要独占写锁(rw_wlock)
例如:
rw_rlock (&taint_lock); // 加读锁
rw_runlock (&taint_lock); // 解读锁
rw_wlock (&taint_lock); // 加写锁
rw_wunlock (&taint_lock); // 解写锁
✅ 第三部分:static 的含义(非常重要)
在这两行中:
static LIST_HEAD(...) taint_head;
static struct rwlock taint_lock;
static 的作用是 限制变量作用域只在当前 C 文件中有效。
📍 为什么要这样?
防止模块中其他文件访问到这些全局变量
提高封装性
避免与其他模块 / 内核全局变量命名冲突
在内核模块中,几乎所有你不希望暴露给外部的全局变量或函数都应加 static。
struct proc { | |
pid_t p_pid; // 进程 ID | |
struct ucred *p_ucred; // 用户身份信息(谁在运行这个进程) | |
struct filedesc *p_fd; // 打开文件的描述符表 | |
struct proc *p_pptr; // 父进程指针 | |
struct thread *p_threads; // 线程链表(FreeBSD 是线程调度) | |
int p_state; // 当前状态 | |
... | |
}; |
# Challenge
For this challenge please use FreeBSD 14.0-RELEASE for everything.
Your submitted code should contain everything necessary to build your module, as well as build instructions.
# A simple module
Write a simple FreeBSD kernel module. This module should register itself as a MAC policy module and should have the following functionality:
- Every open request on a file containing the extended attribute
user.secure
should be denied. - If such a request happened, the requesting process should be marked as
tainted
. - All requests to remove extended attributes by a process marked
tainted
(or one of its descendants) on a file containing the extended attributeuser.secure
should be denied. - Your module has to be cleanly unloadable - no memory leaks!
- Everything has to be appropriately synchronized, your module should not be subject to race-conditions
/* mac_secure.c | |
* | |
* A small FreeBSD 14.0-RELEASE MAC policy module that: | |
* 1. Denies open requests on files with the "user.secure" xattr; | |
* 2. Marks such processes as "tainted"; | |
* 3. Denies any attempts by tainted processes to remove this xattr; | |
* 4. Is cleanly unloadable (no memory leaks); | |
* 5. Uses locking to ensure concurrency safety. | |
*/ | |
#include <sys/param.h> | |
#include <sys/kernel.h> | |
#include <sys/module.h> | |
#include <sys/systm.h> | |
#include <sys/malloc.h> | |
#include <sys/lock.h> | |
#include <sys/rwlock.h> | |
#include <sys/proc.h> | |
#include <sys/ucred.h> | |
#include <sys/vnode.h> | |
#include <sys/extattr.h> | |
#include <sys/errno.h> | |
#include <security/mac/mac_policy.h> | |
#define ATTR_NAMESPACE EXTATTR_NAMESPACE_USER | |
#define ATTR_NAME "secure" | |
//struct malloc_type M_TAINT[1] = { | |
// { NULL, 0, "mac_secure_taint", "tainted-process list" } | |
//}; | |
MALLOC_DEFINE(M_TAINT, "mac_secure_taint", "tainted-process list"); | |
// 定义了目标扩展属性 `user.secure` 的命名空间和名字。 | |
/* ---------- taint tracking ---------- */ | |
struct taint_entry { | |
LIST_ENTRY(taint_entry) link; | |
pid_t pid; | |
}; | |
//#define LIST_HEAD_INITIALIZER(head) { NULL } | |
//struct { | |
// struct taint_entry *lh_first; | |
//} taint_head = { NULL }; | |
static LIST_HEAD(, taint_entry) taint_head = LIST_HEAD_INITIALIZER(taint_head); | |
static struct rwlock taint_lock; /* protects taint_head */ | |
static bool | |
proc_is_tainted(struct proc *p){// 检查给定进程 p 是否已经被标记为 tainted(污染) | |
struct taint_entry *te; | |
bool tainted = false; | |
rw_rlock(&taint_lock);// 使用 rw_rlock () 给 taint_head 加读锁,表示我们只读链表,不会修改它。 | |
LIST_FOREACH(te, &taint_head, link) {// 相当于 for (te = taint_head.lh_first; te != NULL; te = te->link.le_next) | |
if (te->pid == p->p_pid) { | |
tainted = true; | |
break; | |
} | |
} | |
rw_runlock(&taint_lock); | |
return (tainted); | |
} | |
static void | |
proc_mark_tainted(struct proc *p){// 将指定的进程 p 标记为 tainted | |
struct taint_entry *te; | |
if (proc_is_tainted(p)) | |
return; | |
te = malloc(sizeof(*te), M_TAINT, M_WAITOK | M_ZERO); | |
// 分配一块大小为 struct taint_entry 的内存,类型为 M_TAINT,并初始化为全 0。 | |
//M_WAITOK 如果内存不够,允许睡眠等待(而不是立刻失败) | |
te->pid = p->p_pid; | |
rw_wlock(&taint_lock); | |
LIST_INSERT_HEAD(&taint_head, te, link);// 把这个 taint_entry 插入到 taint 链表的头部。 | |
rw_wunlock(&taint_lock); | |
} | |
static void | |
proc_untaint(struct proc *p)// 把一个被标记为 tainted 的进程,从 taint 列表中移除,并释放对应内存。 | |
{ | |
struct taint_entry *te; | |
rw_wlock(&taint_lock); | |
LIST_FOREACH(te, &taint_head, link) {// 相当于 for (te = taint_head.lh_first; te != NULL; te = te->link.le_next) | |
if (te->pid == p->p_pid) { | |
LIST_REMOVE(te, link);// 这是 BSD 提供的宏,用于从链表中删除 te 节点 | |
free(te, M_TAINT); | |
break; | |
} | |
} | |
rw_wunlock(&taint_lock); | |
} | |
static bool | |
vnode_has_secure(struct vnode *vp)// 判断一个 vnode(文件或目录)是否拥有扩展属性 user.secure。 | |
{ | |
char dummy; | |
int buflen = 1; | |
int error; | |
error = vn_extattr_get(vp, // 要操作的 vnode | |
IO_NODELOCKED, // 表示 vnode 已加锁,避免在此函数内部再次加锁(防止死锁) | |
ATTR_NAMESPACE,// 命名空间,一般是 EXTATTR_NAMESPACE_USER,也就是 user | |
ATTR_NAME,// 属性名,这里是 "secure" | |
&buflen,// 输入:缓冲区大小;输出:实际读取的字节数 | |
&dummy,// 缓冲区指针,接收扩展属性内容(我们不关心它) | |
curthread);// 当前线程指针,表示这是谁在发起操作(权限检查用) | |
// 这个函数是 FreeBSD 内核中读取扩展属性的标准接口。 | |
return (error == 0); // 如果 vn_extattr_get 返回 0,表示成功读取属性 // 否则返回错误码(如 ENOATTR 表示属性不存在) | |
} | |
/* ---------- MAC hooks ---------- */ | |
static int | |
secure_vnode_check_open(struct ucred *cred, struct vnode *vp, | |
struct label *vplabel, accmode_t accmode){ | |
//struct ucred *cred 它是 FreeBSD 内核中非常重要的一个结构体指针,用于描述:“当前操作的用户身份信息”,也就是一个进程或线程的权限凭证(credentials)。 | |
//struct ucred { | |
//uint32_t cr_ref; // 引用计数 | |
//uid_t cr_uid; // 实际用户 ID(effective UID) | |
//uid_t cr_ruid; // 实际用户 ID(real UID) | |
//gid_t cr_gid; // 主组 ID | |
//gid_t cr_groups [NGROUPS]; // 所属组列表 | |
//int cr_ngroups; // 有多少个组 | |
// ... | |
//}; | |
if (vnode_has_secure(vp)) {// 判断文件是否有 user.secure 属性 | |
proc_mark_tainted(curproc); | |
return (EPERM);// EPERM 是 C 系统编程中一个非常常见的 错误码,它的含义是:Operation not permitted(操作不被允许) | |
} | |
return (0); | |
} | |
//curproc 和 curthread 是 FreeBSD 内核中两个非常关键的全局变量,用于表示当前正在运行的 “进程” 和 “线程”。 | |
// 这两个变量随时指向正在被 CPU 执行的代码的上下文环境,几乎在所有内核子系统(VFS、MAC、网络)中都要用到。 | |
//curproc struct proc * 当前运行的 进程 获取当前进程的 PID、UID、访问控制、文件描述符等 | |
//curthread struct thread * 当前运行的 线程 获取线程栈、错误码、调度状态、I/O 权限等 | |
static int | |
secure_vnode_check_setextattr(struct ucred *cred, struct vnode *vp, | |
struct label *vplabel, int attrnamespace, | |
const char *name){// 阻止 tainted 进程去修改带有 user.secure 扩展属性的文件的属性。 | |
if (attrnamespace == ATTR_NAMESPACE && | |
name != NULL && strcmp(name, ATTR_NAME) == 0) {// 属性命名空间是否是 user(即 EXTATTR_NAMESPACE_USER) | |
if (proc_is_tainted(curproc) && vnode_has_secure(vp)) | |
return (EPERM); // 我们只想阻止那些:“已经被 tainted 的进程 又 试图修改一个真正是 user.secure 的文件” 的行为。????????????? | |
} | |
return (0); | |
} | |
static void | |
secure_policy_init(struct mac_policy_conf *conf __unused){// 是你 FreeBSD MAC 模块的初始化函数,在模块加载时由内核自动调用。 | |
rw_init(&taint_lock, "mac_secure taint lock"); | |
//taint_lock 是一个全局变量,类型为 struct rwlock | |
// 在使用任何 rw_wlock ()、rw_rlock () 之前必须初始化! | |
// 第二个参数是字符串描述,用于调试、锁统计信息输出 | |
//init 之后才能安全使用 | |
//rw_rlock (&taint_lock); // 加读锁 | |
//rw_runlock (&taint_lock); // 解读锁 | |
//rw_wlock (&taint_lock); // 加写锁 | |
//rw_wunlock (&taint_lock); // 解写锁 | |
} | |
static void | |
secure_policy_destroy(struct mac_policy_conf *conf __unused){ | |
// 是 FreeBSD MAC 模块的卸载钩子函数(销毁函数),它非常重要,作用是: | |
// 在模块被卸载(kldunload)时清除 taint 列表和销毁读写锁,确保没有内存泄漏、没有脏资源留下。 | |
struct taint_entry *te; | |
rw_wlock(&taint_lock); | |
while ((te = LIST_FIRST(&taint_head)) != NULL) { | |
LIST_REMOVE(te, link); | |
free(te, M_TAINT); | |
} | |
rw_wunlock(&taint_lock); | |
rw_destroy(&taint_lock);// 把锁本身释放掉 | |
} | |
static struct mac_policy_ops secure_ops = { // 定义一个结构体 secure_ops,类型是 mac_policy_ops,这是 FreeBSD MAC 框架中用于注册你的安全策略行为的结构体。 | |
.mpo_init = secure_policy_init, | |
.mpo_destroy = secure_policy_destroy, | |
.mpo_vnode_check_open = secure_vnode_check_open, | |
.mpo_vnode_check_setextattr = secure_vnode_check_setextattr, | |
}; | |
MAC_POLICY_SET(&secure_ops, mac_secure, "Deny open/remove user.secure", | |
MPC_LOADTIME_FLAG_UNLOADOK, NULL); // 我有一个策略模块叫 mac_secure,它的行为定义在 secure_ops 结构里,请你以后每次 open 文件时调用我写的 secure_vnode_check_open。 | |
//&secure_ops 所有 hook 函数定义的结构体地址 | |
//mac_secure 模块内部名称 | |
//"My policy" 模块对外说明文字(比如打印在日志中) | |
//MPC_LOADTIME_FLAG_UNLOADOK “这个 MAC 策略模块可以被卸载(unload)。” | |
//// 在 sys/security/mac_policy.h 中 | |
//#define MPC_LOADTIME_FLAG_UNLOADOK 0x00000001 所以,它只是一个 int 类型的标志位值,用来传给注册函数,告诉系统你这个模块是否可以被 kldunload 卸载。 | |
//#define MPC_LOADTIME_FLAG_UNLOADOK 0x00000001 // 可卸载 | |
//#define MPC_LOADTIME_FLAG_NOTLATE 0x00000002 // 必须在启动时加载(不能 late load) | |
//#define MPC_LOADTIME_FLAG_LABELMBUFS 0x00000004 // 标记 mbuf 网络数据结构 | |
//#define MPC_LOADTIME_FLAG_LABELINPCB 0x00000008 // 标记 socket 的 inpcb | |
//MODULE_VERSION(mac_secure, 1); |
# 写完之后
# 1. 编译模块 | |
make | |
# 2. 查看模块是否已经在系统中(应该还没) | |
kldstat | |
# 3. 加载模块 | |
sudo kldload ./hello.ko | |
# 4. 再次查看 | |
kldstat | grep hello # 现在应该能看到 hello.ko | |
# 5. 卸载模块 | |
sudo kldunload hello | |
# 6. 再查看(已移除) | |
kldstat |
Appendix
/* | |
* KLD Skeleton | |
* Inspired by Andrew Reiter's Daemonnews article | |
*/ | |
#include <sys/types.h> | |
#include <sys/systm.h> /* uprintf */ | |
#include <sys/errno.h> | |
#include <sys/param.h> /* defines used in kernel.h */ | |
#include <sys/module.h> | |
#include <sys/kernel.h> /* types used in module initialization */ | |
/* | |
* Load handler that deals with the loading and unloading of a KLD. | |
*/ | |
static int | |
skel_loader(struct module *m, int what, void *arg) | |
{ | |
int err = 0; | |
switch (what) { | |
case MOD_LOAD: /* kldload */ | |
uprintf("Skeleton KLD loaded.\n"); | |
break; | |
case MOD_UNLOAD: | |
uprintf("Skeleton KLD unloaded.\n"); | |
break; | |
default: | |
err = EOPNOTSUPP; | |
break; | |
} | |
return(err); | |
} | |
/* Declare this module to the rest of the kernel */ | |
static moduledata_t skel_mod = { | |
"skel", | |
skel_loader, | |
NULL | |
}; | |
DECLARE_MODULE(skeleton, skel_mod, SI_SUB_KLD, SI_ORDER_ANY); |
最后一句用的是这个
#define DECLARE_MODULE(name, data, sub, order) \ | |
static struct module __CONCAT(name, _mod) = { \ | |
.name = #name, \ | |
.data = &data, \ | |
.subsystem = sub, \ | |
.order = order, \ | |
.handler = module_register_init, \ | |
.arg = NULL, \ | |
}; \ | |
SYSINIT(__CONCAT(name, _init), sub, order, \ | |
module_register, &__CONCAT(name, _mod)); \ | |
SYSUNINIT(__CONCAT(name, _uninit), sub, order, \ | |
module_unregister, &__CONCAT(name, _mod)); |
#include<sys/param.h> | |
#include<sys/kernel.h> | |
#include<sys/module.h> | |
#include <sys/systm.h> | |
static int hello_loader(struct module *m, int event, void *arg) | |
{ | |
switch(event) { | |
case MOD_LOAD: | |
uprintf("hello_kld - Hello World\n"); | |
break; | |
case MOD_UNLOAD: | |
uprintf("hello_kld - Goodbye, Kernel\n"); | |
break; | |
default: | |
return EOPNOTSUPP; | |
} | |
return 0; | |
} | |
static moduledata_t hello_mod = { | |
"Hello World Kernel Module", | |
hello_loader, | |
NULL, | |
}; | |
DECLARE_MODULE(hello_world, hello_mod, SI_SUB_KLD, SI_ORDER_ANY); |
reference
https://docs.freebsd.org/en/books/handbook/mac/
https://docs.freebsd.org/en/books/arch-handbook/mac/
竞态条件(race condition)是并发编程中常见的问题之一,指的是:
当两个或多个线程(或进程)同时访问某个共享资源,并且至少有一个是写操作,程序的最终结果就可能依赖于它们的执行顺序。
# 如何避免竞态条件?
通过同步机制来保护共享资源,例如:
- 🔒 互斥锁(mutex)
- 🔁 读写锁(rwlock)
- 🧩 信号量(semaphore)
- 🧵 原子操作(atomic)
这样可以确保同一时刻