一:背景

1. 讲故事

上一篇我们说到了 minhook 的一个简单使用,这一篇给大家分享一个 minhook 在 dump 分析中的实战,先看下面的线程栈。


0:044> ~~[138c]s
win32u!NtUserMessageCall+0x14:
00007ffc`5c891184 c3 ret
0:061> k
# Child-SP RetAddr Call Site
00 0000008c`00ffec68 00007ffc`5f21bfbe win32u!NtUserMessageCall+0x14
01 0000008c`00ffec70 00007ffc`5f21be38 user32!SendMessageWorker+0x11e
02 0000008c`00ffed10 00007ffc`124fd4af user32!SendMessageW+0xf8
03 0000008c`00ffed70 00007ffc`125e943b cogxImagingDevice!DllUnregisterServer+0x3029f
04 0000008c`00ffeda0 00007ffc`125e9685 cogxImagingDevice!DllUnregisterServer+0x11c22b
05 0000008c`00ffede0 00007ffc`600b50e7 cogxImagingDevice!DllUnregisterServer+0x11c475
06 0000008c`00ffee20 00007ffc`60093ccd ntdll!LdrpCallInitRoutine+0x6f
07 0000008c`00ffee90 00007ffc`60092eef ntdll!LdrpProcessDetachNode+0xf5
08 0000008c`00ffef60 00007ffc`600ae319 ntdll!LdrpUnloadNode+0x3f
09 0000008c`00ffefb0 00007ffc`600ae293 ntdll!LdrpDecrementModuleLoadCountEx+0x71
0a 0000008c`00ffefe0 00007ffc`5cd7c00e ntdll!LdrUnloadDll+0x93
0b 0000008c`00fff010 00007ffc`5d47cf78 KERNELBASE!FreeLibrary+0x1e
0c 0000008c`00fff040 00007ffc`5d447aa3 combase!CClassCache::CDllPathEntry::CFinishObject::Finish+0x28 [onecore\com\combase\objact\dllcache.cxx @ 3420]
0d 0000008c`00fff070 00007ffc`5d4471a9 combase!CClassCache::CFinishComposite::Finish+0x4b [onecore\com\combase\objact\dllcache.cxx @ 3530]
0e 0000008c`00fff0a0 00007ffc`5d3f1499 combase!CClassCache::FreeUnused+0xdd [onecore\com\combase\objact\dllcache.cxx @ 6547]
0f 0000008c`00fff650 00007ffc`5d3f13c7 combase!CoFreeUnusedLibrariesEx+0x89 [onecore\com\combase\objact\dllapi.cxx @ 117]
10 (Inline Function) --------`-------- combase!CoFreeUnusedLibraries+0xa [onecore\com\combase\objact\dllapi.cxx @ 74]
11 0000008c`00fff690 00007ffc`6008a019 combase!CDllHost::MTADllUnloadCallback+0x17 [onecore\com\combase\objact\dllhost.cxx @ 929]
12 0000008c`00fff6c0 00007ffc`6008bec4 ntdll!TppTimerpExecuteCallback+0xa9
13 0000008c`00fff710 00007ffc`5f167e94 ntdll!TppWorkerThread+0x644
14 0000008c`00fffa00 00007ffc`600d7ad1 kernel32!BaseThreadInitThunk+0x14

这是一个 .NET某工控自动化控制系统(https://www.cnblogs.com/huangxincheng/p/16544462.html) 的卡死故障,经过一顿分析之后,找到了最后的卡死原因,即 cogxImagingDevice.dll 中有一个 DllMain 的卸载通知,熟悉 win32 的朋友都知道,代码经过 DllMain 的时候会持有一个 LdrpAcquireLoaderLock 进程加载锁,在持锁过程中它突然向一个窗体发送 SendMessageW 消息,可惜的是这个窗体没有给予响应,一直卡死在这里,这就导致 进程加载锁 迟迟得不到释放,引发系统性卡死。。。

如果有朋友还是比较懵的话,我画一张图给大家看看,黑色加粗就是问题的核心所在。

二:寻找解决方案

1. 现有困境

我可以通过 windbg 提取到 SendMessageW 方法的 窗口句柄 hWnd,通过这个 hWnd 找到创建它的 processID 和 ThreadID,但问题是这两个关键信息 是存放在当前机器的内核态中,言外之意就是用户态dump没有这两个信息,所以关键信息的缺失导致无法有效的排查出问题。

解决办法有两个:

  • 抓内核态dump:由于 win32u 模块是闭源的,要想从内核态dump中找出还得不断的参考 reactos,费时费力。
  • SendMessageW跟踪:这个相对来说轻量级,也是本篇重点说的,即 minhook。

2. 如何跟踪 SendMessageW

我的想法是这样的,对 SendMessageW 进行拦截来获取 hWnd 参数,然后通过 hWnd 参数找到对应的 processid 和 threadid,然后再通过 processid 获取 processname,有了这三个信息就可以让对方无所遁形。

为了让大家眼见为实,我们做一个例子,新建一个 WindowsProject1 的Win32窗体,在网关函数 WndProc 中故意让程序卡死,参考代码如下:


LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_CLOSE) {
Sleep(1000 * 1000);
}
// todo....
return 0;
}

