用windbg核心模式除錯使用者態程式
轉載自:https://blog.csdn.net/lixiangminghate/article/details/52847658
使用核心除錯會話也可以執行一些使用者態除錯任務,比如向位於使用者態的模組設定斷點。但這樣做與使用使用者態偵錯程式有什麼不同呢?我們就以向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(附註,執行這個命令前,需要切換windbg到目標程序)。因為核心斷點已經取消了,這一定是使用者態偵錯程式寫入的。
接下來的問題是,既然這裡有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寫入的斷點寫在了公共的程式碼上,會影響到使用這個公共程式碼的所有程序。