Syscall-Hijacking

Linux的系统调用过程

  • 用户空间到内核空间的转换阶段

    • 需要一个指令来完成,也称为操作系统陷入(operating system trap)指令。

    • Linux 通过软中断指令。X86架构:软中断 0x80,即 int $0x80汇编指令。

    • 通过软中断,系统跳转到一个预设的内核空间地址,它指向了系统调用处理程序 (不要和系统调用服务例程混淆),即在arch/i386/kernel/entry.S文件中使用汇编语言编写的system_call函数

  • 系统调用处理函数system_call到系统调用服务例程的阶段

    • 所有的系统调用都会统一跳转到这个地址进而执行system_call 函数,system_call 要把这些系统调用派发到各自的服务例程。

    • 从0x80中断处理程序分析,获取系统调用表的地址。

获得系统调用表

  • 通过简单的搜索找到call指令,从而得到sys_call_table的地址:
1
2
3
4
5
6
7
8
9
10
11
unsigned int get_sys_call_table_entry(unsigned int sys_call_entry,char * exp,char exp_len,unsigned int cope)
{
char * begin=sys_call_entry;
char * end=sys_call_entry+cope;
for(;begin<end;begin++)
{
if(begin[0]==exp[0]&&begin[1]==exp[1]&&begin[2]==exp[2])
return *((unsigned int *)(begin+3));
}
return 0;
}

Rootkit

定义

  • Rootkit是一种特殊的恶意软件,它的功能是在安装目标上隐藏自身及指定的文件、进程和网络链接等信息.
  • Rootkit通过加载特殊的驱动,修改系统内核,进而达到隐藏信息的目的。
    • 攻击者通过远程攻击获得root访问权限;
    • 攻击者会在侵入的主机中安装rootkit;
    • 经常通过rootkit的后门检查系统是否有其他的用户登录,如果只有自己,攻击者就开始着手清理日志中的有关信息。

分类:

  • 应用级:通过替换login、ps、ls、netstat等系统工具,或修改.rhosts等系统配置文件等实现隐藏及后门

  • 硬件级:bios rootkit,可以在系统加载前获得控制权,通过向磁盘中写入文件,再由引导程序加载该文件重新获得控制权,也可以采用虚拟机技术,使整个操作系统运行在rootkit掌握之中;

  • 内核级 :最常见

    -  lkm rootkit:基于lkm技术,通过系统提供的接口加载到内核空间,成为内核的一部分,进而通过hook系统调用等技术实现隐藏、后门功能。
    
    -  非lkm rootkit:在系统不支持lkm机制时修改内核的一种方法,主要通过/dev/mem、/dev/kmem设备直接操作内存,从而对内核进行修改。 
    

功能

  • 隐藏文件:笔者将会利用此实现对ls 系统命令返回结果的篡改
  • 隐藏进程
  • 隐藏连接:笔者将会利用此实现对netstat 系统命令返回结果的篡改
  • 隐藏模块
  • 嗅探工具
  • 密码记录
  • 日志擦除
  • 内核后门

系统调用劫持

在前述基础上,我们已经可以就行系统调用劫持,也即将系统调用转移到我们自己提供的函数,而不是原始的系统调用。

此处以ps命令为例。

  • 为了实现对诸如ps 命令的返回结果的修改,我们首先需要知道这些命令使用了哪些系统调用来实现。

    • 在linux下,我们可以使用strace命令来跟踪一个用户程序在执行过程中所使用的系统调用。在linux 终端输入:strace ps aux 2>out.txt
    • 查询ps命令所执行的系统调用,结果如下:

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      ......
      open("/proc/meminfo", O_RDONLY) = 4
      lseek(4, 0, SEEK_SET) = 0
      read(4, "MemTotal: 495788 kB\nMemF"..., 2047) = 1114
      stat("/proc/self/task", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
      openat(AT_FDCWD,"/proc",O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 5
      mmap(NULL,135168,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5330b72000
      mmap(NULL,135168,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5330b51000
      getdents(5, /* 140 entries */, 32768) = 3696
      stat("/proc/1", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
      open("/proc/1/stat", O_RDONLY) = 6
      read(6, "1 (init) S 0 1 1 0 -1 4202752 22"..., 4095) = 326
      close(6)
      ......
  • 其中,我们看到ps命令执行过程中调用了getdents这个函数,而ps命令正是使用这个函数来读取/proc目录下的进程文件的。其对应的系统调用是sys_getdents(),其函数原型如下:

    • 1
      int sys_getdents(unsigned int fd, struct linux_dirent64 __user *dirp, unsigned int count)
  • 其中,fd为指向目录文件的文件描述符,该函数根据fd所指向的目录文件读取相应dirent结构,并放入dirp中,其中count为dirp中返回的数据量,正确时该函数返回值为填充到dirp的字节数。

  • 我们要做的就是代替原先的系统调用,使用自己定义的系统调用hacked_getdents(),加上我们自己的判断语句就能实现对进程文件的过滤。

  • 这里我们采用编写一个内核模块(LKM),通过获得系统调用表的地址来替换原先的系统调用的方法。

    • 内核模块是一些可以让操作系统内核在需要时载入和执行的代码,这意味着它可以在不需要时由操作系统卸载。它们扩展了操作系统内核的功能却不需要重新编译内核,因此内核模块机制的好处是:既避免了内核的臃肿不堪,又极大程度地提高了内核的可扩展性。