windwos 調試器內核

前段時間忽然對內核調試器實現原來發生了興趣,於是簡單分析了一下當前windows下主流內核調試器原理,並模仿原理自己也寫了個極其簡單的調試器:)

WinDBG

WinDBG和用戶調試器一點很大不同是內核調試器在一臺機器上啓動,通過串口調試另一個相聯繫的以Debug方式啓動的系統,這個系統可以是虛擬機上的系統,也可以是另一臺機器上的系統(這只是微軟推薦和實現的方法,其實象SoftICE這類內核調試器可以實現單機調試)。很多人認爲主要功能都是在WinDBG裏實現,事實上並不是那麼一回事,windows已經把內核調試的機制集成進了內核,WinDBG、kd之類的內核調試器要做的僅僅是通過串行發送特定格式數據包來進行聯繫,比如中斷系統、下斷點、顯示內存數據等等。然後把收到的數據包經過WinDBG處理顯示出來。

在進一步介紹WinDBG之前,先介紹兩個函數:KdpTrace、KdpStub,我在《windows異常處理流程》一文裏簡單提過這兩個函數。現在再提一下,當異常發生於內核態下,會調用KiDebugRoutine兩次,異常發生於用戶態下,會調用KiDebugRoutine一次,而且第一次調用都是剛開始處理異常的時候。

當WinDBG未被加載時KiDebugRoutine爲KdpStub,處理也很簡單,主要是對由int 0x2d引起的異常如DbgPrint、DbgPrompt、加載卸載SYMBOLS(關於int 0x2d引起的異常將在後面詳細介紹)等,把Context.Eip加1,跳過int 0x2d後面跟着的int 0x3指令。

真正實現了WinDBG功能的函數是KdpTrap,它負責處理所有STATUS_BREAKPOINT和STATUS_SINGLE_STEP(單步)異常。STATUS_BREAKPOINT的異常包括int 0x3、DbgPrint、DbgPrompt、加載卸載SYMBOLS。DbgPrint的處理最簡單,KdpTrap直接向調試器發含有字符串的包。DbgPrompt因爲是要輸出並接收字符串,所以先將含有字符串的包發送出去,再陷入循環等待接收來自調試器的含有回覆字符串的包。SYMBOLS的加載和卸載通過調用KdpReportSymbolsStateChange,int 0x3斷點異常和int 0x1單步異常(這兩個異常基本上是內核調試器處理得最多的異常)通過調用KdpReportExceptionStateChange,這兩個函數很相似,都是通過調用KdpSendWaitContinue函數。KdpSendWaitContinue可以說是內核調試器功能的大管家,負責各個功能的分派。這個函數向內核調試器發送要發送的信息,比如當前所有寄存器狀態,每次單步後我們都可以發現寄存器的信息被更新,就是內核調試器接受它發出的包含最新機器狀態的包;還有SYMBOLS的狀態,這樣加載和卸載了SYMBOLS我們都能在內核調試器裏看到相應的反應。然後KdpSendWaitContinue等待從內核調試器發來的包含命令的包,決定下一步該幹什麼。讓我們來看看KdpSendWaitContinue都能幹些什麼:

