IT技術互動交流平臺

Linux Rootkit 系列二:基于修改 sys_call_table 的系統調用掛鉤

作者:佚名  發布日期:2016-07-01 22:11:50

前言: 《Linux Rootkit 系列一: LKM的基礎編寫及隱藏》的作者似乎跑路了;留下的這個口鍋,我試著背一下。鑒于筆者知識能力上的不足,如有問題歡迎各位扔豆腐,不要砸磚頭。
與第一篇文章作者所想象的不同,本文不打算給大家介紹三種不同的系統調用掛鉤技術,相反,本文僅詳細講解最簡單的系統調用掛鉤方案,并且基于這個方案實現最基本的文件監視工具。這樣,既可以讓讀者輕松上手進行實際應用, 又可以加深、鞏固讀者對LKM 的理解,同時還免去了一次學習多種掛鉤方案的理論知識壓力。
所以,本文力求以實驗為核心,每一個步驟都可能有對應的實驗代碼。代碼倉庫: https://github.com/NoviceLive/research-rootkit 。代碼在最新的 64 比特 Arch 與Kali 上面測試正常。
測試建議: 不要在物理機測試!不要在物理機測試!不要在物理機測試!
如果讀者使用 tmux 或者類似的工具,則可以垂直分割你的終端窗口, 一個窗口開一個 sudo dmesg -C && dmesg -w,用于查看日志; 另一個窗口用來做其他操作,比如構建、加載內核模塊。 不用tmux 也沒關系,開兩個終端,各占半個屏幕。
第一部分:基于修改 sys_call_table 的系統調用掛鉤
在系統調用掛鉤技術中,最簡單、最流行的方案是修改sys_call_table, 成員類型為函數指針的一維數組。
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
 /*
  * Smells like a compiler bug -- it doesn't work
  * when the & below is removed.
  */
 [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include
};
要修改它,首先得拿到它在內存里的位置。 然后,由于sys_call_table所在的內存是有寫保護的, 所以我們需要先去掉寫保護,再做修改。
1. 獲得 sys_call_table 的內存地址
在綜合考量了幾種可選的獲取方案之后,筆者決定采用從內核起始地址開始暴力搜索內存空間的方案。 (但是這種方案有可能被欺騙 。)
其他可能的方案有,一,從/boot/System.map 中讀取,感興趣的讀者可以查閱 Hooking the Linux System CallTable, 這篇文章便是使用這種方案來獲取sys_call_table的地址的。
二,從使用了sys_call_table的某些未導出函數的機器碼里面進行特征搜索, 感興趣的讀者可以查閱Kernel-LandRootkits, 作者花了幾張 slides 闡述了如何從導出的函數中獲取使用了sys_call_table的未導出函數, 進而搜索那個未導出函數的機器碼, 得到sys_call_table的地址;等等。
值得指出的是, 感興趣的讀者在測試這些本文未涉及的方案時,如果遇到了疑惑或者困難,也可以與筆者聯系、交流。
直接看代碼。
unsigned long **
get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;
  for (;(unsigned long)entry 1) {
    if (entry[__NR_close] == (unsigned long *)sys_close) {
        return entry;
      }
  }
  return NULL;
}
PAGE_OFFSET是內核內存空間的起始地址。 因為sys_close是導出函數(需要指出的是, sys_open 、 sys_read 等并不是導出的),我們可以直接得到他的地址;因為系統調用號 (也就是sys_call_table這個一維數組的索引) 在同一ABI (x86跟 x64 不是同一 ABI)上具有高度的后向兼容性,更重要的是,我們可以直接使用這個索引(代碼中的 __NR_close )!
從內核內存的起始地址開始, 逐一嘗試每一個指針大小的內存:把它當成是sys_call_table的地址, 用某個系統調用的編號(也就是索引)訪問數組中的成員,如果訪問得到的值剛好是是這個系統調用號所對應的系統調用的地址,那么我們就認為當前嘗試的這塊指針大小的內存就是我們要找的sys_call_table的地址。
實驗效果如圖。

