Linux sys_call_table變動檢測

Andrew.Hann發表於2017-03-13

catalogue

0. 引言
1. 核心ko timer定時器,檢測sys_call_table adress變動
2. 通過/dev/kmem獲取IDT adress
3. 比較原始的系統呼叫地址和當前核心態中的系統呼叫地址發現是否有sys_call_table hook行為

 

0. 引言

核心rookit通常以系統呼叫為攻擊目標,主要出於兩個原因

1. 在核心態劫持系統呼叫能以較小的代價控制整個系統,不必修太多東西 
2. 應用層大多數函式是一個或多個系統呼叫不同形式的封裝,更改系統呼叫意味著其上層所有的函式都會被欺騙

當前的系統呼叫地址儲存在系統呼叫表中,位於作業系統為核心保留的記憶體空間(虛擬地址最高1GB),系統呼叫入口地址的存放順序同/usr/include/asm/unistd.h中的排列順序,按系統呼叫號遞增9

Relevant Link:

http://www.blackhat.com/presentations/bh-europe-09/Lineberry/BlackHat-Europe-2009-Lineberry-code-injection-via-dev-mem-slides.pdf

 

1. 核心ko timer定時器,檢測sys_call_table adress變動

1. The module does a copy of the Syscall Table to save all syscalls pointers
2. After this first step, the module uses the kernel timer to check every X secondes the diff between the Syscall Table and the copy.
3. If a diff is found, the module creates a workqueue to execute the python script and restore the good syscall pointer. 

The python script is executed with root creds and the syscall number which is hooked, is passed as the first argument of script (sys.argv[1]).

0x1: hook_detection.c

/*
**  Copyright (C) 2013 - Jonathan Salwan - http://twitter.com/JonathanSalwan
** 
**  This program is free software: you can redistribute it and/or modify
**  it under the terms of the GNU General Public License as published by
**  the Free Software Foundation, either version 3 of the License, or
**  (at your option) any later version.
** 
**  This program is distributed in the hope that it will be useful,
**  but WITHOUT ANY WARRANTY; without even the implied warranty of
**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**  GNU General Public License for more details.
**
**  You should have received a copy of the GNU General Public License
**  along with this program.  If not, see <http://www.gnu.org/licenses/>.
**
**  For more information about this module, 
**  see : http://shell-storm.org/blog/Simple-Hook-detection-Linux-module/
**
*/

#include <asm/uaccess.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/syscalls.h>
#include <linux/timer.h>
#include <linux/workqueue.h>

#define PATH_PYTHON "/usr/bin/python2.7"
#define PATH_SCRIPT "/opt/scripts/hook_detected.py"

#define TIME_SLEEP 30000 /* in msec */

static struct timer_list timer_s;
static struct workqueue_struct *wq;
static unsigned int syscall_table_size;
static unsigned long *addr_syscall_table;
static unsigned long *dump_syscall_table;