case DbgKdReadVirtualMemoryApi:
KdpReadVirtualMemory(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdReadVirtualMemory64Api:
KdpReadVirtualMemory64(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdWriteVirtualMemoryApi:
KdpWriteVirtualMemory(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdWriteVirtualMemory64Api:
KdpWriteVirtualMemory64(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdReadPhysicalMemoryApi:
KdpReadPhysicalMemory(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdWritePhysicalMemoryApi:
KdpWritePhysicalMemory(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdGetContextApi:
KdpGetContext(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdSetContextApi:
KdpSetContext(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdWriteBreakPointApi:
KdpWriteBreakpoint(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdRestoreBreakPointApi:
KdpRestoreBreakpoin(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdReadControlSpaceApi:
KdpReadControlSpace(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdWriteControlSpaceApi:
KdpWriteControlSpace(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdReadIoSpaceApi:
KdpReadIoSpace(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdWriteIoSpaceApi:
KdpWriteIoSpace(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdContinueApi:
if (NT_SUCCESS(ManipulateState.u.Continue.ContinueStatus) != FALSE) {
return ContinueSuccess;
} else {
return ContinueError;
}
break;

case DbgKdContinueApi2:
if (NT_SUCCESS(ManipulateState.u.Continue2.ContinueStatus) != FALSE) {
KdpGetStateChange(&ManipulateState,ContextRecord);
return ContinueSuccess;
} else {
return ContinueError;
}
break;

case DbgKdRebootApi:
KdpReboot();
break;

case DbgKdReadMachineSpecificRegister:
KdpReadMachineSpecificRegister(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdWriteMachineSpecificRegister:
KdpWriteMachineSpecificRegister(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdSetSpecialCallApi:
KdSetSpecialCall(&ManipulateState,ContextRecord);
break;

case DbgKdClearSpecialCallsApi:
KdClearSpecialCalls();
break;

case DbgKdSetInternalBreakPointApi:
KdSetInternalBreakpoint(&ManipulateState);
break;

case DbgKdGetInternalBreakPointApi:
KdGetInternalBreakpoint(&ManipulateState);
break;

case DbgKdGetVersionApi:
KdpGetVersion(&ManipulateState);
break;

case DbgKdCauseBugCheckApi:
KdpCauseBugCheck(&ManipulateState);
break;

case DbgKdPageInApi:
KdpNotSupported(&ManipulateState);
break;

case DbgKdWriteBreakPointExApi:
Status = KdpWriteBreakPointEx(&ManipulateState,
&MessageData,
ContextRecord);
if (Status) {
ManipulateState.ApiNumber = DbgKdContinueApi;
ManipulateState.u.Continue.ContinueStatus = Status;
return ContinueError;
}
break;

case DbgKdRestoreBreakPointExApi:
KdpRestoreBreakPointEx(&ManipulateState,&MessageData,ContextRecord);
break;

case DbgKdSwitchProcessor:
KdPortRestore ();
ContinueStatus = KeSwitchFrozenProcessor(ManipulateState.Processor);
KdPortSave ();
return ContinueStatus;

case DbgKdSearchMemoryApi:
KdpSearchMemory(&ManipulateState, &MessageData, ContextRecord);
break;

讀寫內存、搜索內存、設置/恢復斷點、繼續執行、重啓等等,WinDBG裏的功能是不是都能實現了?呵呵。

每次內核調試器接管系統是通過調用在KiDispatchException裏調用KiDebugRoutine(KdpTrace),但我們知道要讓系統執行到KiDispatchException必須是系統發生了異常。而內核調試器與被調試系統之間只是通過串口聯繫,串口只會發生中斷,並不會讓系統引發異常。那麼是怎麼讓系統產生一個異常呢?答案就在KeUpdateSystemTime裏,每當發生時鐘中斷後在HalpClockInterrupt做了一些底層處理後就會跳轉到這個函數來更新系統時間(因爲是跳轉而不是調用,所以在WinDBG斷下來後回溯堆棧是不會發現HalpClockInterrupt的地址的),是系統中調用最頻繁的幾個函數之一。在KeUpdateSystemTime裏會判斷KdDebuggerEnable是否爲TRUE,若爲TRUE則調用KdPollBreakIn判斷是否有來自內核調試器的包含中斷信息的包,若有則調用DbgBreakPointWithStatus,執行一個int 0x3指令,在異常處理流程進入了KdpTrace後將根據處理不同向內核調試器發包並無限循環等待內核調試的迴應。現在能理解爲什麼在WinDBG裏中斷系統後堆棧回溯可以依次發現KeUpdateSystemTime->RtlpBreakWithStatusInstruction,系統停在了int 0x3指令上(其實int 0x3已經執行過了,只不過Eip被減了1而已),實際已經進入KiDispatchException->KdpTrap,將控制權交給了內核調試器。

系統與調試器交互的方法除了int 0x3外,還有DbgPrint、DbgPrompt、加載和卸載symbols,它們共同通過調用DebugService獲得服務。

NTSTATUS DebugService(
ULONG ServiceClass,
PVOID Arg1,
PVOID Arg2
)
{
NTSTATUS Status;

__asm {
mov eax, ServiceClass
mov ecx, Arg1
mov edx, Arg2
int 0x2d
int 0x3
mov Status, eax
}
return Status;
}

ServiceClass可以是BEAKPOINT_PRINT(0x1)、BREAKPOINT_PROMPT(0x2)、BREAKPOINT_LOAD_SYMBOLS(0x3)、BREAKPOINT_UNLOAD_SYMBOLS(0x4)。爲什麼後面要跟個int 0x3,M$的說法是爲了和int 0x3共享代碼(我沒弄明白啥意思-_-),因爲int 0x2d的陷阱處理程序是做些處理後跳到int 0x3的陷阱處理程序中繼續處理。但事實上對這個int 0x3指令並沒有任何處理,僅僅是把Eip加1跳過它。所以這個int 0x3可以換成任何字節。

int 0x2d和int 0x3生成的異常記錄結(EXCEPTION_RECORD)ExceptionRecord.ExceptionCode都是STATUS_BREAKPOINT(0x80000003),不同是int 0x2d產生的異常的ExceptionRecord.NumberParameters>0且ExceptionRecord.ExceptionInformation對應相應的ServiceClass比如BREAKPOINT_PRINT等。事實上,在內核調試器被掛接後,處理DbgPrint等發送字符給內核調試器不再是通過int 0x2d陷阱服務,而是直接發包。用M$的話說,這樣更安全,因爲不用調用KdEnterDebugger和KdExitDebugger。

最後說一下被調試系統和內核調試器之間的通信。被調試系統和內核調試器之間通過串口發數據包進行通信,Com1的IO端口地址爲0x3f8,Com2的IO端口地址爲0x2f8。在被調試系統準備要向內核調試器發包之前先會調用KdEnterDebugger暫停其它處理器的運行並獲取Com端口自旋鎖(當然,這都是對多處理器而言的),並設置端口標誌爲保存狀態。發包結束後調用KdExitDebugger恢復。每個包就象網絡上的數據包一樣,包含包頭和具體內容。包頭的格式如下:

typedef struct _KD_PACKET {
ULONG PacketLeader;
USHORT PacketType;
USHORT ByteCount;
ULONG PacketId;
ULONG Checksum;
} KD_PACKET, *PKD_PACKET;

PacketLeader是四個相同字節的標識符標識發來的包,一般的包是0x30303030,控制包是0x69696969,中斷被調試系統的包是0x62626262。每次讀一個字節,連續讀4次來識別出包。中斷系統的包很特殊,包裏數據只有0x62626262。包標識符後是包的大小、類型、包ID、檢測碼等,包頭後面就是跟具體的數據。這點和網絡上傳輸的包很相似。還有一些相似的地方比如每發一個包給調試器都會收到一個ACK答覆包,以確定調試器是否收到。若收到的是一個RESEND包或者很長時間沒收到迴應,則會再發一次。對於向調試器發送輸出字符串、報告SYMBOL情況等的包都是一接收到ACK包就立刻返回,系統恢復執行,系統的表現就是會卡那麼短短一下。只有報告狀態的包纔會等待內核調試器的每個控制包並完成對應功能,直到發來的包包含繼續執行的命令爲止。無論發包還是收包,都會在包的末尾加一個0xaa,表示結束。

現在我們用幾個例子來看看調試流程。

記得我以前問過jiurl爲什麼WinDBG的單步那麼慢(相對softICE),他居然說沒覺得慢?*$&$^$^(&(&(我ft。。。現在可以理解爲什麼WinDBG的單步和從操作系統正常執行中斷下來爲什麼那麼慢了。單步慢是因爲每單步一次除了必要的處理外,還得從串行收發包,怎麼能不慢。中斷系統慢是因爲只有等到時鐘中斷髮生執行到KeUpdateSystemTime後被調試系統纔會接受來自WinDBG的中斷包。現在我們研究一下爲什麼在KiDispatchException裏不能下斷點卻可以用單步跟蹤KiDispatchException的原因。如果在KiDispatchException中某處下了斷點,執行到斷點時系統發生異常又重新回到KiDispatchException處,再執行到int 0x3,如此往復造成了死循環,無法不能恢復原來被斷點int 0x3所修改的代碼。但對於int 0x1,因爲它的引起是因爲EFLAG寄存中TF位被置位,並且每次都自動被複位,所以系統可以被繼續執行而不會死循環。現在我們知道了內部機制,我們就可以調用KdXXX函數實現一個類似WinDBG之類的內核調試器,甚至可以替換KiDebugRoutine(KdpTrap)爲自己的函數來自己實現一個功能更強大的調試器,呵呵。


SoftICE

SoftICE的原理和WinDBG完全不一樣。它通過替換正常系統中的中斷處理程序來獲得系統的控制權,也正因爲這樣它才能夠實現單機調試。它的功能實現方法很底層,很少依賴與windows給的接口函數,大部分功能的實現都是靠IO端口讀寫等來完成的。

SoftICE替換了IDT表中以下的中斷(陷阱)處理程序:

0x1: 單步陷阱處理程序
0x2: NMI不可屏蔽中斷
0x3: 調試陷阱處理程序
0x6: 無效操作碼陷阱處理程序
0xb: 段不存在陷阱處理程序
0xc: 堆棧錯誤陷阱處理程序
0xd: 一般保護性錯誤陷阱處理程序
0xe: 頁面錯誤陷阱處理程序
0x2d: 調試服務陷阱處理程序
0x2e: 系統服務陷阱處理程序
0x31: 8042鍵盤控制器中斷處理程序
0x33: 串口2(Com2)中斷處理程序
0x34: 串口1(Com1)中斷處理程序
0x37: 並口中斷處理程序
0x3c: PS/2鼠標中斷處理程序
0x41: 未使用

(這是在PIC系統上更換的中斷。如果是APIC系統的話更換的中斷號有不同,但同樣是更換這些中斷處理程序)

其中關鍵是替換了0x3 調試陷阱處理程序和0x31 i8042鍵盤中斷處理驅動程序(鍵盤是由i8042芯片控制的),SoftICE從這兩個地方獲取系統的控制權。

啓動softICE服務後SoftICE除了更換了IDT裏的處理程序,還有幾點重要的,一是HOOK了i8042prt.sys裏的READ_PORT_UCHAR函數,因爲在對0x60端口讀後,會改變0x64端口對應控制寄存器的狀態。所以在SoftICE的鍵盤中斷控制程序讀了0x60端口後並返回控制權給正常的鍵盤中斷控制程序後,不要讓它再讀一次。還有就是把物理內存前1MB的地址空間通過調用MmMapIoSpace映射到虛擬的地址空間裏,裏面包括顯存物理地址,以後重畫屏幕就通過修改映射到虛擬地址空間的這段顯存內容就行了。

如果顯示模式是彩色模式,那麼顯存起始地址是0xb8000,CRT索引寄存器端口0x3d4,CRT數據寄存器端口0x3d5。如果顯示模式是單色模式,那麼顯存起始地址是0xb0000,CRT索引寄存器端口0x3b4,CRT數據寄存器端口0x3b5。首先寫索引寄存器選擇要進行設置的顯示控制內部寄存器之一(r0-r17),然後將參數寫到其數據寄存器端口。

i8042鍵盤控制器中斷控制驅動程序在每按下一個鍵和彈起一個鍵都會被觸發。SoftICE在HOOK了正常的鍵盤中斷控制程序獲得系統控制權後,首先從0x60端口讀出按下鍵的掃描碼然後向0x20端口發送通用EOI(0x20)表示中斷已結束,如果沒有按下激活熱鍵(ctrl+d),則返回正常鍵盤中斷處理程序。如果是按下熱鍵則會判斷控制檯(就是那個等待輸入命令的顯示代碼的黑色屏幕)是否被激活,未被激活的話則先激活。然後設置IRQ1鍵盤中斷的優先級爲最高,同時設置兩個8259A中斷控制器裏的中斷屏蔽寄存器(向0x21和0xa1發中斷掩碼,要屏蔽哪個中斷就把哪一位設爲1),只允許IRQ1(鍵盤中斷)、IRQ2(中斷控制器2級聯中斷,因爲PS/2鼠標中斷是歸8259A-2中斷控制器管的,只有開放IRQ2才能響應來自8259A-2管理的中斷)、IRQ12(PS/2鼠標中斷,如果有的話),使系統這時只響應這3箇中斷。新的鍵盤和鼠標中斷處理程序會建立一個緩衝區,保存一定數量的輸入掃描信息。當前面的工作都完成後會進入一段循環代碼,負責處理鍵盤和鼠標輸入的掃描碼緩衝區,同時不斷地更新顯存的映射地址緩衝區重畫屏幕(這段循環代碼和WinDBG裏循環等待從串口發來的包的原理是一樣的,都是在後臺循環等待用戶的命令)。這段循環代碼是在激活控制檯的例程裏調用的,也就是說當控制檯已被激活的話正常流程不會再次進入這段循環代碼的(廢話,再進入系統不就死循環了)。當有一個新的鍵按下時,都會重新調用一遍鍵盤中斷處理程序,因爲控制檯已激活,所以它只是簡單地更新鍵盤輸入緩衝區內容然後iret返回。它並不會返回正常的鍵盤中斷處理程序,因爲那樣會交出控制權(想證明這點也很簡單,在SoftICE裏斷正常的鍵盤中斷處理程序,然後g,1秒後在這裏斷下,這是我們可以F10,如果SoftICE會把控制權交給正常的鍵盤中斷處理程序的話,在這裏早就發生死循環了)。鼠標中斷驅動也是一樣。這個時候實際iret返回到的還是那段循環代碼裏面,所以被調試的代碼並不會被執行,除非按下了F10之類的鍵,它會指示退出循環返回最開始時的中斷處理程序,然後再iret返回最開始中斷的地方。當然,因爲設置了EFLAG裏的TF位,執行了一個指令又會通過單步的處理程序進入那段循環的代碼。

而處理int 0x3也差不多,若沒有激活控制檯則先激活並屏蔽除了鍵盤、鼠標及8259A-2中斷控制器外的所有中斷,然後進入那段循環代碼。

作爲對比同樣來看一下在SoftICE裏處理int 0x3和單步的過程。當執行到int 0x3時,激活控制檯並屏蔽中斷,然後將int 0x3指令前後範圍的指令反彙編並寫入顯存映射地址空間,並把最新的寄存器值也寫進去,最後在後臺循環等待鍵盤輸入命令。當命令是F10時,設置好EFLAG的TF位,清除8259A中斷控制器裏的中斷屏蔽寄存器,開放所有中斷,將控制檯清除,從循環代碼中返回新鍵盤(或int 0x3)中斷處理程序,然後再返回到正常鍵盤(或int 0x3)中斷處理程序,由這裏iret到被中斷代碼處執行。執行了一個指令後因爲發生單步異常又進入後臺循環代碼。

SoftICE裏的單步比WinDBG要快得多的原因很簡單,SoftICE只需要把反匯編出來的代碼和數據經過簡單處理再寫入顯存映射地址緩衝區裏刷新屏幕就可以繼續執行了,省略了串行的發包收包,怎麼會不快。而中斷系統更快,按下鍵中斷就會發生,根本不用象WinDBG等時鐘中斷才能把系統斷下來。


後記:

好象說得很簡單,其實一個內核調試器實現起來極其複雜,沒說得再詳細,一是因爲題目就叫“淺析”,就是類似於科普的東西;二是水平和時間有限(主要原因^^);三是真要詳細寫起來就不是這幾千字能說得明白的東西了。還有,反彙編ntice.sys真是一項艱鉅的任務,比分析漏洞要複雜N倍,剛開始沒着門道時真看得我頭昏眼花。在此特別感謝Syser的作者,牛人就是牛人,在我對SoftICE工作原理的認識還處於混沌狀態時,幾句話點醒了我^^。因爲水平有限難免有很多錯漏,還忘高手指出:)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章