转载自:https://www.anquanke.com/post/id/179748
《Dive into Windbg》是一系列关于如何理解和使用Windbg的文章,主要涵盖三个方面:
- 1、Windbg实战运用,排查资源占用、死锁、崩溃、蓝屏等,以解决各种实际问题为导向。
- 2、Windbg原理剖析,插件、脚本开发,剖析调试原理,便于较更好理解Windbg的工作机制。
- 3、Windbg后续思考,站在开发和逆向角度,谈谈软件开发,分享作者使用Windbg的一些经历。
涉及知识点:消息机制、进程退出过程、MUI、国际化等。
一台Windows服务器迁移后,发现资源管理器explorer启动不了。手动运行依然无法启动,在任务管理器能看到进程启动后消失,服务器是Server2008 R2,64位。
关于进程退出,能想到其大致流程:
exit => ExitProcess => TerminateProcess => NtTerminateProcess => PspTerminateThreadByPointer => 线程内 PspExitNormalApc => PspExitThread =>
释放各种资源、通知调试器等 => KeTerminateThread => 交出调度权
_ExitProcess伪代码流程如下:
//from: https://bbs.pediy.com/thread-188064-1.htm#1287019
int __stdcall _ExitProcess(UINT ExitCode)
{
int result; // eax@1
char v2; // [sp+10h] [bp-DCh]@2
UINT v3; // [sp+38h] [bp-B4h]@2
CPPEH_RECORD ms_exc; // [sp+D4h] [bp-18h]@2
result = __security_cookie;
if ( !BaseRunningInServerProcess )
{
RtlAcquirePebLock();
ms_exc.disabled = 0;
NtTerminateProcess(0, ExitCode);
LdrShutdownProcess();
v3 = ExitCode;
CsrClientCallServer(&v2, 0, 65539, 4);
NtTerminateProcess(-1, ExitCode);
ms_exc.disabled = -1;
result = RtlReleasePebLock();
}
return result;
}
因为是在迁移机器,需要开关机,猜测是explorer产生已知异常导致的正常退出,可能是什么东西被破坏了,常见的像注册表配置、文件等,这种在关机时最可能会出现。
对于这种稳定复现的问题,先挂个调试器看看再说,因为此时资源管理器不在,用热键调出任务管理器,运行windbg,下面是具体调试过程:
直接g调试运行,发现进程直接退出,调试器中断在NtTerminateProcess,通过栈回溯可知是从main函数中的正常退出,跟之前猜想一样,说明这是程序内部已知的错误。
仔细观察发现,调试器捕获到一处异常:
(ae4.284): Unknown exception - code 000006ba (first chance)
难道跟这个异常有关?调试器没断下来,只在first chance,说明异常被内部SEH捕获了,没有到达second chance,还是看下异常是什么。使用sx命令查看当前windbg调试事件设置:
这里也说明了未知异常只在调试器收到second chance(即SEH不处理)时才break下来,输入sxe *,让其直接中断,重新调试运行。
观察异常堆栈,原来是音频服务RPC Server端抛了异常,不过这跟explorer无法启动应该没有关联。
输入!teb,看看LastError有没有什么线索:
LastStatusValue是c0000034,这里使用一款开源工具:OpenArk
https://github.com/BlackINT3/OpenArk
使用.err命令或者直接查看NTSTATUS。
找不到文件?还是先跟进explorer去看看,至少目前有一点线索。
在main函数中直接退出,想到WinProc的各种switch,很多应该都被折腾过。不过有符号已经能省很多事了,想想那些没符号全是各种条件断点的crack日子….
通过栈回溯找到调用ExitProcess的地方,ub查看上面的代码,观察哪个点最接近,也可在靠近GetCommandLineW等附近下断点,这样就不用从OEP去跟了。
对于这种必现问题,有两种方法:
1、反汇编代码,可尝试多次下断点、单步跟踪,找到可疑的地方,尤其是要检查一些返回值和LastError。
2、wt直接跟踪,有符号的话,根据函数名找到可疑的地方,结合方法1。
Tips:用bu命令下断点,然后保存到workspace,下次调试继续生效,注意windbg根据符号偏移下断点可能会有bug,以后有机会再来探讨,若有需要可以关掉ASLR。
跟踪发现是CreateDesktopAndTray函数返回NULL,导致退出。
这是第一个关键点,进入函数,发现是因为explorer!c_tray+0x8为NULL导致,一般情况我们可能会考虑内存访问断点来找,不过这种情况下,执行失败后explorer!c_tray可能不会赋值。
观察explorer!CTray::SyncThreadProc线程,c_tray是该线程的一个参数,那么线程应该在初始化c_tray信息,继续跟踪。
最后发现SyncThreadProc调用SHFusionCreateWindowEx函数创建窗口失败了,窗口句柄是NULL,调用之前和调用之后,LastStatusValue都是c0000034,说明开始发现的这个值有误。
跟进SHFusionCreateWindowEx,单步走过CreateWindowEx。有意思的事发生了,得到的窗口句柄rax却不是0,为什么?
说明这个函数内部递归调用,查看栈回溯如下:
说到Windows消息机制,就想到了KeUserModeCallback、KiUserCallbackDispatcher、KernelCallbackTable、LoadLibrary注入、消息钩子等等。
感兴趣的可以看看《KeUserModeCallback用法详解》https://bbs.pediy.com/thread-104918.htm。
NT6之后虽有些不同,但实现思路是一致的。
看到这个_fnINLPCREATESTRUCT蛋疼的命名,我便想起了David Culter看到GUI部门的表情,还是ntoskrnl内部颇有美感。
_fnINLPCREATESTRUCT这个函数便在分发WM_CREATE消息,具体可以看看KernelCallbackTable表,在这个点可以做一些拦截,什么消息钩子、窗口子类化。
接下来思路就清晰了,肯定是某个窗口创建失败了,在调用CreateWindowExW处下条件断点。
bu *** ".if(rax == 0){}.else{gc}"
断下来后,继续查看TEB LastStatus,最后发现是c00b0006错误。
15105 ERROR_MUI_FILE_NOT_LOADED <--> c00b0006 STATUS_MUI_FILE_NOT_LOADED
提示MUI文件不存在,到系统Windows目录查看,explorer.exe.mui果然不在,拷贝之,再次验证,explorer启动成功。。。
MUI文件本身也是PE文件,里面含有语言资源,为了解决国际化,动态分离程序和语言文件,这个和”大名鼎鼎”的LPK有关联。
MUI可以参考:https://docs.microsoft.com/en-us/windows/desktop/intl/overview-of-mui
很多时候,看似一些难缠的问题背后往往原因很简单。积累经验,总结反思,培养测量意识,善用调试跟踪工具,自动化去解决问题。
比如这次我可以在TEB的LastError用内存访问断点,也可以用Procmon监视文件访问,解决问题的方法基本是:猜想 + 论证 =>…=> 解决。
Thanks for reading。
参考资料:
Google
MSDN
Windbg Help
WRK