static int exec_python_script(unsigned int sys_num)
{
  char s_num[32];
  char *argv[] = {PATH_PYTHON, PATH_SCRIPT, s_num, NULL};
  static char *envp[] = {"HOME=/", "TERM=linux", "PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL};
  struct subprocess_info *sub_info;

  sprintf(s_num, "%d", sys_num);
  sub_info = call_usermodehelper_setup(argv[0], argv, envp, GFP_ATOMIC);
  if (sub_info == NULL)
    return -ENOMEM;
  call_usermodehelper_exec(sub_info, UMH_WAIT_PROC);
  return 0;
}

static unsigned long *get_syscalls_table(void)
{
  unsigned long *start;

  /* hack :/ */
  for (start = (unsigned long *)0xc0000000; start < (unsigned long *)0xffffffff; start++)
    if (start[__NR_close] == (unsigned long)sys_close){
      return start;
    }
  return NULL;
}

static unsigned int get_size_syscalls_table(void)
{
  unsigned int size = 0;

  while (addr_syscall_table[size++]);
  return size * sizeof(unsigned long *);
}

static void check_diff_handler(struct work_struct *w)
{
  unsigned int sys_num = 0;

  while (addr_syscall_table[sys_num]){
    if (addr_syscall_table[sys_num] != dump_syscall_table[sys_num]){
      printk(KERN_INFO "hook_detection: Hook detected ! (syscall %d)\n", sys_num);
      write_cr0(read_cr0() & (~0x10000));
      addr_syscall_table[sys_num] = dump_syscall_table[sys_num];
      write_cr0(read_cr0() | 0x10000);
      exec_python_script(sys_num);
      printk(KERN_INFO "hook_detection: syscall %d is restored\n", sys_num);
    }
    sys_num++;
  }
}
static DECLARE_DELAYED_WORK(check_diff, check_diff_handler);

static void timer_handler(unsigned long data)
{
  unsigned long onesec;

  onesec = msecs_to_jiffies(1000);
  queue_delayed_work(wq, &check_diff, onesec);
  if (mod_timer(&timer_s, jiffies + msecs_to_jiffies(TIME_SLEEP)))
    printk(KERN_INFO "hook_detection: Failed to set timer\n");
}

static int __init hook_detection_init(void)
{
  addr_syscall_table = get_syscalls_table();
  if (!addr_syscall_table){
    printk(KERN_INFO "hook_detection: Failed - Address of syscalls table not found\n");
    return -ECANCELED;
  }

  syscall_table_size = get_size_syscalls_table();
  dump_syscall_table = kmalloc(syscall_table_size, GFP_KERNEL);
  if (!dump_syscall_table){
    printk(KERN_INFO "hook_detection: Failed - Not enough memory\n");
    return -ENOMEM;
  }
  memcpy(dump_syscall_table, addr_syscall_table, syscall_table_size);

  wq = create_singlethread_workqueue("hook_detection_wq");

  setup_timer(&timer_s, timer_handler, 0);
  if (mod_timer(&timer_s, jiffies + msecs_to_jiffies(TIME_SLEEP))){
    printk(KERN_INFO "hook_detection: Failed to set timer\n");
    return -ECANCELED;
  }

  printk(KERN_INFO "hook_detection: Init OK\n");
  return 0;
}

static void __exit hook_detection_exit(void)
{
  if (wq)
    destroy_workqueue(wq);
  kfree(dump_syscall_table);
  del_timer(&timer_s);
  printk(KERN_INFO "hook_detection: Exit\n");
}

module_init(hook_detection_init);
module_exit(hook_detection_exit);

MODULE_AUTHOR("Jonathan Salwan");
MODULE_DESCRIPTION("Hook Detection");
MODULE_LICENSE("GPL");

Relevant Link:

http://shell-storm.org/blog/Simple-Hook-detection-Linux-module/hook_detection.c
http://shell-storm.org/blog/Simple-Hook-detection-Linux-module/

 

2. 通過/dev/kmem獲取IDT adress

核心邏輯是通過idt獲取80中斷的地址,獲取了sys_call_table之後,進行一次拷貝,之後就可以進行diff對比

#include < stdio.h >
#include < sys/types.h >
#include < fcntl.h >
#include < stdlib.h >

int kfd;

 struct
 {
      unsigned short limit;
      unsigned int base;
 } __attribute__ ((packed)) idtr;

 struct
 {
      unsigned short off1;
      unsigned short sel;
      unsigned char none, flags;
       unsigned short off2;
 } __attribute__ ((packed)) idt;


 int readkmem (unsigned char *mem, 
              unsigned off, 
              int bytes)
 {
      if (lseek64 (kfd, (unsigned long long) off, 
                                    SEEK_SET) != off)
      {
                 return -1;
      }

      if (read (kfd, mem, bytes) != bytes) 
     {
             return -1;
     }

 }
 
 int main (void)
 {
      unsigned long sct_off;
      unsigned long sct;
       unsigned char *p, code[255];
      int i;


/* request IDT and fill struct */



    asm ("sidt %0":"=m" (idtr));

    if ((kfd = open ("/dev/kmem", O_RDONLY)) == -1)
    {
                perror("open");
            exit(-1);
    }
    
    if (readkmem ((unsigned char *)&idt, 
               idtr.base + 8 * 0x80, sizeof (idt)) == -1)
    {
            printf("Failed to read from /dev/kmem\n");
            exit(-1);
    }
    
    sct_off = (idt.off2 < < 16) | idt.off1;

    if (readkmem (code, sct_off, 0x100) == -1)
    {
            printf("Failed to read from /dev/kmem\n");
            exit(-1);
    }

/* find the code sequence that calls SCT */


 sct = 0;
    for (i = 0; i < 255; i++)
    {
            if (code[i] == 0xff && code[i+1] == 0x14 && 
                                        code[i+2] == 0x85)
                    sct = code[i+3] + (code[i+4] < < 8) + 
                        (code[i+5] < < 16) + (code[i+6] < < 24);
    }
    if (sct)
            printf ("sys_call_table: 0x%x\n", sct);
    close (kfd);
 }

Relevant Link:

http://www.rootkitanalytics.com/kernelland/IDT-dev-kmem-method.php
http://www.phpweblog.net/GaRY/archive/2007/06/17/get_sys_call_table_address.html

 

3. 比較原始的系統呼叫地址和當前核心態中的系統呼叫地址發現是否有sys_call_table hook行為 

原始的系統呼叫地址在核心編譯階段被指定,不會更改,通過比較原始的系統呼叫地址和當前核心態中的系統呼叫地址我們就可以發現系統呼叫有沒有被更改。原始的系統呼叫地址在編譯階段被寫入兩個檔案

1. System.map: 該檔案包含所有的符號地址,系統呼叫也包含在內
2. vmlinux-2.4.x: 系統初始化時首先被讀入記憶體的核心映像檔案

vmlinux-2.4.x檔案通常以壓縮的格式存放在/boot目錄下,所以在比較之前必須解壓這個檔案,另一個問題是: 我們的比較的前提是假設system.map及vmlinuz image都沒有被入侵者更改,所以更安全的做法是在系統乾淨時已經建立這兩個檔案的可信任的拷貝,並建立檔案的md5 hash

在大多數被裝載核心後門情況中,核心在系統初始化之後才被更改,更改發生在載入了rootkit的module或者被植入直接讀寫/dev/kmem的on-the-fly kernel patch之後。而通常情況下rootkit並不更改vmlinuz和system.map這兩個檔案,所以列印這兩個檔案中的符號地址就可以知道系統原始的系統呼叫地址,系統當前執行中的系統呼叫地址(可能被更改)可以同過/proc下的kcore檔案得到,比較兩者就知道結果

0x1: 獲取原始核心系統呼叫函式地址

/boot/System.map-3.10.0-327.el7.x86_64

0x2: 獲取當前核心系統呼叫地址

cat /proc/kallsyms 

0x3: 對比區別

從裡面列舉sys_call_table的function point地址
/boot/System.map-3.10.0-327.el7.x86_64

和
cat /proc/kallsyms | grep sys_fork
進行diff對比 

cat System.map-4.4.0-53-generic | grep sys_fork

這裡遇到一個問題,ubuntu對sys_socket相關的系統呼叫會進行核心地址重定位,因此需要對檢測結果進行一個誤報過濾,看是否所有的函式的gap都相同,如果相同,則說明是系統自己的function address relocation行為

def hex2dec(string_num):
    return str(int(string_num.upper(), 16))

def check_rookit(diff_func_table):
    last_func_address_gap = 0
    cur_func_address_gap = 0
    for item in diff_func_table:
        cur_func_address_gap = abs(int(hex2dec(item['cur_funcaddress'])) - int(hex2dec(item['ori_funcaddress'])))
        if last_func_address_gap != 0 and cur_func_address_gap != last_func_address_gap:
            return True
        else:
            last_func_address_gap = cur_func_address_gap
    return False

Relevant Link:

http://www.xfocus.net/articles/200411/754.html
http://www.magicsite.cn/blog/Linux-Unix/Linux/Linux41576.html
http://blog.csdn.net/tommy_wxie/article/details/8039695

Copyright (c) 2017 LittleHann All rights reserved

相關文章