无需费力编写rootkit:Linux内核系统调用拦截实战

本文详细讲解了如何通过Linux内核模块实现系统调用拦截技术,包括获取系统调用表地址、绕过CPU写保护机制,并演示了修改read系统调用的完整实现过程。该技术可用于故障注入测试或构建rootkit等场景。

如何轻松编写rootkit - Trail of Bits技术博客

我们开源了名为KRF的故障注入工具,它利用内核态系统调用拦截技术。您现在就可以用它来发现程序中的错误假设(以及由此产生的bug)。

内核模块拦截系统调用

本文将介绍通过普通内核模块在Linux内核中拦截系统调用的方法。我们将快速回顾系统调用及其拦截价值,然后演示一个拦截read(2)系统调用的基础模块。

与其他故障注入技术的区别

常见故障注入技术主要有:

  1. LD_PRELOAD技巧:拦截libc暴露的系统调用包装器

    • 缺点:仅适用于动态链接,不适用于Go等静态编译语言
    • 系统调用包装器与实际系统调用可能存在差异
  2. 动态插桩框架:如DynamoRIO或Intel PIN

    • 缺点:运行时开销大

内核空间故障注入避免了这些问题:直接重写实际系统调用,且几乎无运行时开销。

系统调用基础

系统调用是内核向用户空间暴露资源的函数,包括:

  • I/O操作:open(2), close(2), read(2), write(2)
  • 进程管理:fork(2), kill(2), exit(2)
  • 网络通信:send(2), recv(2)

系统调用比用户空间函数调用代价高得多,涉及CPU中断处理和特权上下文切换。

实现步骤

获取系统调用表

Linux内核自2.5版后不再直接暴露sys_call_table符号。我们使用kallsyms_lookup_name接口获取地址:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static unsigned long *sys_call_table;

int init_module(void) {
  sys_call_table = (void *)kallsyms_lookup_name("sys_call_table");
  if (!sys_call_table) {
    printk(KERN_ERR "查找sys_call_table失败\n");
    return -1;
  }
  return 0;
}

替换系统调用

实现read系统调用拦截:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static typeof(sys_read) *orig_read;

asmlinkage long phony_read(int fd, char __user *buf, size_t count) {
  printk(KERN_INFO "拦截read调用: fd=%d, %lu字节\n", fd, count);
  return orig_read(fd, buf, count);
}

int init_module(void) {
  // ...获取sys_call_table...
  orig_read = (typeof(sys_read) *)sys_call_table[__NR_read];
  
  // 需要解除CR0写保护
  CR0_WRITE_UNLOCK({
    sys_call_table[__NR_read] = (void *)phony_read;
  });
  
  return 0;
}

void cleanup_module(void) {
  CR0_WRITE_UNLOCK({
    sys_call_table[__NR_read] = (void *)orig_read;
  });
}

处理x86写保护

x86架构下需要通过修改CR0寄存器解除写保护:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define CR0_WRITE_UNLOCK(x) \
  do { \
    unsigned long __cr0; \
    preempt_disable(); \
    __cr0 = read_cr0() & (~X86_CR0_WP); \
    write_cr0(__cr0); \
    x; \
    __cr0 = read_cr0() | X86_CR0_WP; \
    write_cr0(__cr0); \
    preempt_enable(); \
  } while (0)

进阶应用

我们可以扩展phony_read实现更多功能:

  1. 直接返回错误:
1
return -ENOSYS;
  1. 针对特定用户:
1
2
3
if (current_uid().val == 1005) {
  return -ENOSYS;
}
  1. 返回伪造数据:
1
2
3
4
unsigned char kbuf[1024];
memset(kbuf, 'A', sizeof(kbuf));
copy_to_user(buf, kbuf, sizeof(kbuf));
return sizeof(kbuf);

总结

本文介绍了内核空间系统调用拦截的基础知识。我们的KRF工具实现了更高级的功能:

  • 按可执行文件精确拦截
  • 支持整个系统调用"配置文件"
  • 实时故障注入能力

其他相关技术包括:

  • syscall_intercept:通过LD_PRELOAD实现
  • ptrace(2):用户空间拦截子进程系统调用

查看完整KRF工具源码

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计