Skip to content

第一百五十七章:系统调用劫持

渡劫期

涉及内核源码:

林小源跟着 security.c 老者继续深入,来到一片古老的石门前。石门上刻满了密密麻麻的符号,每一个符号都指向一个系统调用。

"这是系统调用表,"老者说,"用户空间进入内核的唯一入口。所有程序想要使用内核的服务,都必须经过这里。"

林小源伸手触摸石门,感受到一股温暖的力量从门后传来。那是内核的服务——文件操作、网络通信、进程管理,都在这扇门的后面。

"rootkit 最常用的技术,就是劫持这扇门,"老者的声音变得严肃,"它替换门后的守卫,让所有经过的请求都被它拦截。"

"怎么做到的?"

"它需要解除写保护,"老者指向石门上方的一块铭牌,"x86 的 CR0 寄存器有 WP (Write Protect) 位。只要清除这个位,内核代码段也可以被修改。rootkit 就趁机替换系统调用表中的函数指针。"

林小源感到一阵寒意。这扇门是如此重要,却如此脆弱。

"除了修改系统调用表,还有其他方式吗?"林小源问。

老者点头:"有。inline hook 是在函数入口处注入跳转指令,5 字节的相对跳转,直接把执行流导向恶意代码。ftrace hook 利用 ftrace 框架,在函数入口处插入回调。kprobe hook 更强大,可以在任意地址设置探针。"

"这些技术……都是合法的?"

"技术本身是中性的,"老者说,"ftrace 和 kprobe 是内核提供的调试工具。但 rootkit 滥用它们来隐藏自己。这就是为什么防御如此困难——敌人用的是我们的武器。"

"那怎么保护系统调用表?"林小源问。

老者指向远方几座守护塔。

"现代内核有多重保护。KASLR 让攻击者找不到系统调用表的位置——每次启动,表的地址都不同。SMEP/SMAP 防止内核执行用户空间的代码,防止内核意外访问用户空间的数据。内核写保护防止代码段被修改。"

"但这些保护……"

"不是万能的,"老者说,"没有绝对的安全。但每一层保护都增加了攻击的难度。守住入口,就守住了内核。"

林小源望着那扇石门,心中暗下决心。在这片内核的深处,每一扇门都需要守护。


c
/*
 * 系统调用劫持的方法:
 *
 * 1. 修改 sys_call_table
 *    直接修改表中的函数指针
 *    需要解除写保护 (CR0)
 *
 * 2. inline hook
 *    在函数入口处注入跳转指令
 *    jmp malicious_function
 *
 * 3. ftrace hook
 *    利用 ftrace 框架
 *    在函数入口处插入回调
 *
 * 4. kprobe hook
 *    利用 kprobe 框架
 *    在任意地址设置探针
 */

/* 模拟 sys_call_table */
typedef int (*syscall_fn_t)(void);

struct {
    syscall_fn_t *table;
    int size;
} sys_call_table_obj = {
    .table = NULL,
    .size = 5,
};

syscall_fn_t original_table[5];
syscall_fn_t hooked_table[5];

/* 模拟原始系统调用 */
int sys_read_original(void) {
    return 100;  /* 返回读取的字节数 */
}

int sys_write_original(void) {
    return 200;  /* 返回写入的字节数 */
}

/* hook 后的系统调用 */
int sys_read_hooked(void) {
    printf("  [hook] 拦截 sys_read\n");
    printf("  [hook] 可以记录、修改、拒绝请求\n");
    /* 调用原始实现 */
    return sys_read_original();
}

int sys_write_hooked(void) {
    printf("  [hook] 拦截 sys_write\n");
    printf("  [hook] 可以过滤敏感数据\n");
    return sys_write_original();
}

printf("=== 系统调用劫持 — 控制入口 ===\n\n");

/* 初始化原始表 */
original_table[0] = sys_read_original;
original_table[1] = sys_write_original;

/* 初始化 hook 表 */
hooked_table[0] = sys_read_hooked;
hooked_table[1] = sys_write_hooked;

printf("--- 原始系统调用 ---\n");
printf("sys_read: %d\n", original_table[0]());
printf("sys_write: %d\n", original_table[1]());

printf("\n--- 安装 hook ---\n");
printf("修改 sys_call_table[0] 指向 hook\n");
printf("修改 sys_call_table[1] 指向 hook\n\n");

printf("--- hook 后的系统调用 ---\n");
hooked_table[0]();
hooked_table[1]();

printf("\n--- hook 的实现方式 ---\n");
printf("1. 修改 sys_call_table:\n");
printf("   write_cr0(read_cr0() & ~WP)\n");
printf("   sys_call_table[NR] = hook_fn\n");
printf("   write_cr0(read_cr0() | WP)\n\n");
printf("2. inline hook:\n");
printf("   在函数入口写入 jmp\n");
printf("   5 字节相对跳转\n\n");
printf("3. ftrace hook:\n");
printf("   register_ftrace_function()\n");
printf("   利用 mcount 机制\n\n");

printf("--- 检测 hook ---\n");
printf("1. 检查 sys_call_table 完整性\n");
printf("2. 检查函数入口指令\n");
printf("3. 比较 /proc/kallsyms\n");

道藏笔记

内核启示

系统调用表是用户空间进内核的唯一入口,rootkit 最爱盯的就是这里。最直接的办法是改 sys_call_table 里的函数指针——先把 CR0 的写保护位清掉,再把自己的函数塞进去。更隐蔽的还有 inline hook(在函数入口塞一条 jmp)、ftrace hook(利用 ftrace 的回调机制)、kprobe hook(在任意地址下探针)。讽刺的是,ftrace 和 kprobe 本来是内核提供的调试工具,被 rootkit 拿来当武器了。

防御靠多层叠加:KASLR 每次启动把表地址随机化,让攻击者找不到目标;SMEP/SMAP 禁止内核执行用户空间代码和访问用户空间数据;内核写保护阻止代码段被改写。单独一层不够,但叠起来门槛就高了。

系统调用是入口——守住入口,就守住了内核。


破关试炼

系统调用劫持之试

rootkit 直接替换系统调用入口函数指针时,最常盯上的那张表叫什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道