标题: MSDN系列(43)--从僵死的调试器中抢救被调试进程
创建: 2021-12-07 15:04 更新: 2021-12-23 11:48 链接: https://scz.617.cn/windows/202112071504.txt
目录:
☆ 背景介绍
☆ Skywing的技术方案
☆ 调试lsass引发的死锁
☆ 利用kd detach
☆ detach.nasm
☆ 使用示例
☆ 后记
☆ 背景介绍
云海在测试机中误操作,直接Attach lsass了,然后windbg僵死。碰上过这事的可能 知道,此时大概率杀不掉windbg,也不敢杀啊,杀了lsass也挂了。同时没法Detach, 因为windbg失去响应了。他来问我有啥办法旁路Detach,否则只能重启系统了。我还 真在十三年前看过Skywing的神操作,这事某些情况下有解,但云海来问时,我没想 起细节。
本文在Win10企业版2016 LTSB 1607(OS Build 14393.4704)上测试。
☆ Skywing的技术方案
Recovering a process from a hung debugger - Ken Johnson [2009-02-21] http://www.nynaeve.net/?p=274
要点是给windbg指定"-pe"二次Attach,然后Resume线程,最后qd退出。
二十一世纪初时,Skywing和Skape这对双S组合在安全领域很是拉风。Skape真名是 Matt Miller,大约在2008年底加入微软,负责MSRC?这我不确认,反正后来给微软 报漏洞、报缓解措施的可能跟他打过交道。他的个人主页还在维护中。
Skywing的真名是Ken Johnson,大约在2009年初加入微软,比Skape晚了没几个月, 去了同一个安全团队吧。这是不输Alex Ionescu的顶级神人,曾经从他那儿学了好多 东西,其个人主页停更于2010年,但仍活着。
云海说Skywing应该还在微软。Matt Miller提过一次,说他们试图让Skywing用 Twitter,失败了,但他们说服Skywing重新写blog,下面这篇就是Skywing写的
https://msrc-blog.microsoft.com/2018/03/23/kva-shadow-mitigating-meltdown-on-windows/
总的来说,Skywing进入了神隐模式,不更新自己的旧blog,也不用Twitter。他这种 神仙范儿,真心佩服。只是有些遗憾,再也不能从他那里学东西了。
☆ 调试lsass引发的死锁
在管理员级cmd中执行
C:\temp\cdb.exe -sins -y "srv*http://msdl.microsoft.com/download/symbols" -noinh -snul -hd -o -pn lsass.exe
依次尝试访问任务栏上的"桌面"、开始菜单、资源管理器,尝试Ctrl-Shift-Esc、 Win-R,确保整个系统进入死锁状态。
若此刻Host与Guest之间有kd接入,可以看到发生了什么。
kd> !process 0 0x1f lsass.exe PROCESS ffffda884002a800 SessionId: 0 Cid: 0314 Peb: e6548a7000 ParentCid: 02ac FreezeCount 1 ... DebugPort ffffda8843215970
THREAD ffffda8840027080 Cid 0314.0324 Teb: 000000e6548ac000 Win32Thread: 0000000000000000 WAIT: (Suspended) KernelMode Non-Alertable
SuspendCount 1
FreezeCount 1
ffffda8840027360 NotificationEvent
Not impersonating
DeviceMap ffffa08d0bc17c50
Owning Process ffffda884002a800 Image: lsass.exe
Attached Process N/A Image: N/A
Wait Start TickCount 98653130 Ticks: 10534 (0:00:02:44.593)
Context Switch Count 583 IdealProcessor: 0
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address lsass!LsapRmServerThread (0x00007ff772ec3570)
Stack Init ffffb10165dddc90 Current ffffb10165ddd180
Base ffffb10165dde000 Limit ffffb10165dd8000 Call 0000000000000000
Priority 11 BasePriority 9 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP RetAddr Call Site
ffffb10165ddd1c0 fffff802
b7c751bd nt!KiSwapContext+0x76
ffffb10165ddd300 fffff802
b7c74c5f nt!KiSwapThread+0x17d
ffffb10165ddd3b0 fffff802
b7c76a37 nt!KiCommitThreadWait+0x14f
ffffb10165ddd450 fffff802
b7cb95ad nt!KeWaitForSingleObject+0x377
ffffb10165ddd500 fffff802
b7c77c4a nt!KiSchedulerApc+0x231
ffffb10165ddd620 fffff802
b7c753a4 nt!KiDeliverApc+0x22a
ffffb10165ddd6b0 fffff802
b7c74c5f nt!KiSwapThread+0x364
ffffb10165ddd760 fffff802
b7c76a37 nt!KiCommitThreadWait+0x14f
ffffb10165ddd800 fffff802
b7cfb18a nt!KeWaitForSingleObject+0x377
ffffb10165ddd8b0 fffff802
b80079cd nt!AlpcpWaitForSingleObject+0x3e
ffffb10165ddd8f0 fffff802
b7ffd59b nt!AlpcpReceiveMessagePort+0x44d
ffffb10165ddd980 fffff802
b7ffd3fe nt!AlpcpReceiveLegacyMessage+0x10b
ffffb10165ddda20 fffff802
b7ffd323 nt!NtReplyWaitReceivePortEx+0xce
ffffb10165dddac0 fffff802
b7d75103 nt!NtReplyWaitReceivePort+0xf
ffffb10165dddb00 00007fff
88d95de4 nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffb10165dddb00)
000000e6
54b7f3f8 00000000`00000000 ntdll!NtReplyWaitReceivePort+0x14
...
THREAD ffffda8841a8b600 Cid 0314.1928 Teb: 000000e654962000 Win32Thread: 0000000000000000 WAIT: (Executive) KernelMode Non-Alertable
SuspendCount 1
FreezeCount 1
ffffb10166027d80 SynchronizationEvent
Not impersonating
DeviceMap ffffa08d0bc17c50
Owning Process ffffda884002a800 Image: lsass.exe
Attached Process N/A Image: N/A
Wait Start TickCount 98653131 Ticks: 10533 (0:00:02:44.578)
Context Switch Count 9 IdealProcessor: 0
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address ntdll!DbgUiRemoteBreakin (0x00007fff88dc0480)
Stack Init ffffb10166028c90 Current ffffb101660279b0
Base ffffb10166029000 Limit ffffb10166023000 Call 0000000000000000
Priority 9 BasePriority 9 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP RetAddr Call Site
ffffb101660279f0 fffff802
b7c751bd nt!KiSwapContext+0x76
ffffb10166027b30 fffff802
b7c74c5f nt!KiSwapThread+0x17d
ffffb10166027be0 fffff802
b7c76a37 nt!KiCommitThreadWait+0x14f
ffffb10166027c80 fffff802
b821f79c nt!KeWaitForSingleObject+0x377
ffffb10166027d30 fffff802
b8220985 nt!DbgkpQueueMessage+0x230
ffffb10166027f30 fffff802
b80fe7da nt!DbgkpSendApiMessage+0xa9
ffffb10166027f80 fffff802
b7cbb521 nt!DbgkForwardException+0x14e
ffffb10166028100 fffff802
b7d75702 nt!KiDispatchException+0x331
ffffb10166028920 fffff802
b7d70298 nt!KiExceptionDispatch+0xc2
ffffb10166028b00 00007fff
88d994f1 nt!KiBreakpointTrap+0x2d8 (TrapFrame @ ffffb10166028b00)
000000e6
671ffd08 00007fff88dc04ca ntdll!DbgBreakPoint+0x1
000000e6
671ffd10 00007fff885b84d4 ntdll!DbgUiRemoteBreakin+0x4a
000000e6
671ffd40 00007fff88d41791 KERNEL32!BaseThreadInitThunk+0x14
000000e6
671ffd70 00000000`00000000 ntdll!RtlUserThreadStart+0x21
lsass.exe中有很多线程,重点看上面列出来的两个线程。
第一个线程在调nt!AlpcpReceiveMessagePort,该线程负责处理RPC/ALPC的读取。第 二个线程对应ntdll!DbgUiRemoteBreakin,这是cdb调试lsass时远程注入的调试线程。
现在看看cdb里发生了什么
kd> !process 0 0x1f cdb.exe PROCESS ffffda8844326800 SessionId: 1 Cid: 1680 Peb: f607a9c000 ParentCid: 1b0c ...
THREAD ffffda884146e080 Cid 1680.0e80 Teb: 000000f607a9d000 Win32Thread: ffffda883f8b6370 WAIT: (WrLpcReply) UserMode Non-Alertable
ffffda884146e6c0 Semaphore Limit 0x1
Waiting for reply to ALPC Message ffffa08d2120ccf0 : queued at port ffffda8840064ad0 : owned by process ffffda884002a800
Not impersonating
DeviceMap ffffa08d1dc71550
Owning Process ffffda8844326800 Image: cdb.exe
Attached Process N/A Image: N/A
Wait Start TickCount 98653131 Ticks: 10541 (0:00:02:44.703)
Context Switch Count 252 IdealProcessor: 0
UserTime 00:00:00.000
KernelTime 00:00:00.046
Win32 Start Address cdb!wmainCRTStartup (0x00007ff7a7ddc8b0)
Stack Init ffffb10165242c90 Current ffffb10165242300
Base ffffb10165243000 Limit ffffb1016523d000 Call 0000000000000000
Priority 13 BasePriority 13 PriorityDecrement 0 IoPriority 2 PagePriority 5
Kernel stack not resident.
Child-SP RetAddr Call Site
ffffb101`65242340 fffff802`b7c751bd nt!KiSwapContext+0x76
ffffb101`65242480 fffff802`b7c74c5f nt!KiSwapThread+0x17d
ffffb101`65242530 fffff802`b7c76a37 nt!KiCommitThreadWait+0x14f
ffffb101`652425d0 fffff802`b7c78048 nt!KeWaitForSingleObject+0x377
ffffb101`65242680 fffff802`b8002b28 nt!AlpcpSignalAndWait+0x1d8
ffffb101`65242720 fffff802`b80db059 nt!AlpcpReceiveSynchronousReply+0x58
ffffb101`65242780 fffff802`b806412d nt!AlpcpProcessConnectionRequest+0x241
ffffb101`65242890 fffff802`b80fb39c nt!AlpcpConnectPort+0x2c5
ffffb101`65242a10 fffff802`b7d75103 nt!NtAlpcConnectPortEx+0x70
ffffb101`65242a90 00007fff`88d96b34 nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffb101`65242b00)
000000f6`07c75bf8 00007fff`8813dc12 ntdll!NtAlpcConnectPortEx+0x14
000000f6`07c75c00 00007fff`8813e5e7 RPCRT4!LRPC_CASSOCIATION::AlpcConnect+0x28a
000000f6`07c75dc0 00007fff`881386ae RPCRT4!LRPC_CASSOCIATION::Connect+0x17f
000000f6`07c75e90 00007fff`8812ebf9 RPCRT4!LRPC_BASE_BINDING_HANDLE::DriveStateForward+0x47e
000000f6`07c75f20 00007fff`881d7072 RPCRT4!LRPC_BINDING_HANDLE::NegotiateTransferSyntax+0xa79
000000f6`07c76020 00007fff`881d6652 RPCRT4!NdrpClientCall3+0x7e2
000000f6`07c763c0 00007fff`84e34a5d RPCRT4!NdrClientCall3+0xf2
000000f6`07c76750 00007fff`84e344f1 SSPICLI!CreateRpcConnection+0x69
000000f6`07c767d0 00007fff`84e37a26 SSPICLI!InitState+0x55
000000f6`07c76820 00007fff`84e37790 SSPICLI!SspipGetUserName+0x1d6
000000f6`07c76910 00007fff`84e3a8ab SSPICLI!GetUserNameExW+0x50
000000f6`07c76960 00007fff`74b8fb9c SSPICLI!GetUserNameExA+0x6b
000000f6`07c769b0 00007fff`74b8fa66 wininet!WininetGetUserName+0x28
000000f6`07c769e0 00007fff`88d1a491 wininet!_InitOnceGlobalUserName+0x56
000000f6`07c76b60 00007fff`860bc7aa ntdll!RtlRunOnceExecuteOnce+0x91
000000f6`07c76ba0 00007fff`74b964ff KERNELBASE!InitOnceExecuteOnce+0xa
000000f6`07c76bd0 00007fff`74b3fa76 wininet!InitializeGlobalUserName+0x2f
000000f6`07c76c00 00007fff`74b42c7d wininet!CacheClientGlobalInitialize+0x56
000000f6`07c76c40 00007fff`74b4038e wininet!GetCurrentSettingsVersion+0x55
000000f6`07c76c70 00007fff`74bac303 wininet!GlobalDataInitialize+0xde
000000f6`07c76d30 00007fff`74bac0f9 wininet!InternetOpenA+0x183
000000f6`07c76e70 00007fff`73df87dc wininet!InternetOpenW+0x129
000000f6`07c76f10 00007fff`73df416e symsrv!StoreWinInet::connect+0x39c
000000f6`07c77430 00007fff`73deac47 symsrv!StoreHTTP::find+0x5e
000000f6`07c77b50 00007fff`73debb69 symsrv!cascade+0xe7
000000f6`07c77fa0 00007fff`73debcc6 symsrv!SymbolServerByIndexWorkerW+0x369
000000f6`07c78b70 00007fff`73deb49a symsrv!SymbolServerByIndexAndChecksumsW+0x66
000000f6`07c78bc0 00007fff`7063fb6f symsrv!SymbolServerW+0xba
000000f6`07c78e50 00007fff`70629be1 dbghelp!symsrvGetFile+0x2ef
000000f6`07c79710 00007fff`7062aa49 dbghelp!diaLocatePdb+0x595
...
cdb.exe中有多个线程。第一个线程正在试图加载PDB,由此发起RPC调用,这个调用的 RPC服务端在lsass.exe里。lsass正被cdb调试,无法响应RPC请求,cdb又等着lsass 响应自己发起的RPC请求,典型的死锁。
"Waiting for reply"这一行已经指明死锁关键,包括引起死锁的进程
可以利用调用栈上的数据获取cdb发起的RPC请求的更多信息
IID 4f32adc8-6052-4a04-8701-293ccf2096f0 ProcNum 0 (sspisrv!SspirConnectRpc)
☆ 利用kd detach
假设在Guest中cdb调试lsass.exe陷入死锁,但Host与Guest间有kd接入,如何用kd进 行抢救?这个问题有现实意义。我一般用cdb,不怎么用GUI的windbg。调试lsass需 要管理员级cmd,我只有一个管理员级cmd,还被cdb占用了,Skywing的技术方案不适 用于此真实场景,这可能是一个用windbg而不用cdb的理由吧。当时我有kd,所以很 不甘心重启或恢复快照,想利用kd直接抢救,后来找到一个可用方案。
我问了一下杨军锋、王宇,二位给了大致类似的思路,设法让cdb走detach的流程。 杨军锋建议利用kd在cdb进程空间注入shellcode,调用DebugActiveProcessStop。 后来证明,思路可行,但有一些细节要处理。
KERNELBASE!DebugActiveProcessStop(PID) KERNELBASE!ProcessIdToHandle(PID) ntdll!DbgUiStopDebugging(ProcessHandle) ntdll!NtRemoveProcessDebug(ProcessHandle,DebugObjectHandle)
假设中断在NtRemoveProcessDebug,kpn看调用栈,看不到DbgUiStopDebugging,因 为在汇编层面有个优化,DbgUiStopDebugging是jmp到NtRemoveProcessDebug,不是 call过去的。
真正进行detach操作的是NtRemoveProcessDebug,它需要两个参数,一个是被调试进 程句柄,另一个是DebugObject句柄,这是被调试进程DebugPort在调试器进程空间的 索引。虽然DebugActiveProcessStop只需要指定目标PID,但实际上要用到 DebugObjectHandle,该值来自线程变量"NtCurrentTeb()->DbgSsReserved[1]"。
KERNELBASE!DebugActiveProcess ntdll!DbgUiConnectToDbg ntdll!NtCreateDebugObject
DbgUiConnectToDbg中调用NtCreateDebugObject设置DbgSsReserved[1]。cdb进程有 多个线程,应该在调用DebugActiveProcess创建调试会话的那个线程中调用配对的 DebugActiveProcessStop,因为它们共用同一个线程变量DbgSsReserved[1]。
但现代cdb实现,或者说dbgeng实现,不再使用DbgSsReserved[1]。detach时调用栈 回溯如下
# Call Site 00 ntdll!NtRemoveProcessDebug 01 dbgeng!LiveUserDebugServices::DetachProcess+0x77 02 dbgeng!LiveUserTargetInfo::DetachProcess+0xe0 03 dbgeng!ProcessInfo::Separate+0x354 04 dbgeng!ParseSeparateCurrentProcess+0xf5 05 dbgeng!DotCommand+0xf1 06 dbgeng!ProcessCommands+0xc90 07 dbgeng!ProcessCommandsAndCatch+0x86 08 dbgeng!Execute+0x346 09 dbgeng!DebugClient::ExecuteWide+0x94 0a cdb!MainLoop+0x532 0b cdb!wmain+0x4df 0c cdb!__wmainCRTStartup+0x14d 0d KERNEL32!BaseThreadInitThunk+0x14 0e ntdll!RtlUserThreadStart+0x21
detach时甚至没有调用DebugActiveProcessStop,直接调了NtRemoveProcessDebug。 现代dbgeng实现有几个类,ProcessInfo、LiveUserTargetInfo、 LiveUserDebugServices,原来保存在DbgSsReserved[1]的DebugObjectHandle现在应 该是某个类成员变量。手工检查cdb所有线程TEB,所有DbgSsReserved[]都是NULL, 无一被使用。
不必找那个保存DebugObjectHandle的类成员变量,"!process 0 1 lsass.exe"得到
DebugPort地址,"!findhandle
既然有DebugObjectHandle,就想直接调NtRemoveProcessDebug来detach。起初用
"!findhandle
dx @$teb->DbgSsReserved[1]=(void*)0x16c
可以修改cdb!wmainCRTStartup附近的代码,用来存放定制的汇编代码,反正也不会 用到了。
如果不在kd里,还可以用.dvalloc分配一块内存存放汇编代码。接下来的问题是,怎 么让cdb的流程转向修改过的cdb!wmainCRTStartup?由于对Windows内核不熟,我用 了个最直白的办法,杀掉cmd导致杀掉cdb,流程转ntdll!NtTerminateProcess。
在用户态用cdb调notepad,然后杀cmd间接杀cdb,有个调用栈回溯
# Call Site 00 ntdll!NtTerminateProcess 01 ntdll!RtlExitUserProcess+0x5b 02 KERNELBASE!DefaultHandler+0xf 03 KERNELBASE!CtrlRoutine+0xa3 04 KERNEL32!BaseThreadInitThunk+0x14 05 ntdll!RtlUserThreadStart+0x21
用cdb调lsass时也一样。可以先对NtTerminateProcess设断,命中后强制修改rip到 cdb!wmainCRTStartup,这样定制的汇编代码得到执行。
不要在kd中.kill杀cdb,那样的话流程不过ntdll!NtTerminateProcess。得去用户态 杀cmd间接杀cdb。
为了验证思路可行,先用cdb调notepad,用kd帮cdb detach,即使出幺蛾子也没啥影 响。起初在cdb中只调DebugActiveProcessStop,发现notepad的DebugPort确实关闭 了,杀cmd间接杀cdb后,notepad没跟着一块儿完蛋。但是,notepad呈僵死状态。在 kd里"!process 0 3 notepad.exe",一堆的"SuspendCount 1"、"FreezeCount 1"。 没去查SuspendCount、FreezeCount的意义,前者应该是KeSuspendThread导致的,后 者是啥概念?正常运行的notepad,这两种值都是0。
想了想,除了DebugActiveProcessStop,可能还得ResumeThread。实验表明,确实如 此。又一次用了笨办法
.shell -ci "!process 0 2 notepad.exe" findstr "THREAD " awk -F' ' '{print "!findhandle " $2 " ffffda883eaef800";}' some.txt
得到notepad所有线程在cdb进程空间的对应句柄,不是TID,ResumeThread用的是线 程句柄。循环调用ResumeThread,继续执行notepad的所有线程。之后,notepad恢复 正常,attach上来的cdb已经被杀,notepad还活着。相当于在cdb失去响应的情况下 旁路detach成功。把同样的思路用于lsass死锁场景,成功解除死锁,系统恢复响应。
有无可能利用kd强制cdb走.detach对应的流程,直接使用现有代码,而不是定制一段 汇编,尤其是lsass死锁场景。这也是各种遗留问题之一,留待有缘人解答。
☆ detach.nasm
很久不写Intel汇编代码,下面这段写得相当扯淡,但能用。这只是个模板,需要填 写有效值再编译。
; nasm -f bin -o detach.bin detach.nasm
BITS 64
%macro pushaq 0 pushfq push rcx push rdx push rbx push rbp push rsi push rdi push r8 push r9 push r10 push r11 push r12 push r13 push r14 push r15 %endmacro
%macro popaq 0 pop r15 pop r14 pop r13 pop r12 pop r11 pop r10 pop r9 pop r8 pop rdi pop rsi pop rbp pop rbx pop rdx pop rcx popfq %endmacro
DebugActiveProcessStop equ 0 ResumeThread equ 8 TerminateProcess equ 0x10
; dt nt!_TEB DbgSsReserved DbgSsReserved equ 0x16a0
_start:
pushaq
; get rip relative to the next instruction after the lea
lea rbp, [rel $+7]
base:
movzx rax, word [rbp+DebugPort_handle-base]
mov rdx, [gs:0x30]
mov qword[rdx+DbgSsReserved+1*8], rax
movzx rcx, word [rbp+target_pid-base]
call qword [rbp+func_array+DebugActiveProcessStop-base]
xor rbx, rbx
loop:
movzx rcx, word [rbp+thread_handle_array-base+rbx*2]
call qword [rbp+func_array+ResumeThread-base]
inc rbx
cmp bx, word [rbp+thread_handle_count-base]
jne loop
popaq
lea rax, [rel $+7]
jmp qword [rax+func_array+TerminateProcess-$]
func_array:
; KERNELBASE!DebugActiveProcessStop
dq 0x7fff86136e10
; KERNELBASE!ResumeThread
dq 0x7fff860dd770
; KERNELBASE!TerminateProcess
dq 0x7fff860e1130
thread_handle_count:
dw ( thread_handle_end - thread_handle_array ) / 2
DebugPort_handle:
dw 0x1314
target_pid:
dw 0x1315
thread_handle_array:
dw 0x1316
dw 0x1317
dw 0x1318
dw 0x1319
dw 0x131a
dw 0x131b
dw 0x131c
thread_handle_end:
☆ 使用示例
在管理员级cmd中执行
C:\temp\cdb.exe -sins -y "srv*http://msdl.microsoft.com/download/symbols" -noinh -snul -hd -o -pn lsass.exe
在kd中".process /i"切到cdb进程空间,执行如下操作
.readmem detach_lsass.bin cdb!wmainCRTStartup l 0n159 u cdb!wmainCRTStartup cdb!wmainCRTStartup+0n159 ba e1 /1 /p @$proc KERNELBASE!CtrlRoutine "r rip=cdb!wmainCRTStartup;gc"
回Guest对着僵死的cdb按下Ctrl-C解除死锁,管理员级cmd仍在。
上例假设detach_lsass.bin大小是159字节。
☆ 后记
相比Skywing的方案,我的方案没啥优势,但有时没机会用Skywing的方案,我碰上了, 就研究了一下。绝大多数人这辈子都不需要了解这些东西,也用不上。
2021-12-22 06:04 bluerust
kill cmd来终止cdb这步我不一定会想到,如果是我的话,可能会想着塞一段kernel shellcode进去,然后KeStackAttachProcess,在内核分配用户进程空间,写入用户 态shellcode,最后KeInsertQueueApc在cdb里执行shellcode,应该也行。当然,我 这还是YY,实操不知道可行不。我很久没有用汇编写shellcode了,我都是用C。
参
Loading and Debugging Windows Kernel Shellcodes with Windbg - Javier Vicente Vallejo [2017-07-23] https://vallejocc.wordpress.com/2017/06/23/loading-and-debugging-windows-kernel-shellcodes-with-windbg-debugging-doublepulsar-shellcode/