如何用Ptrace攔截並模擬Linux系統調用
ptrace (process trace)系統調用通常與調試相關,它是本地調試器監視類unix系統上調試的主要機制,同時也是實現strace系統調用跟蹤的常用方法。使用Ptrace,跟蹤器可以暫停跟蹤、檢查和設置寄存器和內存、監視系統調用,甚至攔截系統調用。
通過攔截,跟蹤器可以改變系統調用參數,改變系統調用返回值,甚至阻止某些系統調用。這意味著跟蹤器可以完全服務於系統調用本身,這特別有趣,因為它還意味著跟蹤器可以模擬整個操作系統,而且是在Ptrace之外的內核沒有任何特殊幫助的情況下完成的。
不過也有其他問題出現,比如一個進程一次只能連接一個跟蹤器,因此不可能在模擬一個操作系統的同時,也用GDB之類的工具對該進程進行調試,還有就是模擬系統調用的成本會更高。
我將在本文重點介紹x86-64上的Linux的Ptrace,並將利用一些特定的Linux擴展。不過為了節省篇幅,我省略了錯誤檢查的過程,但是完整的源代碼清單將包含這些檢查。
點擊以下鏈接,你可以找到運行代碼:https://github.com/skeeto/ptrace-examples
strace
讓我先回顧一下strace的一個基本實現過程,按照strace官網的描述, strace是一個可用於診斷、調試和教學的Linux用戶空間跟蹤器。我用它來監控用戶空間進程和內核的交互,比如系統調用、信號傳遞、進程狀態變更等。strace底層使用內核的ptrace特性來實現其功能,它能作為一種動態跟蹤工具,能夠幫助運維高效地定位進程和服務故障。它像是一個偵探,通過系統調用的蛛絲馬跡,告訴你異常的真相。
雖然Ptrace從未被標準化,但它的介面在不同的操作系統中是相似的,特別是在其核心功能上。ptrace原型通常看起來如下所示,儘管具體的類型可能不同。
long ptrace(int request, pid_t pid, void *addr, void *data);
Pid代表tracee的進程ID,雖然tracee一次只能連接一個跟蹤器,但是一個跟蹤器可以同時連接到許多跟蹤器。
請求欄位選擇一個特定的Ptrace函數,就像ioctl介面一樣。對於strace,只需要3個運行參數:
·PTRACE_TRACEME:此進程由其父進程跟蹤。
·PTRACE_SYSCALL:繼續,但在下一個系統調用入口或出口處停止。
·PTRACE_GETREGS:獲取tracee寄存器的副本。
另外兩個欄位addr和data作為所選Ptrace函數的泛型參數,經常被省略,如果是這樣,我可以輸入0。
strace介面實質上是另一個命令的前綴:
$ strace [strace options] program [arguments]
由於最簡單的strace命令沒有任何選項,所以要做的第一件事,就是假設它至少有一個參數是fork和exec,exec是argv末尾的tracee進程。但是在載入目標程序之前,新進程將通知內核它將被它的父進程跟蹤,此時這個Ptrace系統調用將暫停tracee。
pid_t pid = fork();switch (pid) {
case -1: /* error */
FATAL("%s", strerror(errno));
case 0: /* child */
ptrace(PTRACE_TRACEME, 0, 0, 0);
execvp(argv[1], argv + 1);
FATAL("%s", strerror(errno));}
父進程使用wait等待子進程的PTRACE_TRACEME。當wait返回時,將暫停子進程。
waitpid(pid, 0, 0);
在允許子進程繼續之前,我會告訴操作系統應該終止tracee及其父進程。真正的strace實現可能需要設置其他選項,比如PTRACE_O_TRACEFORK。
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_EXITKILL);
剩下的就是一個簡單的循環過程,該過程一次只能捕獲一個系統調用。具體步驟如下:
1.等待進程進入下一個系統調用;
2.輸出系統調用代理;
3.允許系統調用執行並等待返回;
4.輸出系統調用返回值;
PTRACE_SYSCALL請求用於等待下一個系統調用開始,以及等待系統調用退出。前面說過,需要wait來等待tracee進入所需的狀態。
ptrace(PTRACE_SYSCALL, pid, 0, 0);waitpid(pid, 0, 0);
當wait返回時,進行系統調用的線程的寄存器中有系統調用號及其參數。但是,此時操作系統還沒有為這個系統調用提供服務,這個細節會很重要,稍後再說。
接著是收集系統調用信息,在x86-64上,系統調用號在rax中傳遞,參數(最多6個)在rdi、rsi、rdx、r10、r8和r9中傳遞。儘管不需要wait,讀取寄存器則是另一個Ptrace調用,因為tracee沒有改變狀態。
struct user_regs_struct regs;ptrace(PTRACE_GETREGS, pid, 0, ®s);long syscall = regs.orig_rax;fprintf(stderr, "%ld(%ld, %ld, %ld, %ld, %ld, %ld)",
syscall,
(long)regs.rdi, (long)regs.rsi, (long)regs.rdx,
(long)regs.r10, (long)regs.r8, (long)regs.r9);
有一點需要注意,出於內部內核的目的,系統調用號會存儲在orig_rax而不是rax中,所有其他系統調用參數都很簡單。
接下來是另一個PTRACE_SYSCALL和wait,然後是另一個PTRACE_GETREGS來獲取結果,結果存儲在rax中。
ptrace(PTRACE_GETREGS, pid, 0, ®s);fprintf(stderr, " = %ld
", (long)regs.rax);
這個輸出非常簡單,系統調用沒有符號名稱,並且每個參數都以數字方式輸出,即使它是指向緩衝區的指針。而更完整的strace則知道哪些參數是指針,並使用process_vm_readv從tracee中讀取這些緩衝區,以便正確地輸出它們。
然而,這個簡單地輸出確實為系統調用攔截打下了一個基礎。
系統調用攔截
假設我希望使用Ptrace來實現OpenBSD的pledge,其中一個進程只保證使用一組受限的系統調用。許多程序通常都有一個初始化階段,其中需要大量的系統訪問(打開文件、綁定套接字等)。初始化之後,它們進入一個主循環,在主循環中處理輸入信息,此時只需要少量的系統調用。
在進入此主循環之前,進程可以將自身限制為所需的少數操作。如果程序有一個允許被錯誤輸入利用的漏洞,那麼一個進程只保證使用一組受限的系統調用的思想,會極大地限制系統調用攔截。
使用相同的strace模型,而不是輸出所有系統調用,我可以阻止某些系統調用,或者在tracee發生故障時終止它。終止很容易的,只需在跟蹤器中調用exit。因為exit也會終止tracee,所以阻止系統調用並允許子調用繼續進行操作是一個比較棘手的問題。
如果一旦系統調用啟動,就無法中止它。當跟蹤器從系統調用入口處的wait返回時,阻止系統調用發生的惟一方法是終止跟蹤器。
然而,我不僅可以篡改系統調用參數,還可以更改系統調用號,將其轉換為不存在的系統調用。作為回報,我可以通過正常的帶內信令報告errno中的EPERM錯誤。
for (;;) {
/* Enter next system call */
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, 0, 0);
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, 0, ®s);
/* Is this system call permitted? */
int blocked = 0;
if (is_syscall_blocked(regs.orig_rax)) {
blocked = 1;
regs.orig_rax = -1; // set to invalid syscall ptrace(PTRACE_SETREGS, pid, 0, ®s);
}
/* Run system call and stop on exit */
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, 0, 0);
if (blocked) {
/* errno = EPERM */
regs.rax = -EPERM; // Operation not permitted ptrace(PTRACE_SETREGS, pid, 0, ®s);
}}
這個簡單的示例只檢查了白名單或系統調用的黑名單,更多細緻的檢查還沒有,比如為什麼允許文件以只讀方式打開(open)而不是以可寫方式打開,為什麼允許匿名內存映射但不允許非匿名映射等等。
創建一個人工系統調用
我將新創建的pledge系統調用,命名為xpledge(),系統調用號為10000,以區別於真正的系統調用。
#define SYS_xpledge 10000
為了演示,我構建了一個非常小的介面,這在實踐中並不是很好用。它與使用字元串介面的OpenBSD pledge沒有什麼共同之處。實際上,設計強大且安全的許可權集是非常複雜的。以下是tracee系統調用的整個介面和實現:過程
#define _GNU_SOURCE#include
#define XPLEDGE_RDWR (1
#define xpledge(arg) syscall(SYS_xpledge, arg)
如果參數為0,則只允許幾個基本的系統調用,包括用於分配內存的系統調用(例如brk)。PLEDGE_RDWR位允許各種讀寫系統調用(read、readv、pread、preadv等),PLEDGE_OPEN允許open。
為了防止許可權被重新升級,pledge()會阻止自身的運行。在xpledge跟蹤器中,我只需要檢查以下的系統調用。
/* Handle entrance */switch (regs.orig_rax) {
case SYS_pledge:
register_pledge(regs.rdi);
break;}
操作系統將返回ENOSYS(函數未實現),因為這不是真正的系統調用。在退出的過程中,我將它重寫為success(0)。
/* Handle exit */switch (regs.orig_rax) {
case SYS_pledge:
ptrace(PTRACE_POKEUSER, pid, RAX * 8, 0);
break;}
我編寫了一個打開/dev/urandom進行讀取的小測試程序,該程序會先嘗試pledge,然後再嘗試打開/dev/urandom,最後確認它可以從原始的/dev/urandom文件描述符中進行讀取。在沒有pledge跟蹤器的情況下運行時,輸出如下:
$ ./example
fread("/dev/urandom")[1] = 0xcd2508c7
XPledging...
XPledge failed: Function not implemented
fread("/dev/urandom")[2] = 0x0be4a986
fread("/dev/urandom")[1] = 0x03147604
進行無效的系統調用不會使應用程序崩潰,只會發生調用失敗。在跟蹤器下運行時,它看起來像這樣:
$ ./xpledge ./example
fread("/dev/urandom")[1] = 0xb2ac39c4
XPledging...
fopen("/dev/urandom")[2]: Operation not permitted
fread("/dev/urandom")[1] = 0x2e1bd1c4
Pledge雖然是成功的,但是第二個fopen並沒有出現,這是因為跟蹤器的EPERM阻止了它。
可以進一步設想,假如更改文件路徑或返回虛假結果,跟蹤器是否可以更改它的跟蹤對象,並在通過系統調用的任何路徑的根目錄中添加一些跟蹤路徑,甚至欺騙進程,偽裝成根用戶運行。事實上,這正是Fakeroot NG程序的工作原理。
模擬系統
如果你的目的是攔截所有系統調用,由於已經有一個二進位程序要在另一個操作系統上運行,所以它調用的任何系統都不會工作。
雖然你可以使用我以上所描述的方法來管理所有系統調用,然而跟蹤器總是將系統調用號替換為一個假的調用號,為系統調用本身提供服務,這種方式效率低下。自2005年以來, PTrace出現,PTrace每次系統調用只停止一次,並且在允許tracee繼續之前由跟蹤器來為該系統調用提供服務。
for (;;) {
ptrace(PTRACE_SYSEMU, pid, 0, 0);
waitpid(pid, 0, 0);
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, 0, ®s);
switch (regs.orig_rax) {
case OS_read:
/* ... */
case OS_write:
/* ... */
case OS_open:
/* ... */
case OS_exit:
/* ... */
/* ... and so on ... */
}}
要從具有穩系統調用ABI的任何系統運行相同系統結構的二進位文件,只需使用PTRACE_SYSEMU跟蹤器、一個載入程序(取代exec)以及二進位文件所需的任何系統庫(只運行靜態二進位文件也可以)。


※淡出視線不意味威脅消除:針對主流漏洞利用工具包的分析
※Tick組織通過破壞USB驅動來攻擊被隔離的系統
TAG:嘶吼RoarTalk |