接下来新建一个 ConsoleApplication 控制台程序,通过 SendMessageWindowsProject1 打close消息,来演示无故卡死,完整的代码如下:


using System;
using System.Runtime.InteropServices;
using System.Text; namespace ConsoleApplication
{
public static class Program
{
private const uint WM_CLOSE = 0x0010; public static void Main()
{
// 安装 Hook
HookManager.InstallHook(); // 测试:发送 WM_CLOSE 消息(会触发 Hook)
IntPtr hWnd = FindWindow(null, "WindowsProject1"); if (hWnd != IntPtr.Zero)
{
SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
Console.WriteLine("Sent WM_CLOSE to target window.");
}
else
{
Console.WriteLine("Target window not found.");
} Console.ReadKey(); // 卸载 Hook
HookManager.UninstallHook();
} [DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
} public static class HookManager
{
// SendMessageW 的原始函数签名
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
private delegate IntPtr SendMessageWDelegate(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); private static SendMessageWDelegate _originalSendMessageW;
private static IntPtr _sendMessageWPtr = IntPtr.Zero; public static void InstallHook()
{
// 1. 获取 SendMessageW 的地址
_sendMessageWPtr = MinHook.GetProcAddress(
MinHook.GetModuleHandle("user32.dll"), "SendMessageW"); if (_sendMessageWPtr == IntPtr.Zero)
{
Console.WriteLine("Failed to find SendMessageW address.");
return;
} // 2. 初始化 MinHook
var status = MinHook.MH_Initialize();
if (status != MinHook.MH_STATUS.MH_OK)
{
Console.WriteLine($"MH_Initialize failed: {status}");
return;
} // 3. 创建 Hook
var detourPtr = Marshal.GetFunctionPointerForDelegate(
new SendMessageWDelegate(HookedSendMessageW)); status = MinHook.MH_CreateHook(_sendMessageWPtr, detourPtr, out var originalPtr);
if (status != MinHook.MH_STATUS.MH_OK)
{
Console.WriteLine($"MH_CreateHook failed: {status}");
return;
} _originalSendMessageW = Marshal.GetDelegateForFunctionPointer<SendMessageWDelegate>(originalPtr); // 4. 启用 Hook
status = MinHook.MH_EnableHook(_sendMessageWPtr);
if (status != MinHook.MH_STATUS.MH_OK)
{
Console.WriteLine($"MH_EnableHook failed: {status}");
return;
} Console.WriteLine("SendMessageW hook installed successfully!");
} public static void UninstallHook()
{
if (_sendMessageWPtr == IntPtr.Zero)
return; // 1. 禁用 Hook
var status = MinHook.MH_DisableHook(_sendMessageWPtr);
if (status != MinHook.MH_STATUS.MH_OK)
Console.WriteLine($"MH_DisableHook failed: {status}"); // 2. 卸载 MinHook
status = MinHook.MH_Uninitialize();
if (status != MinHook.MH_STATUS.MH_OK)
Console.WriteLine($"MH_Uninitialize failed: {status}"); _sendMessageWPtr = IntPtr.Zero;
Console.WriteLine("Hook uninstalled.");
} private static IntPtr HookedSendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam)
{
Console.WriteLine($"[HOOK] SendMessageW: hWnd=0x{hWnd.ToInt64():X}, Msg=0x{Msg:X}"); // 获取窗口所属的线程和进程ID
uint processId = 0;
uint threadId = GetWindowThreadProcessId(hWnd, out processId); // 使用 System.Diagnostics.Process 获取进程信息
string processName = "Unknown";
try
{
var targetProcess = System.Diagnostics.Process.GetProcessById((int)processId);
processName = targetProcess.ProcessName; Console.WriteLine($"Window belongs to - ThreadID: {threadId}, ProcessID: {processId}, ProcessName: {processName}");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
} // 调用原始函数
return _originalSendMessageW(hWnd, Msg, wParam, lParam);
} // 需要的Win32 API声明
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
} public static class MinHook
{
public enum MH_STATUS
{
MH_OK = 0,
MH_ERROR_ALREADY_INITIALIZED,
MH_ERROR_NOT_INITIALIZED,
// ... 其他状态码
} [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern MH_STATUS MH_Initialize(); [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern MH_STATUS MH_Uninitialize(); [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern MH_STATUS MH_CreateHook(IntPtr pTarget, IntPtr pDetour, out IntPtr ppOriginal); [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern MH_STATUS MH_EnableHook(IntPtr pTarget); [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern MH_STATUS MH_DisableHook(IntPtr pTarget); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
}
}

最核心的代码是上面的 HookedSendMessageW,大家可以多品鉴品鉴,接下来依次运行 WindowsProject1ConsoleApplication 程序,输出如下:

从输出看,是不是一下子就把排查范围缩小了很多,最起码我知道是一个叫 WindowsProject1 的进程坏了我的好事,后续就可以针对 WindowsProject1 深入探究为何方神物。。。

3. 还能更完美一点吗

虽然排查范围极大的缩小了,还但是有一点不完美,如果这个窗口是本进程创建的还好,如果不是本进程创建的,最好能抓到对方进程的dump那就真完美了。。。

接下来的问题是怎么抓对方进程的dump呢?为了确保通用性,我建议在本进程中调 procdump 自动捕获,参考代码如下:


namespace ConsoleApplication
{
public class DumpGen
{
// 生成进程 Dump 文件
public static void GenerateProcessDump(int processId, string dumpPath)
{
try
{
// ProcDump 命令行参数:
// -mm: 生成 MiniDump
// -accepteula: 自动接受许可协议(避免首次运行时弹出提示)
string procDumpPath = $@"{Environment.CurrentDirectory}\procdump.exe";
string arguments = $"-accepteula -mm {processId} \"{dumpPath}\""; var startInfo = new ProcessStartInfo
{
FileName = procDumpPath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
}; using (var proc = new Process { StartInfo = startInfo })
{
proc.Start();
proc.WaitForExit();
Console.WriteLine("Dump captured successfully");
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to launch ProcDump: {ex.Message}");
}
}
}
}

然后修改下 HookedSendMessageW 方法,如果 _originalSendMessageW 超时,将会自动抓取dump,当然这里只是一个简单的演示,更复杂的逻辑大家可以根据自己的情况编写,比如用一个 字典 来存放 hWnd,然后根据超时时间自动的抓取进程的dump,参考代码如下:


private static IntPtr HookedSendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam)
{
Console.WriteLine($"[HOOK] SendMessageW: hWnd=0x{hWnd.ToInt64():X}, Msg=0x{Msg:X}"); // 获取窗口所属的线程和进程ID
uint processId = 0;
uint threadId = GetWindowThreadProcessId(hWnd, out processId); // 使用 System.Diagnostics.Process 获取进程信息
string processName = "Unknown";
try
{
var targetProcess = System.Diagnostics.Process.GetProcessById((int)processId);
processName = targetProcess.ProcessName; Console.WriteLine($"Window belongs to - ThreadID: {threadId}, ProcessID: {processId}, ProcessName: {processName}"); //定时检测代码:如果超时自动抓取dump
Task.Run(() =>
{
Thread.Sleep(3000); if (Msg == 0x0010)
{
string dumpPath = Path.Combine(Environment.CurrentDirectory, $"ProcessDump_{processName}_{DateTime.Now:yyyyMMddHHmmss}.dmp"); DumpGen.GenerateProcessDump(targetProcess.Id, dumpPath); Console.WriteLine($"Launching ProcDump to generate dump: {dumpPath}");
}
});
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
} // 调用原始函数
return _originalSendMessageW(hWnd, Msg, wParam, lParam);
}

一切都搞定之后,运行下程序,截图如下:

打开生成好的dump文件,找到目标线程,参考如下:


0:000> ~
. 0 Id: 4f34.338c Suspend: 0 Teb: 009c2000 Unfrozen
1 Id: 4f34.6470 Suspend: 0 Teb: 009d2000 Unfrozen
2 Id: 4f34.62a8 Suspend: 0 Teb: 009d6000 Unfrozen
0:000> ? 4f34 ; ? 338c; k
Evaluate expression: 20276 = 00004f34
Evaluate expression: 13196 = 0000338c
# ChildEBP RetAddr
00 00b3f910 77a23999 ntdll!NtDelayExecution+0xc
01 00b3f930 776a8760 ntdll!RtlDelayExecution+0xe9
02 00b3f998 776a86ff KERNELBASE!SleepEx+0x50
03 00b3f9a8 00f81be3 KERNELBASE!Sleep+0xf
04 00b3fae8 76b36d13 WindowsProject1!WndProc+0x43 [D:\sources\woodpecker\ConsoleApplication\WindowsProject1\WindowsProject1.cpp @ 127]
05 00b3fb14 76b2540d user32!_InternalCallWinProc+0x2b
06 00b3fc18 76b24eb0 user32!UserCallWinProcCheckWow+0x49d
07 00b3fc7c 76b31709 user32!DispatchClientMessage+0x190
08 00b3fcb8 77a0bb66 user32!__fnDWORD+0x39
09 00b3fcf0 76b33ef0 ntdll!KiUserCallbackDispatcher+0x36
0a 00b3fd2c 00f81e9b user32!GetMessageW+0x30
0b 00b3fe44 00f8273d WindowsProject1!wWinMain+0xbb [D:\sources\woodpecker\ConsoleApplication\WindowsProject1\WindowsProject1.cpp @ 46]
0c 00b3fe64 00f8258a WindowsProject1!invoke_main+0x2d [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 123]
0d 00b3fec0 00f8241d WindowsProject1!__scrt_common_main_seh+0x15a [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
0e 00b3fec8 00f827b8 WindowsProject1!__scrt_common_main+0xd [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331]
0f 00b3fed0 76705d49 WindowsProject1!wWinMainCRTStartup+0x8 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_wwinmain.cpp @ 17]
10 00b3fee0 779fcebb kernel32!BaseThreadInitThunk+0x19
11 00b3ff38 779fce41 ntdll!__RtlUserThreadStart+0x2b
12 00b3ff48 00000000 ntdll!_RtlUserThreadStart+0x1b

从卦中看,原来卡死是因为主线程正在 KERNELBASE!Sleep,无语了,到此为止,这次卡死事故真相大白于天下。

三:总结

再回头看文章开头的 cogxImagingDevice.dll 导致的程序卡死,如果用本篇的解决方案,是不是非常的轻量级,从此以后再也不需要抓内核的dump,也不需要在客户的电脑上用 spy++ 捣鼓来捣鼓去了。。。完美!

MinHook 对.NET底层的 SendMessage 拦截真实案例反思的更多相关文章

  1. Andrdoid中对应用程序的行为拦截实现方式之----从底层C进行拦截

    之前的一篇概要文章中主要说了我这次研究的一些具体情况,这里就不在多说了,但是这里还需要指出的是,感谢一下三位大神愿意分享的知识(在我看来,懂得分享和细致的人才算是大神,不一定是技术牛奥~~) 第一篇: ...

  2. ENode 2.0 - 第一个真实案例剖析-一个简易论坛(Forum)

    前言 经过不断的坚持和努力,ENode 2.0的第一个真实案例终于出来了.这个案例是一个简易的论坛,开发这个论坛的初衷是为了验证用ENode框架来开发一个真实项目的可行性.目前这个论坛在UI上是使用了 ...

  3. 利用UDP19端口实施DOS攻击的真实案例

    昨天在一个用户现场发现了一个利用UDP19端口对互联网受害者主机进行DOS攻击的真实案例.这个情况是我第一次见到,个人认为对以后遇到此类情况的兄弟具有参考价值.有必要做一个简单的分析记录. 在此次的分 ...

  4. JavaScript写的随机选人真实案例

    JavaScript写的随机选人真实案例 因工作需要,写了一个随机选人的小网页,先看效果图. 背景也是动态的,只不过在写的时候碰到个问题,就是如果把生成动态流星雨的画布放到上生成随机数的操作界面之上的 ...

  5. 又一起.NET程序挂死, 用 Windbg 抽丝剥茧式的真实案例分析

    一:背景 1. 讲故事 前天有位粉丝朋友在后台留言让我帮忙看看他的 Winform程序 UI无响应 + 410线程 到底是啥情况,如下图: 说实话,能看到这些真实案例我是特别喜欢的 ,就像医生看病,光 ...

  6. openvpn用户管理、linux客户端配置及企业常用真实案例解析

    1.给企业用户分配VPN账户的流程: 添加拨号需要密码的用户 # source vars NOTE: If you run ./clean-all, I will be doing a rm -rf ...

  7. mybatis拦截器案例之获取结果集总条数

    最近做的项目前端是外包出去的,所以在做查询分页的时候比较麻烦 我们需要先吧结果集的条数返回给前端,然后由前端根据页面情况(当前页码,每页显示条数)将所需参数传到后端. 由于在项目搭建的时候,是没有考虑 ...

  8. 一文拆解Faas的真实案例

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文来自腾讯云技术沙龙,本次沙龙主题为Serverless架构开发与SCF部署实践 刘敏洁:具有多年云计算行业经验,曾任职于华为.UClou ...

  9. RestTemplate真实案例

    1. 场景描述 现在越来越的系统之间的交互采用http+json的交互方式,以前用的比较多的HttpClient,后来用的RestTemplate,感觉RestTemplate要比httpClent简 ...

  10. 二十 Spring的事务管理及其API&事务的传播行为,编程式&声明式(xml式&注解式,底层AOP),转账案例

    Spring提供两种事务方式:编程式和声明式(重点) 前者需要手写代码,后者通过配置实现. 事务的回顾: 事务:逻辑上的一组操作,组成这组事务的各个单元,要么全部成功,要么全部失败 事务的特性:ACI ...

随机推荐

  1. Delphi 模糊查询和字段查询

    procedure TFrmain.scGPEdit1Change(Sender: TObject); var ASql, AKey: string; //模糊查询和字段查询 const vsql1: ...

  2. AI与.NET技术实操系列(九):总结篇 ── 探讨.NET 开发 AI 生态:工具、库与未来趋势

    1. 引言 本文作为本系列的最后一篇,旨在全面探讨 .NET 生态中与 AI 相关的工具.库.框架和资源,帮助开发者了解如何在 .NET 环境中开发 AI 应用.我们将分析 Microsoft 的 A ...

  3. Docker,vs2019下 使用.net core创建docker镜像 遇到的一些问题

      步骤主要分为以下几步: 1.创建docker for linux 的.netcore 项目(vs 自动创建了dockerfile 如果没有需要自己创建在根目录下) 2.编译项目到指定目录下 3.b ...

  4. 初识if,if的三种结构

    1.if语句 流程控制语句:通过一语句,来控制程序的执行流程.其中if属于分支结构 2.if语句的第一种格式 . 实操: 3.if的第二种格式 实操: 4.if的第三种格式 实操: 5.注意事项 在i ...

  5. wrk

    github.com/wg/wrk 是一个现代的 HTTP 基准测试工具.

  6. 从客户端(XXX)中检测到有潜在危险的 Request.Form 值

    维护别人的某功能模块的时候,页面返回如下错误信息: [HttpRequestValidationException (0x80004005): 从客户端(TextBox1="<?xml ...

  7. Web前端入门第 23 问:CSS 选择器的优先级

    任何地方都存在阶级,CSS 选择器也不例外,也会讲一个三六九等. 选择器类别 通配符选择器 标签选择器 类选择器 ID选择器 属性选择器 伪类选择器 伪元素选择器 关系选择器 流传已久的阶级划分 选择 ...

  8. Maven导包报错Could not resolve dependencies for projectXXX was cached in the local repository....

    问题 将项目和maven仓库一起拿到了内网环境,一直报错无法解析依赖was cached in the local repository, resolution will not be reattem ...

  9. PHP传递参数(跨文件)的8种常见方法

    以下是 PHP 中跨文件传递参数的 8 种常见方法,按场景和安全性分类整理,附详细说明和示例代码: 一.超全局变量(适合请求间数据共享) 1. $_GET / $_POST 用途:通过 URL 或表单 ...

  10. Asp.net mvc基础(九)使用DropDownList下拉列表

    第一种下拉列表写法: 后端 前端 第二种下拉列表写法: 使用Html辅助方法@Html.DropDownList("名称","List<SelectListItem ...