使用內核調試會話也可以執行一些用戶態調試任務,比如向位于用戶態的模塊設置斷點。但這樣做與使用用戶態調試器有什么不同呢?我們就以向NTDLL.dll模塊的ZwTerminateProcess函數(Stub)為例談談二者的區別。
區別一、在內核調試會話中設置這個斷點的“難度”略大些。這是因為NTDLL不屬于內核態的模塊,所以內核會話通常不會加載這個模塊(的符號),因此當執行bp命令時很可能被自動蛻化為bu命令。
0:?kd>?bp?ntdll!ZwTerminateProcess
Bp?expression?'ntdll!ZwTerminateProcess'?could?not?be?resolved,?adding?deferred?bp
恢復執行后,一般的操作也不會觸發調試器來加載NTDLL模塊和解決這個未決的斷點。因此再中斷下來,重新加載符號也可能沒有用:
2:?kd>?.reload
Connected?to?Windows?Vista?6000?x86?compatible?target,?ptr64?FALSE
Loading?Kernel?Symbols
.............................................................................................................................
Loading?User?Symbols
Loading?unloaded?module?list
........
2:?kd>?bl
?0?eu?????????????0001?(0001)?(ntdll!ZwTerminateProcess)
使用.reload命令強制加載這個模塊也不那么容易:
0:?kd>?.reload?/s?/f?ntdll.dll
"ntdll.dll"?was?not?found?in?the?image?list.
Debugger?will?attempt?to?load?"ntdll.dll"?at?given?base?00000000.
Please?provide?the?full?image?name,?including?the?extension?(i.e.?kernel32.dll)
for?more?reliable?results.Base?address?and?size?overrides?can?be?given?as
.reload?<image.ext>=<base>,<size>.
Unable?to?add?module?at?00000000
那么該如何設置呢?方法一需要以下幾步:
1.A?使用!process命令顯示當前進程:
kd>?!process
PROCESS?80af22a0??SessionId:?none??Cid:?0000????Peb:?00000000??ParentCid:?0000
????DirBase:?00039000??ObjectTable:?e1001e38??HandleCount:?240.
????Image:?Idle
如果像上面這樣是IDLE進程或者是System這些沒有用戶態的進程,那么就需要執行下面一步,否則跳到1.C。
1.B?使用!process?0?0命令列出所有進程,然后選一個普通的Windows進程,并切換到這個進程:
kd>?!process?0?0?
****?NT?ACTIVE?PROCESS?DUMP?****
...
PROCESS?82748330??SessionId:?0??Cid:?0110????Peb:?7ffde000??ParentCid:?059c
????DirBase:?13076000??ObjectTable:?e1a55640??HandleCount:??72.
????Image:?notepad.exe
kd>?.PROCESS?82748330
Implicit?process?is?now?82748330
WARNING:?.cache?forcedecodeuser?is?not?enabled
1.C?執行.reload或.reload?/user重新加載符號:
kd>?.reload
Connected?to?Windows?XP?2600?x86?compatible?target,?ptr64?FALSE
Loading?Kernel?Symbols
................................................................................................
Loading?User?Symbols
...............................
Loading?unloaded?module?list
..............................
kd>?lm?m?ntdll
start????end????????module?name
7c800000?7c8c3000???ntdll??????(pdb?symbols)??????????d:\symbols\ntdll.pdb\9A2A73EBE8194059A14361915257B0B01\ntdll.pdb
第二種看起來可能更費事的方法就是在系統服務的內核函數設置斷點,斷點命中后,執行棧回溯這樣的命令,再執行.reload(加/user會省些時間,不是必須)。例如:
0:?kd>?bp?nt!NtTerminateProcess
0:?kd>?g
Breakpoint?2?hit
nt!NtTerminateProcess:
81a1b043?8bff????????????mov?????edi,edi
0:?kd>?kv
ChildEBP?RetAddr??Args?to?Child??????????????
9f272d54?8188c96a?00000000?00000000?0021f998?nt!NtTerminateProcess
9f272d54?77c20f34?00000000?00000000?0021f998?nt!KiFastCallEntry+0x12a?(FPO:?[0,3]?TrapFrame?@?9f272d64)
WARNING:?Frame?IP?not?in?any?known?module.?Following?frames?may?be?wrong.
0021f998?7682d873?00000000?77e8f3b0?ffffffff?0x77c20f34
...
0:?kd>?.reload
Connected?to?Windows?Vista?6000?x86?compatible?target,?ptr64?FALSE
Loading?Kernel?Symbols
..............................................................................................................................
Loading?User?Symbols
..................
再執行kv:
0:?kd>?kv
ChildEBP?RetAddr??Args?to?Child??????????????
9f272d54?8188c96a?00000000?00000000?0021f998?nt!NtTerminateProcess
9f272d54?77c20f34?00000000?00000000?0021f998?nt!KiFastCallEntry+0x12a?(FPO:?[0,3]?TrapFrame?@?9f272d64)
0021f978?77c20580?77bfa35f?00000000?00000000?ntdll!KiFastSystemCallRet?(FPO:?[0,0,0])
0021f97c?77bfa35f?00000000?00000000?00af0e70?ntdll!NtTerminateProcess+0xc?(FPO:?[2,0,0])
0021f998?7682d872?00000000?77e8f3b0?ffffffff?ntdll!RtlExitUserProcess+0x39?(FPO:?[Non-Fpo])
此時可以確信內核調試會話已經加載NTDLL的符號了,再顯示斷點:
0:?kd>?bl
?0?e?77c20574?????0001?(0001)?ntdll!NtTerminateProcess
?2?e?81a1b043?????0001?(0001)?nt!NtTerminateProcess
這個顯示表明內核調試會話已經落實了這個用戶態的斷點。
如果是在加載NTDLL模塊后再執行bp命令,恢復執行后,KD會有一個提示告訴我們它成功的向斷點位置寫入了INT?3。
0:?kd>?bp?ntdll!ZwTerminateProcess
0:?kd>?bl
?0?e?77c20574??0001?(0001)?ntdll!NtTerminateProcess
0:?kd>?g
KD:?write?to?77c20574??ok
相對而言,如果是在用戶態調試會話中,因為NTDLL會被映射到所有用戶態進程中,而且ZwTerminateProcess是導出的函數,所以bp?ntdll!ZwTerminateProcess會非常順利的執行。
區別二、斷點的作用范圍不同,在內核調試會話中設置的ntdll!NtTerminateProcess斷點會影響所有進程(可能有特例),而在用戶態調試中對這個位置設置的斷點只對當前進程有效。舉例來說,剛才在內核調試會話中設置bp斷點時的當前進程是notepad,但是當我們關閉計算器進程時這個斷點也會命中。甚至當我們新啟動一個WinMine程序,然后關閉它時,斷點也會命中。
相對而言,如果是在調試notepad進程的用戶態調試會話中對ntdll!NtTerminateProcess設置一個斷點,那么這絕不會影響其它進程。
那么為什么有這個差異呢?
首先解釋一下,為什么在內核調試會話中設置的斷點會影響所有進程。還是通過試驗來說明,我們先想辦法觀察到我們設置的斷點所對應的INT?3指令。當KD落實我們的斷點后,將目標再中斷到調試器,這時無論是直接觀察線性地址還是物理地址,都看不到INT?3:
1:?kd>?dd?77c20574
77c20574??000152cc?0300ba00?12ff7ffe?900008c2
77c20584??000153b8?0300ba00?12ff7ffe?900008c2
77c20594??000154b8?0300ba00?12ff7ffe?00498dc3
77c205a4??000155b8?0300ba00?12ff7ffe?00498dc3
77c205b4??000156b8?0300ba00?12ff7ffe?00498dc3
77c205c4??000157b8?0300ba00?12ff7ffe?900010c2
77c205d4??000158b8?0300ba00?12ff7ffe?900018c2
77c205e4??000159b8?0300ba00?12ff7ffe?900010c2
0:?kd>?!pte?77c20574?????
???????????????VA?77c20574
PDE?at?00000000C0601DF0????PTE?at?00000000C03BE100
contains?000000001C9AC867??contains?000000001DDDD025
pfn?1c9ac?---DA--UWEV????pfn?1dddd?----A--UREV
0:?kd>?!dd?1dddd574?
#1dddd574?000152b8?0300ba00?12ff7ffe?900008c2
#1dddd584?000153b8?0300ba00?12ff7ffe?900008c2
#1dddd594?000154b8?0300ba00?12ff7ffe?00498dc3
#1dddd5a4?000155b8?0300ba00?12ff7ffe?00498dc3
#1dddd5b4?000156b8?0300ba00?12ff7ffe?00498dc3
#1dddd5c4?000157b8?0300ba00?12ff7ffe?900010c2
#1dddd5d4?000158b8?0300ba00?12ff7ffe?900018c2
#1dddd5e4?000159b8?0300ba00?12ff7ffe?900010c2
這是因為調試器在將目標中斷到調試器之前會恢復已經設置的斷點,按Ctrl+Alt+D啟用WinDBG與KD的通信過程后就可以看到這樣的信息:
DbgKdRestoreBreakPoint(1)?returns?00000000
當恢復執行時,WinDBG會重新把斷點寫入:
DbgKdWriteBreakPoint(77c20574)?returns?00000000,?1
那么如何觀察到寫入的INT?3呢?一種很愜意的方法就是使用ITP這樣的硬件調試器,用了ITP,對付這樣的任務真是手到擒來(圖1)。

