玩轉ptrace(一)
by Pradeep Padala
Created 2002-11-01 02:00
翻譯: Magic.D E-mail: adamgic@163.com
譯者序:在開發Hust Online Judge的過程中,查閱了不少資料,關于調試器技術的資料在網上是很少,即便是UNIX編程巨著《UNIX環境高級編程》中,相關內容也不多,直到我在http://www.linuxjournal.com上找到這篇文章,如獲至寶,特翻譯之,作為鄙人翻譯技術文檔的第一次嘗試,必定會有不少蹩腳之處,各位就將就一下吧,歡迎大力拍磚。
你想過怎么實現對系統調用的攔截嗎?你嘗試過通過改變系統調用的參數來愚弄你的系統kernel嗎?你想過調試器是如何使運行中的進程暫停并且控制它嗎?
你
可能會開始考慮怎么使用復雜的kernel編程來達到目的,那么,你錯了。實際上Linux提供了一種優雅的機制來完成這些:ptrace系統函數。
ptrace提供了一種使父進程得以監視和控制其它進程的方式,它還能夠改變子進程中的寄存器和內核映像,因而可以實現斷點調試和系統調用的跟蹤。
使用ptrace,你可以在用戶層攔截和修改系統調用(sys call)
在這篇文章中,我們將學習如何攔截一個系統調用,然后修改它的參數。在本文的第二部分我們將學習更先進的技術:設置斷點,插入代碼到一個正在運行的程序中;我們將潛入到機器內部,偷窺和纂改進程的寄存器和數據段。
基本知識
操
作系統提供了一種標準的服務來讓程序員實現對底層硬件和服務的控制(比如文件系統),叫做系統調用(system
calls)。當一個程序需要作系統調用的時候,它將相關參數放進系統調用相關的寄存器,然后調用軟中斷0x80,這個中斷就像一個讓程序得以接觸到內核
模式的窗口,程序將參數和系統調用號交給內核,內核來完成系統調用的執行。
在i386體系中(本文中所有的代碼都是面向i386體系),系統調用號將放入%eax,它的參數則依次放入%ebx, %ecx, %edx, %esi 和 %edi。 比如,在以下的調用
Write(2, “Hello”, 5)
的匯編形式大概是這樣的
|
這里的$hello指向的是標準字符串”Hello”。
那么,ptrace會在什么時候出現呢?在執行系統調用之前,內核會先檢查當前進程是否處于被“跟蹤”(traced)的狀態。如果是的話,內核暫停當前進程并將控制權交給跟蹤進程,使跟蹤進程得以察看或者修改被跟蹤進程的寄存器。
讓我們來看一個例子,演示這個跟蹤程序的過程
|
運行這個程序,將會在輸出ls命令的結果的同時,輸出:
The child made a system call 11
說明:11是execve的系統調用號,這是該程序調用的第一個系統調用。
想知道系統調用號的詳細內容,察看 /usr/include/asm/unistd.h。
在
以上的示例中,父進程fork出了一個子進程,然后跟蹤它。在調用exec函數之前,子進程用PTRACE_TRACEME作為第一個參數調用了
ptrace函數,它告訴內核:讓別人跟蹤我吧!然后,在子進程調用了execve()之后,它將控制權交還給父進程。當時父進程正使用wait()函數
來等待來自內核的通知,現在它得到了通知,于是它可以開始察看子進程都作了些什么,比如看看寄存器的值之類。
出現系統調用之后,內核會將eax中的值(此時存的是系統調用號)保存起來,我們可以使用PTRACE_PEEKUSER作為ptrace的第一個參數來讀到這個值。
我們察看完系統調用的信息后,可以使用PTRACE_CONT作為ptrace的第一個參數,調用ptrace使子進程繼續系統調用的過程。
ptrace函數的參數
Ptrace有四個參數
long ptrace(enum __ptrace_request request,
pid_t pid,
void *addr,
void *data);
第一個參數決定了ptrace的行為與其它參數的使用方法,可取的值有:
PTRACE_ME
PTRACE_PEEKTEXT
PTRACE_PEEKDATA
PTRACE_PEEKUSER
PTRACE_POKETEXT
PTRACE_POKEDATA
PTRACE_POKEUSER
PTRACE_GETREGS
PTRACE_GETFPREGS,
PTRACE_SETREGS
PTRACE_SETFPREGS
PTRACE_CONT
PTRACE_SYSCALL,
PTRACE_SINGLESTEP
PTRACE_DETACH
在下文中將對這些常量的用法進行說明。
讀取系統調用的參數
通過將PTRACE_PEEKUSER作為ptrace 的第一個參數進行調用,可以取得與子進程相關的寄存器值。
先看下面這個例子
|
以上的例子中我們跟蹤了write系統調用,而ls命令的執行將產生三個write系統調用。使用PTRACE_SYSCALL作為ptrace的
第一個參數,使內核在子進程做出系統調用或者準備退出的時候暫停它。這種行為與使用PTRACE_CONT,然后在下一個系統調用/進程退出時暫停它是等
價的。
在前一個例子中,我們用PTRACE_PEEKUSER來察看write系統調用的參數。系統調用的返回值會被放入%eax。
wait函數使用status變量來檢查子進程是否已退出。它是用來判斷子進程是被ptrace暫停掉還是已經運行結束并退出。有一組宏可以通過status的值來判斷進程的狀態,比如WIFEXITED等,詳情可以察看wait(2) man。
讀取寄存器的值
如果你想在系統調用或者進程終止的時候讀取它的寄存器,使用前面那個例子的方法是可以的,但是這是笨拙的方法。使用PRACE_GETREGS作為ptrace的第一個參數來調用,可以只需一次函數調用就取得所有的相關寄存器值。
獲得寄存器值得例子如下:
|
這段代碼與前面的例子是比較相似的,不同的是它使用了PTRACE_GETREGS。 其中的user_regs_struct結構是在<linux/user.h>中定義的。
來點好玩的
現在該做點有意思的事情了,我們將要把傳給write系統調用的字符串給反轉。
|
這個例子中涵蓋了前面討論過的所有知識點,當然還有些新的內容。這里我們用PTRACE_POKEDATA作為第一個參數,以此來改變子進程中的變量值。它以與PTRACE_PEEKDATA相似的方式工作,當然,它不只是偷窺變量的值了,它可以修改它們。
單步
ptrace
提供了對子進程進行單步的功能。 ptrace(PTRACE_SINGLESTEP, …)
會使內核在子進程的每一條指令執行前先將其阻塞,然后將控制權交給父進程。下面的例子可以查出子進程當前將要執行的指令。為了便于理解,我用匯編寫了這個
受控程序,而不是讓你為c的庫函數到底會作那些系統調用而頭痛。
以下是被控程序的代碼 dummy1.s,使用gcc –o dummy1 dummy1.s來編譯
|
以下的程序則用來完成單步:
|
你可能需要察看Intel的用戶手冊來了解這些指令代碼的意思。
更復雜的單步,比如設置斷點,則需要很仔細的設計和更復雜的代碼才可以實現。
在第二部分,我們將會看到如何在程序中加入斷點,以及將代碼插入到已經在運行的程序中
posted on 2009-07-25 17:45 葉子 閱讀(1518) 評論(5) 編輯 收藏 引用 所屬分類: Unix