最近在完成学校操作系统实验,其中一个小任务是篡改系统调用,但是在读完老师发的实验手册后有点出入(手册认为我们还在用32位的系统hhh),于是根据我在使用的环境重新完善/改写了实验手册:

实验环境: Ubuntu-22.04LTS

实验任务:

  • 实现系统调用的篡改;

实验原理:

  • 先来聊一聊系统调用的原理,这样下面每一步在干什么就很清晰明了了:篡改系统调用其实就是通过修改内核符号表,起到劫持的效果。因为系统调用实际上是触发了一个0x80的软中断,随后转到了系统调用处理程序的入口system_call()。system_call()会检查系统调用号来得出到底是调用哪种服务,然后会根据内核符号表跳转到所需要调用的内核函数的入口地址,所以如果我们这个时候修改了内核符号表,使其跳转到我们自己的函数上,就可以完成劫持。

    实验步骤:

  • 选定要修改的系统调用,查询该调用的linux编号并测试

    • 64位系统编号地址
    • 例如,我选定96号系统调用gettimeofday,它会返回当前的时间戳.
    • 测试代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <stdio.h>
      #include <sys/time.h>
      #include <unistd.h>
      int main(){
      struct timeval tv;
      syscall(96, &tv, NULL); // before modify syscall 96 :gettimeofday
      printf("tv_sec:%ld\n", tv.tv_sec);
      printf("tv_usec:%ld\n", tv.tv_usec);
      return 0;
      }
  • 篡改系统调用

    • 首先我们需要解除内核的位保护(内核默认不允许删除寄存器的写保护cr0。它会检查arch/x86/kernel/cpu/common.c:native_write_cr0),因此我们只需要在篡改前后分别接触保护即可(资料来源),具体的解除和恢复读写保护位的代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #define __force_order (*(volatile char*)0)
      inline void mywrite_cr0(unsigned long cr0) {
      asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
      }

      void enable_write_protection(void) {
      unsigned long cr0 = read_cr0();
      set_bit(16, &cr0);
      mywrite_cr0(cr0);
      }

      void disable_write_protection(void) {
      unsigned long cr0 = read_cr0();
      clear_bit(16, &cr0);
      mywrite_cr0(cr0);
      }
      值得注意的是,因为这涉及到修改操作系统内核模块,因此和常规意义上的编译程序不一样,具体可以参考网络上的教程
    • 编写一个内核模块以篡改系统调用,具体流程如下:

      1. 解除写保护
      2. 修改原本的系统调用的地址为我们自定义的函数的地址
      3. 恢复写保护

      同时不要忘了在模块的退出函数中恢复原本的系统调用,具体流程和上面一样。

  • 测试篡改后的系统调用
    • 例如在上面我们将96号系统调用修改为函数hello(a,b),返回a+b。在我们编译、加载完模块后,即可在用户空间通过syscall(96, arg1, arg2)的方式测试篡改是否成功
    • 显然,能写到这就说明上一步尝试没成功。在通过打印debug后发现用户空间的参数arg1和arg2没有传到系统调用中。因此只得修改原本的函数,尝试通过读取寄存器的方式获取参数:
      • 原本尝试直接读取寄存器register int a asm("rdi"),但是仍然没有传进内核空间,调查发现系统调用被执行时会触发软中断,当前寄存器的值会发生变化,因此获取不到正确的参数
      • 在得知第一个办法的错误原因后便去寻找linux提供的API,果不其然发现了一些很有用的办法,例如暴力出奇迹的copy_from_user(),亦如通过struct pt_regs直接读取到参数。

至此,这个小的实验就完成了,完整的代码可以在我的仓库看到。