2. 關閉寫保護
寫保護指的是寫入只讀內存時出錯。 這個特性可以通過CR0寄存器控制:開啟或者關閉, 只需要修改一個比特,也就是從 0 開始數的第 16個比特。
看代碼。我們可以使用read_cr0 /write_cr0 來讀取 /寫入 CR0 寄存器,免去我們自己寫內聯匯編的麻煩。
函數原型。
static inline unsigned long read_cr0(void);
static inline void write_cr0(unsigned long x);
關閉寫保護的源代碼:將CR0 寄存器從 0開始數的第 16 個比特置為 0。
void
disable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  write_cr0(cr0);
}
開啟寫保護的源代碼:將CR0 寄存器從 0開始數的第 16 個比特置為 1。
void
enable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  write_cr0(cr0);
}
在設置或者清除某個比特,我們使用了set_bit與clear_bit。 它們是 Linux 內核提供給內核模塊使用的編程接口,簡單易懂,同時還免去了我們自己寫那種很難讀的位運算的痛苦。
函數原型。
static __always_inline void
set_bit(long nr, volatile unsigned long *addr);
static __always_inline void
clear_bit(long nr, volatile unsigned long *addr);
實驗結果截圖。

3. 修改 sys_call_table
一維數組賦值,當之無愧最簡單的方案。當然,我們需要先把真正的值保存好,以備后面之需。
disable_write_protection();
real_open = (void *)sys_call_table[__NR_open];
sys_call_table[__NR_open] = (unsigned long*)fake_open;

real_unlink = (void *)sys_call_table[__NR_unlink];
sys_call_table[__NR_unlink] = (unsigned long*)fake_unlink;
real_unlinkat = (void *)sys_call_table[__NR_unlinkat];
sys_call_table[__NR_unlinkat] = (unsigned long*)fake_unlinkat;
enable_write_protection();
4. 恢復
disable_write_protection();
sys_call_table[__NR_open] = (unsigned long*)real_open;
sys_call_table[__NR_unlink] = (unsigned long*)real_unlink;
sys_call_table[__NR_unlinkat] = (unsigned long*)real_unlinkat;
enable_write_protection();
第二部分:基于系統調用掛鉤的初級文件監視
監視文件的創建與刪除。 我們掛鉤sys_open,sys_unlink,sys_unlinkat這三個函數, 并且在我們的鉤子函數把操作到的文件名打印出來,然后把控制交給真正的系統調用處理。
1. sys_open 的鉤子函數: fake_open
考慮到在系統運行時,對文件的讀寫操作從未中斷,這里只打印了進行創建操作的文件名,準確地說是,sys_open 的 flags中包含 O_CREAT 。
asmlinkage long
fake_open(const char __user *filename, int flags, umode_t mode)
{
  if ((flags & O_CREAT) && strcmp(filename, "/dev/null") != 0) {
    printk(KERN_ALERT "open: %s ", filename);
  }
  return real_open(filename, flags, mode);
}
注:這里的strcmp也是內核提供的。
2. sys_unlink 與 sys_unlinkat 的鉤子函數: fake_unlink 與 fake_unlinkat
簡單處理,直接打印路徑名。
asmlinkage long
fake_unlink(const char __user *pathname)
{
  printk(KERN_ALERT "unlink: %s ", pathname);
  return real_unlink(pathname);
}
asmlinkage long
fake_unlinkat(int dfd, const char __user * pathname, int flag)
{
  printk(KERN_ALERT "unlinkat: %s ", pathname);
  return real_unlinkat(dfd, pathname, flag);
}
3. 測試我們的文件監視工具
初級的文件監視就到這了,以后我們在做進一步的改進與完善。
效果見下圖。

第三部分:參考資料與延伸閱讀
1. 參考資料
Linux Cross Reference
The Linux KernelAPI
How the Linux kernel handles a systemcall
CR0
2. 延伸閱讀
Hooking the Linux System CallTable
Kernel-LandRootkits
 
Tag標簽: 系統  
  • 專題推薦

About IT165 - 廣告服務 - 隱私聲明 - 版權申明 - 免責條款 - 網站地圖 - 網友投稿 - 聯系方式
本站內容來自于互聯網,僅供用于網絡技術學習,學習中請遵循相關法律法規
彩票联盟网站 马关县| 临澧县|