這篇文章將為大家詳細(xì)講解有關(guān)怎么使用Ptrace去攔截和仿真Linux系統(tǒng)調(diào)用,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
十載的甌海網(wǎng)站建設(shè)經(jīng)驗(yàn),針對(duì)設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對(duì)一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。全網(wǎng)整合營(yíng)銷推廣的優(yōu)勢(shì)是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整甌海建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。創(chuàng)新互聯(lián)從事“甌海網(wǎng)站設(shè)計(jì)”,“甌海網(wǎng)站推廣”以來,每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
ptrace(2)
(“進(jìn)程跟蹤”)系統(tǒng)調(diào)用通常都與調(diào)試有關(guān)。它是類 Unix 系統(tǒng)上通過原生調(diào)試器監(jiān)測(cè)被調(diào)試進(jìn)程的主要機(jī)制。它也是實(shí)現(xiàn) strace(系統(tǒng)調(diào)用跟蹤)的常見方法。使用 Ptrace,跟蹤器可以暫停被跟蹤進(jìn)程,檢查和設(shè)置寄存器和內(nèi)存,監(jiān)視系統(tǒng)調(diào)用,甚至可以攔截系統(tǒng)調(diào)用。
通過攔截功能,意味著跟蹤器可以篡改系統(tǒng)調(diào)用參數(shù),篡改系統(tǒng)調(diào)用的返回值,甚至阻塞某些系統(tǒng)調(diào)用。言外之意就是,一個(gè)跟蹤器本身完全可以提供系統(tǒng)調(diào)用服務(wù)。這是件非常有趣的事,因?yàn)檫@意味著一個(gè)跟蹤器可以仿真一個(gè)完整的外部操作系統(tǒng),而這些都是在沒有得到內(nèi)核任何幫助的情況下由 Ptrace 實(shí)現(xiàn)的。
問題是,在同一時(shí)間一個(gè)進(jìn)程只能被一個(gè)跟蹤器附著,因此在那個(gè)進(jìn)程的調(diào)試期間,不可能再使用諸如 GDB 這樣的工具去仿真一個(gè)外部操作系統(tǒng)。另外的問題是,仿真系統(tǒng)調(diào)用的開銷非常高。
在進(jìn)入到最有趣的部分之前,我們先從回顧 strace 的基本實(shí)現(xiàn)來開始。它不是 DTrace,但 strace 仍然非常有用。
Ptrace 一直沒有被標(biāo)準(zhǔn)化。它的接口在不同的操作系統(tǒng)上非常類似,尤其是在核心功能方面,但是在不同的系統(tǒng)之間仍然存在細(xì)微的差別。ptrace(2)
的原型基本上應(yīng)該像下面這樣,但特定的類型可能有些差別。
long ptrace(int request, pid_t pid, void *addr, void *data);
pid
是被跟蹤進(jìn)程的 ID。雖然同一個(gè)時(shí)間只有一個(gè)跟蹤器可以附著到該進(jìn)程上,但是一個(gè)跟蹤器可以附著跟蹤多個(gè)進(jìn)程。
request
字段選擇一個(gè)具體的 Ptrace 函數(shù),比如 ioctl(2)
接口。對(duì)于 strace,只需要兩個(gè):
PTRACE_TRACEME
:這個(gè)進(jìn)程被它的父進(jìn)程跟蹤。
PTRACE_SYSCALL
:繼續(xù)跟蹤,但是在下一下系統(tǒng)調(diào)用入口或出口時(shí)停止。
PTRACE_GETREGS
:取得被跟蹤進(jìn)程的寄存器內(nèi)容副本。
另外兩個(gè)字段,addr
和 data
,作為所選的 Ptrace 函數(shù)的一般參數(shù)。一般情況下,可以忽略一個(gè)或全部忽略,在那種情況下,傳遞零個(gè)參數(shù)。
strace 接口實(shí)質(zhì)上是前綴到另一個(gè)命令之前。
$ strace [strace options] program [arguments]
最小化的 strace 不需要任何選項(xiàng),因此需要做的***件事情是 —— 假設(shè)它至少有一個(gè)參數(shù) —— 在 argv
尾部的 fork(2)
和 exec(2)
被跟蹤進(jìn)程。但是在加載目標(biāo)程序之前,新的進(jìn)程將告知內(nèi)核,目標(biāo)程序?qū)⒈凰母高M(jìn)程繼續(xù)跟蹤。被跟蹤進(jìn)程將被這個(gè) Ptrace 系統(tǒng)調(diào)用暫停。
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));}
父進(jìn)程使用 wait(2)
等待子進(jìn)程的 PTRACE_TRACEME
,當(dāng) wait(2)
返回后,子進(jìn)程將被暫停。
waitpid(pid, 0, 0);
在允許子進(jìn)程繼續(xù)運(yùn)行之前,我們告訴操作系統(tǒng),被跟蹤進(jìn)程和它的父進(jìn)程應(yīng)該一同被終止。一個(gè)真實(shí)的 strace 實(shí)現(xiàn)可能會(huì)設(shè)置其它的選擇,比如: PTRACE_O_TRACEFORK
。
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_EXITKILL);
剩余部分就是一個(gè)簡(jiǎn)單的、無休止的循環(huán)了,每循環(huán)一次捕獲一個(gè)系統(tǒng)調(diào)用。循環(huán)體總共有四步:
等待進(jìn)程進(jìn)入下一個(gè)系統(tǒng)調(diào)用。
輸出系統(tǒng)調(diào)用的一個(gè)描述。
允許系統(tǒng)調(diào)用去運(yùn)行并等待返回。
輸出系統(tǒng)調(diào)用返回值。
這個(gè) PTRACE_SYSCALL
請(qǐng)求被用于等待下一個(gè)系統(tǒng)調(diào)用時(shí)開始,和等待那個(gè)系統(tǒng)調(diào)用退出。和前面一樣,需要一個(gè) wait(2)
去等待被跟蹤進(jìn)程進(jìn)入期望的狀態(tài)。
ptrace(PTRACE_SYSCALL, pid, 0, 0);waitpid(pid, 0, 0);
當(dāng) wait(2)
返回時(shí),進(jìn)行了系統(tǒng)調(diào)用的線程的寄存器中寫入了該系統(tǒng)調(diào)用的系統(tǒng)調(diào)用號(hào)及其參數(shù)。盡管如此,操作系統(tǒng)仍然沒有為這個(gè)系統(tǒng)調(diào)用提供服務(wù)。這個(gè)細(xì)節(jié)對(duì)后續(xù)操作很重要。
接下來的一步是采集系統(tǒng)調(diào)用信息。這是各個(gè)系統(tǒng)架構(gòu)不同的地方。在 x86-64 上,系統(tǒng)調(diào)用號(hào)是在 rax
中傳遞的,而參數(shù)(最多 6 個(gè))是在 rdi
、rsi
、rdx
、r10
、r8
和 r9
中傳遞的。這些寄存器是由另外的 Ptrace 調(diào)用讀取的,不過這里再也不需要 wait(2)
了,因?yàn)楸桓欉M(jìn)程的狀態(tài)再也不會(huì)發(fā)生變化了。
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);
這里有一個(gè)警告。由于 內(nèi)核的內(nèi)部用途,系統(tǒng)調(diào)用號(hào)是保存在 orig_rax
中而不是 rax
中。而所有的其它系統(tǒng)調(diào)用參數(shù)都是非常簡(jiǎn)單明了的。
接下來是它的另一個(gè) PTRACE_SYSCALL
和 wait(2)
,然后是另一個(gè) PTRACE_GETREGS
去獲取結(jié)果。結(jié)果保存在 rax
中。
ptrace(PTRACE_GETREGS, pid, 0, ®s);fprintf(stderr, " = %ld\n", (long)regs.rax);
這個(gè)簡(jiǎn)單程序的輸出也是非常粗糙的。這里的系統(tǒng)調(diào)用都沒有符號(hào)名,并且所有的參數(shù)都是以數(shù)字形式輸出,甚至是一個(gè)指向緩沖區(qū)的指針也是如此。更完整的 strace 輸出將能知道哪個(gè)參數(shù)是指針,并使用 process_vm_readv(2)
從被跟蹤進(jìn)程中讀取哪些緩沖區(qū),以便正確輸出它們。
然而,這些僅僅是系統(tǒng)調(diào)用攔截的基礎(chǔ)工作。
假設(shè)我們想使用 Ptrace 去實(shí)現(xiàn)如 OpenBSD 的 pledge(2)
這樣的功能,它是 一個(gè)進(jìn)程承諾只使用一套受限的系統(tǒng)調(diào)用。初步想法是,許多程序一般都有一個(gè)初始化階段,這個(gè)階段它們都需要進(jìn)行許多的系統(tǒng)訪問(比如,打開文件、綁定套接字、等等)。初始化完成以后,它們進(jìn)行一個(gè)主循環(huán),在主循環(huán)中它們處理輸入,并且僅使用所需的、很少的一套系統(tǒng)調(diào)用。
在進(jìn)入主循環(huán)之前,一個(gè)進(jìn)程可以限制它自己只能運(yùn)行所需要的幾個(gè)操作。如果 程序有缺陷,能夠通過惡意的輸入去利用該缺陷,這個(gè)承諾可以有效地限制漏洞利用的實(shí)現(xiàn)。
使用與 strace 相同的模型,但不是輸出所有的系統(tǒng)調(diào)用,我們既能夠阻塞某些系統(tǒng)調(diào)用,也可以在它的行為異常時(shí)簡(jiǎn)單地終止被跟蹤進(jìn)程。終止它很容易:只需要在跟蹤器中調(diào)用 exit(2)
。因此,它也可以被設(shè)置為去終止被跟蹤進(jìn)程。阻塞系統(tǒng)調(diào)用和允許子進(jìn)程繼續(xù)運(yùn)行都只是些雕蟲小技而已。
最棘手的部分是當(dāng)系統(tǒng)調(diào)用啟動(dòng)后沒有辦法去中斷它。當(dāng)跟蹤器在入口從 wait(2)
中返回到系統(tǒng)調(diào)用時(shí),從一開始停止一個(gè)系統(tǒng)調(diào)用的僅有方式是,終止被跟蹤進(jìn)程。
然而,我們不僅可以“搞亂”系統(tǒng)調(diào)用的參數(shù),也可以改變系統(tǒng)調(diào)用號(hào)本身,將它修改為一個(gè)不存在的系統(tǒng)調(diào)用。返回時(shí),在 errno
中 通過正常的內(nèi)部信號(hào),我們就可以報(bào)告一個(gè)“友好的”錯(cuò)誤信息。
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); }}
這個(gè)簡(jiǎn)單的示例只是檢查了系統(tǒng)調(diào)用是否違反白名單或黑名單。而它們?cè)谶@里并沒有差別,比如,允許文件以只讀而不是讀寫方式打開(open(2)
),允許匿名內(nèi)存映射但不允許非匿名映射等等。但是這里仍然沒有辦法去動(dòng)態(tài)撤銷被跟蹤進(jìn)程的權(quán)限。
跟蹤器與被跟蹤進(jìn)程如何溝通?使用人為的系統(tǒng)調(diào)用!
對(duì)于我的這個(gè)類似于 pledge 的系統(tǒng)調(diào)用 —— 我可以通過調(diào)用 xpledge()
將它與真實(shí)的系統(tǒng)調(diào)用區(qū)分開 —— 我設(shè)置 10000 作為它的系統(tǒng)調(diào)用號(hào),這是一個(gè)非常大的數(shù)字,真實(shí)的系統(tǒng)調(diào)用中從來不會(huì)用到它。
#define SYS_xpledge 10000
為演示需要,我同時(shí)構(gòu)建了一個(gè)非常小的接口,這在實(shí)踐中并不是個(gè)好主意。它與 OpenBSD 的 pledge(2)
稍有一些相似之處,它使用了一個(gè) 字符串接口。事實(shí)上,設(shè)計(jì)一個(gè)健壯且安全的權(quán)限集是非常復(fù)雜的,正如在 pledge(2)
的手冊(cè)頁(yè)面上所顯示的那樣。下面是對(duì)被跟蹤進(jìn)程的系統(tǒng)調(diào)用的完整接口和實(shí)現(xiàn):
#define _GNU_SOURCE#include#define XPLEDGE_RDWR (1 << 0)#define XPLEDGE_OPEN (1 << 1) #define xpledge(arg) syscall(SYS_xpledge, arg)
如果給它傳遞個(gè)參數(shù) 0 ,僅允許一些基本的系統(tǒng)調(diào)用,包括那些用于去分配內(nèi)存的系統(tǒng)調(diào)用(比如 brk(2)
)。 PLEDGE_RDWR
位允許 各種 讀和寫的系統(tǒng)調(diào)用(read(2)
、readv(2)
、pread(2)
、preadv(2)
等等)。PLEDGE_OPEN
位允許 open(2)
。
為防止發(fā)生提升權(quán)限的行為,pledge()
會(huì)攔截它自己 —— 但這樣也防止了權(quán)限撤銷,以后再細(xì)說這方面內(nèi)容。
在 xpledge 跟蹤器中,我需要去檢查這個(gè)系統(tǒng)調(diào)用:
/* Handle entrance */switch (regs.orig_rax) { case SYS_pledge: register_pledge(regs.rdi); break;}
操作系統(tǒng)將返回 ENOSYS
(函數(shù)尚未實(shí)現(xiàn)),因?yàn)樗皇且粋€(gè)真實(shí)的系統(tǒng)調(diào)用。為此在退出時(shí)我用一個(gè) success(0)
去覆寫它。
/* Handle exit */switch (regs.orig_rax) { case SYS_pledge: ptrace(PTRACE_POKEUSER, pid, RAX * 8, 0); break;}
我寫了一小段測(cè)試程序去打開 /dev/urandom
,做一個(gè)讀操作,嘗試去承諾后,然后試著第二次打開 /dev/urandom
,然后確認(rèn)它能夠讀取原始的 /dev/urandom
文件描述符。在沒有承諾跟蹤器的情況下運(yùn)行,輸出如下:
$ ./examplefread("/dev/urandom")[1] = 0xcd2508c7XPledging...XPledge failed: Function not implementedfread("/dev/urandom")[2] = 0x0be4a986fread("/dev/urandom")[1] = 0x03147604
做一個(gè)無效的系統(tǒng)調(diào)用并不會(huì)讓應(yīng)用程序崩潰。它只是失敗,這是一個(gè)很方便的返回方式。當(dāng)它在跟蹤器下運(yùn)行時(shí),它的輸出如下:
>$ ./xpledge ./examplefread("/dev/urandom")[1] = 0xb2ac39c4XPledging...fopen("/dev/urandom")[2]: Operation not permittedfread("/dev/urandom")[1] = 0x2e1bd1c4
這個(gè)承諾很成功,第二次的 fopen(3)
并沒有進(jìn)行,因?yàn)楦櫰饔靡粋€(gè) EPERM
阻塞了它。
可以將這種思路進(jìn)一步發(fā)揚(yáng)光大,比如,改變文件路徑或返回一個(gè)假的結(jié)果。一個(gè)跟蹤器可以很高效地 chroot 它的被跟蹤進(jìn)程,通過一個(gè)系統(tǒng)調(diào)用將任意路徑傳遞給 root 從而實(shí)現(xiàn) chroot 路徑。它甚至可以對(duì)用戶進(jìn)行欺騙,告訴用戶它以 root 運(yùn)行。事實(shí)上,這些就是 Fakeroot NG 程序所做的事情。
假設(shè)你不滿足于僅攔截一些系統(tǒng)調(diào)用,而是想攔截全部系統(tǒng)調(diào)用。你就會(huì)有了 一個(gè)打算在其它操作系統(tǒng)上運(yùn)行的二進(jìn)制程序,無需系統(tǒng)調(diào)用,這個(gè)二進(jìn)制程序可以一直運(yùn)行。
使用我在前面所描述的這些內(nèi)容你就可以管理這一切。跟蹤器可以使用一個(gè)假冒的東西去代替系統(tǒng)調(diào)用號(hào),允許它失敗,以及為系統(tǒng)調(diào)用本身提供服務(wù)。但那樣做的效率很低。其實(shí)質(zhì)上是對(duì)每個(gè)系統(tǒng)調(diào)用做了三個(gè)上下文切換:一個(gè)是在入口上停止,一個(gè)是讓系統(tǒng)調(diào)用總是以失敗告終,還有一個(gè)是在系統(tǒng)調(diào)用退出時(shí)停止。
從 2005 年以后,對(duì)于這個(gè)技術(shù),PTrace 的 Linux 版本有更高效的操作:PTRACE_SYSEMU
。PTrace 僅在每個(gè)系統(tǒng)調(diào)用發(fā)出時(shí)停止一次,在允許被跟蹤進(jìn)程繼續(xù)運(yùn)行之前,由跟蹤器為系統(tǒng)調(diào)用提供服務(wù)。
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 ... */ }}
從任何具有(足夠)穩(wěn)定的系統(tǒng)調(diào)用 ABI(LCTT 譯注:應(yīng)用程序二進(jìn)制接口),在相同架構(gòu)的機(jī)器上運(yùn)行一個(gè)二進(jìn)制程序時(shí),你只需要 PTRACE_SYSEMU
跟蹤器、一個(gè)加載器(用于代替 exec(2)
),和這個(gè)二進(jìn)制程序所需要(或僅運(yùn)行靜態(tài)的二進(jìn)制程序)的任何系統(tǒng)庫(kù)即可。
關(guān)于“怎么使用Ptrace去攔截和仿真Linux系統(tǒng)調(diào)用”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。