圖1?使用硬件調試器觀察斷點指令(0xCC)
因為NTDLL是映射到所有進程中的,所以每個進程執行NtTerminateProcess函數時都會撞見這個0xCC,于是乎這個斷點對所有進程都起作用也就在情理之中了。
下面再說說另一種情況,也就是在用戶態調試器中對ntdll!NtTerminateProcess設置斷點,難道這時就沒有把0xCC寫在大家都會“撞見”的地方么?的確如此。
我們在內核調試會話中使用bc?*命令清除所有斷點,并恢復執行一次,而且通過ITP觀察確保剛才的0xcc已經不在。然后在目標系統中啟動系統中自帶的 NTSD來調試計算器程序,并使用bp?ntdll!NtTerminateProcess設置一個斷點。恢復執行一次,以便讓調試器寫入這個斷點。然后退出計算器程序,這時計算器程序會中斷到NTSD,NTSD中不做分析,直接用g命令恢復執行,這下,我們前面設置的nt! NtTerminateProcess斷點會命中,也就是中斷到內核調試器中。
在內核調試器中,觀察nt!NtTerminateProcess所對應的線性地址:
1:?kd>?dd?77c20574
77c20574??000152cc?0300ba00?12ff7ffe?900008c2
77c20584??000153b8?0300ba00?12ff7ffe?900008c2
77c20594??000154b8?0300ba00?12ff7ffe?00498dc3
77c205a4??000155b8?0300ba00?12ff7ffe?00498dc3
77c205b4??000156b8?0300ba00?12ff7ffe?00498dc3
77c205c4??000157b8?0300ba00?12ff7ffe?900010c2
77c205d4??000158b8?0300ba00?12ff7ffe?900018c2
77c205e4??000159b8?0300ba00?12ff7ffe?900010c2
睜大眼睛看那個0xCC,對的,這里的確有0xCC。因為內核斷點已經取消了,這一定是用戶態調試器寫入的。
接下來的問題是,既然這里有0xCC,那么為什么不影響其它進程呢?注意這個線性地址與前面的一模一樣。
其中的奧妙在于這個線性地址已經不再是前面那個物理地址了:
1:?kd>?!pte?77c20574?
???????????????VA?77c20574
PDE?at?00000000C0601DF0????PTE?at?00000000C03BE100
contains?00000000012E0867??contains?00000000049BD025
pfn?12e0?---DA--UWEV????pfn?49bd?----A--UREV
雖然還同是一個線性地址,但是它現在對應的物理地址變成了49bd574。觀察這個物理地址,其內容與剛才使用線性地址的得到的結果是一樣的:
1:?kd>?!dd?49bd574?
#?49bd574?000152cc?0300ba00?12ff7ffe?900008c2
#?49bd584?000153b8?0300ba00?12ff7ffe?900008c2
#?49bd594?000154b8?0300ba00?12ff7ffe?00498dc3
#?49bd5a4?000155b8?0300ba00?12ff7ffe?00498dc3
#?49bd5b4?000156b8?0300ba00?12ff7ffe?00498dc3
#?49bd5c4?000157b8?0300ba00?12ff7ffe?900010c2
#?49bd5d4?000158b8?0300ba00?12ff7ffe?900018c2
#?49bd5e4?000159b8?0300ba00?12ff7ffe?900010c2
而此時,物理地址1dddd574那里根本沒有0xCC。
說到這里,謎團基本揭開了。事實上,?對于一個普通的進程,系統會把NTDLL的代碼映射給它,如果這個進程始終很普通,那么它便會永遠使用這份映射過來的代碼。但是當它要修改代碼時,系統會執行所謂的Copy?on?Write動作,為其復制一份,讓它來寫。結合我們的情況,當在用戶態調試會話中向 NTDLL中設置斷點時,系統為其復制了一份代碼,讓它去寫,因此它寫入的斷點只有它自己“撞的到”,不會影響其它進程。但是當在內核會話中寫入斷點時,因為是內核調試引擎執行的寫動作,所以沒有觸發Copy?on?Write,因此KD寫入的斷點寫在了公共的代碼上,會影響到使用這個公共代碼的所有進程。
原文出處:
http://advdbg.com/blogs/advdbg_syste...cles/1492.aspx《軟件調試》下載:http://bv.csdn.net/resource/rjts.pdf