背景
前段時間,我們的項目組在幫客戶解決一些操作系統安全領域的問題,涉及到windows,Linux,macOS三大操作系統平臺。無論什么操作系統,本質上都是一個軟件,任何軟件在一開始設計的時候,都不能百分之百的滿足人們的需求,所以操作系統也是一樣,為了盡可能的滿足人們需求,不得不提供一些供人們定制操作系統的機制。當然除了官方提供的一些機制,也有一些黑魔法,這些黑魔法不被推薦使用,但是有時候面對具體的業務場景,可以作為一個參考的思路。
Linux中常見的攔截過濾
本文著重介紹Linux平臺上常見的攔截:
動態庫劫持
Linux上的動態庫劫持主要是基于LD_ PRELOAD環境變量,這個環境變量的主要作用是改變動態庫的加載順序,讓用戶有選擇的載入不同動態庫中的相同函數。但是使用不當就會引起嚴重的安全問題,我們可以通過它在主程序和動態連接庫中加載別的動態函數,這就給我們提供了一個機會,向別人的程序注入惡意的代碼。
假設有以下用戶名密碼驗證的函數:
#include <stdio.h>#include <string.h>#include <stdlib.h>int main(int argc, char **argv){char passwd[] = "password";if (argc < 2) {printf("Invalid argc!/n");return;}if (!strcmp(passwd, argv[1])) {printf("Correct Password!/n");return;}printf("Invalid Password!/n");}
我們再寫一段hookStrcmp的程序,讓這個比較永遠正確。
#include <stdio.h>int strcmp(const char *s1, const char *s2){/* 永遠返回0,表示兩個字符串相等 */return 0;}
依次執行以下命令,就會使我們的hook程序先執行。
gcc -Wall -fPIC -shared -o hookStrcmp.so hookStrcmp.cexport LD_PRELOAD=”./hookStrcmp.so”
結果會發現,我們自己寫的strcmp函數優先被調用了。這是一個最簡單的劫持 ,但是如果劫持了類似于geteuid/getuid/getgid,讓其返回0,就相當于暴露了root權限。所以為了安全起見,一般將LD_ PRELOAD環境變量禁用掉。
Linux系統調用劫持
最近發現在4.4.0的內核中有513多個系統調用(很多都沒用過),系統調用劫持的目的是改變系統中原有的系統調用,用我們自己的程序替換原有的系統調用。Linux內核中所有的系統調用都是放在一個叫做sys_ call _table的內核數組中,數組的值就表示這個系統調用服務程序的入口地址。整個系統調用的流程如下:
當用戶態發起一個系統調用時,會通過80軟中斷進入到syscall hander,進而進入全局的系統調用表sys_ call _table去查找具體的系統調用,那么如果我們將這個數組中的地址改成我們自己的程序地址,就可以實現系統調用劫持。但是內核為了安全,對這種操作做了一些限制:
對于以上兩個問題,解決方案如下(方法不止一種):
/* make the page writable */int make_rw(unsigned long address){unsigned int level;pte_t *pte = lookup_address(address, &level);//查找虛擬地址所在的頁表地址pte->pte |= _PAGE_RW;//設置頁表讀寫屬性return 0;}
/* make the page write protected */int make_ro(unsigned long address){unsigned int level;pte_t *pte = lookup_address(address, &level);pte->pte &= ~_PAGE_RW;//設置只讀屬性return 0;}
開始替換系統調用
本文實現的是對 ls這個命令對應的系統調用,系統調用號是 _ NR _getdents。
static int syscall_init_module(void){orig_getdents = sys_call_table[__NR_getdents];make_rw((unsigned long)sys_call_table); //修改頁屬性sys_call_table[__NR_getdents] = (unsigned long *)hacked_getdents; //設置新的系統調用地址make_ro((unsigned long)sys_call_table);return 0;}
恢復原狀
static void syscall_cleanup_module(void){printk(KERN_ALERT "Module syscall unloaded./n");make_rw((unsigned long)sys_call_table);sys_call_table[__NR_getdents] = (unsigned long *)orig_getdents;make_ro((unsigned long)sys_call_table);}
使用Makefile編譯,insmod插入內核模塊后,再執行ls時,就會進入到我們的系統調用,我們可以在hook代碼中刪掉某些文件,ls就不會顯示這些文件,但是這些文件還是存在的。
堆棧式文件系統
Linux通過vfs虛擬文件系統來統一抽象具體的磁盤文件系統,從上到下的IO棧形成了一個堆棧式。通過對內核源碼的分析,以一次讀操作為例,從上到下所執行的流程如下:
內核中采用了很多c語言形式的面向對象,也就是函數指針的形式,例如read是vfs提供用戶的接口,具體底下調用的是ext2的read操作。我們只要實現VFS提供的各種接口,就可以實現一個堆棧式文件系統。Linux內核中已經集成了一些堆棧式文件系統,例如Ubuntu在安裝時會提醒你是否需要加密home目錄,其實就是一個堆棧式的加密文件系統(eCryptfs),原理如下:
實現了一個堆棧式文件系統,相當于所有的讀寫操作都會進入到我們的文件系統,可以拿到所有的數據,就可以進行做一些攔截過濾。
以下是我實現的一個最簡單的堆棧式文件系統,實現了最簡單的打開、讀寫文件,麻雀雖小但五臟俱全。
https://github.com/wangzhangjun/wzjfs
inline hook
我們知道內核中的函數不可能把所有功能都在這個函數中全部實現,它必定要調用它的下層函數。如果這個下層函數可以得到我們想要的過濾信息內容,就可以把下層函數在上層函數中的offset替換成新的函數的offset,這樣上層函數調用下層函數時,就會跳到新的函數中,在新的函數中做過濾和劫持內容的工作。所以從原理上來說,inline hook可以想hook哪里就hook哪里。
inline hook 有兩個重要的問題:
對于第一個問題:
需要有一點的內核源碼經驗,比如說對于read操作,源碼如下:
在這里當發起read系統調用后,就會進入到sys read,在sys read中會調用vfs read函數,在vfs read的參數中正好有我們需要過濾的信息,那么就可以把vfs_ read當做一個hook點。
對于第二個問題:
如何Hook?這里介紹兩種方式:
第一種方式:直接進行二進制替換,將call指令的操作數替換為hook函數的地址。
第二種方式:Linux內核提供的kprobes機制。
其原理是在hook點注入int 3(x86)的機器碼,讓cpu運行到這里的時候會觸發sig trap信號,然后將用戶自定義的hook函數注入到sig trap的回調函數中,達到觸發hook函數的目的。這個其實也是調試器的原理。
LSM
LSM是Linux Secrity Module的簡稱,即linux安全模塊。是一種通用的Linux安全框架,具有效率高,簡單易用等特點。原理如下:
LSM
在內核中做了以下工作:
適用場景
對于以上幾種Hook方式,有其不同的應用場景。
總結
篇幅有限,本文只是介紹了Linux上的攔截技術,后續有機會可以一起探討windows和macOS上的攔截技術。事實上類似的審計HOOK放到任何一個系統中都是剛需,不只是kernel,我們可以看到越來越多的vm和runtime甚至包括很多web組件、前端應用都提供了更靈活的hook方式,這是透明化和實時性兩個安全大趨勢下最常見的解決方案。
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對VEVB武林網的支持。
新聞熱點
疑難解答
圖片精選