找回密码
 立即注册
首页 业界区 业界 MinHook 对.NET底层的 SendMessage 拦截真实案例反思 ...

MinHook 对.NET底层的 SendMessage 拦截真实案例反思

痕厄 2025-6-10 12:58:20
一:背景

1. 讲故事

上一篇我们说到了 minhook 的一个简单使用,这一篇给大家分享一个 minhook 在 dump 分析中的实战,先看下面的线程栈。
  1. 0:044> ~~[138c]s
  2. win32u!NtUserMessageCall+0x14:
  3. 00007ffc`5c891184 c3              ret
  4. 0:061> k
  5. # Child-SP          RetAddr               Call Site
  6. 00 0000008c`00ffec68 00007ffc`5f21bfbe     win32u!NtUserMessageCall+0x14
  7. 01 0000008c`00ffec70 00007ffc`5f21be38     user32!SendMessageWorker+0x11e
  8. 02 0000008c`00ffed10 00007ffc`124fd4af     user32!SendMessageW+0xf8
  9. 03 0000008c`00ffed70 00007ffc`125e943b     cogxImagingDevice!DllUnregisterServer+0x3029f
  10. 04 0000008c`00ffeda0 00007ffc`125e9685     cogxImagingDevice!DllUnregisterServer+0x11c22b
  11. 05 0000008c`00ffede0 00007ffc`600b50e7     cogxImagingDevice!DllUnregisterServer+0x11c475
  12. 06 0000008c`00ffee20 00007ffc`60093ccd     ntdll!LdrpCallInitRoutine+0x6f
  13. 07 0000008c`00ffee90 00007ffc`60092eef     ntdll!LdrpProcessDetachNode+0xf5
  14. 08 0000008c`00ffef60 00007ffc`600ae319     ntdll!LdrpUnloadNode+0x3f
  15. 09 0000008c`00ffefb0 00007ffc`600ae293     ntdll!LdrpDecrementModuleLoadCountEx+0x71
  16. 0a 0000008c`00ffefe0 00007ffc`5cd7c00e     ntdll!LdrUnloadDll+0x93
  17. 0b 0000008c`00fff010 00007ffc`5d47cf78     KERNELBASE!FreeLibrary+0x1e
  18. 0c 0000008c`00fff040 00007ffc`5d447aa3     combase!CClassCache::CDllPathEntry::CFinishObject::Finish+0x28 [onecore\com\combase\objact\dllcache.cxx @ 3420]
  19. 0d 0000008c`00fff070 00007ffc`5d4471a9     combase!CClassCache::CFinishComposite::Finish+0x4b [onecore\com\combase\objact\dllcache.cxx @ 3530]
  20. 0e 0000008c`00fff0a0 00007ffc`5d3f1499     combase!CClassCache::FreeUnused+0xdd [onecore\com\combase\objact\dllcache.cxx @ 6547]
  21. 0f 0000008c`00fff650 00007ffc`5d3f13c7     combase!CoFreeUnusedLibrariesEx+0x89 [onecore\com\combase\objact\dllapi.cxx @ 117]
  22. 10 (Inline Function) --------`--------     combase!CoFreeUnusedLibraries+0xa [onecore\com\combase\objact\dllapi.cxx @ 74]
  23. 11 0000008c`00fff690 00007ffc`6008a019     combase!CDllHost::MTADllUnloadCallback+0x17 [onecore\com\combase\objact\dllhost.cxx @ 929]
  24. 12 0000008c`00fff6c0 00007ffc`6008bec4     ntdll!TppTimerpExecuteCallback+0xa9
  25. 13 0000008c`00fff710 00007ffc`5f167e94     ntdll!TppWorkerThread+0x644
  26. 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.png

二:寻找解决方案

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 中故意让程序卡死,参考代码如下:
  1. LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  2. {
  3.         if (message == WM_CLOSE) {
  4.                 Sleep(1000 * 1000);
  5.         }
  6.     // todo....
  7.         return 0;
  8. }
复制代码
接下来新建一个 ConsoleApplication 控制台程序,通过 SendMessage 给 WindowsProject1 打close消息,来演示无故卡死,完整的代码如下:
  1. using System;
  2. using System.Runtime.InteropServices;
  3. using System.Text;
  4. namespace ConsoleApplication
  5. {
  6.     public static class Program
  7.     {
  8.         private const uint WM_CLOSE = 0x0010;
  9.         public static void Main()
  10.         {
  11.             // 安装 Hook
  12.             HookManager.InstallHook();
  13.             // 测试:发送 WM_CLOSE 消息(会触发 Hook)
  14.             IntPtr hWnd = FindWindow(null, "WindowsProject1");
  15.             if (hWnd != IntPtr.Zero)
  16.             {
  17.                 SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
  18.                 Console.WriteLine("Sent WM_CLOSE to target window.");
  19.             }
  20.             else
  21.             {
  22.                 Console.WriteLine("Target window not found.");
  23.             }
  24.             Console.ReadKey();
  25.             // 卸载 Hook
  26.             HookManager.UninstallHook();
  27.         }
  28.         [DllImport("user32.dll", CharSet = CharSet.Unicode)]
  29.         private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
  30.         [DllImport("user32.dll", CharSet = CharSet.Unicode)]
  31.         private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
  32.     }
  33.     public static class HookManager
  34.     {
  35.         // SendMessageW 的原始函数签名
  36.         [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)]
  37.         private delegate IntPtr SendMessageWDelegate(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
  38.         private static SendMessageWDelegate _originalSendMessageW;
  39.         private static IntPtr _sendMessageWPtr = IntPtr.Zero;
  40.         public static void InstallHook()
  41.         {
  42.             // 1. 获取 SendMessageW 的地址
  43.             _sendMessageWPtr = MinHook.GetProcAddress(
  44.                 MinHook.GetModuleHandle("user32.dll"), "SendMessageW");
  45.             if (_sendMessageWPtr == IntPtr.Zero)
  46.             {
  47.                 Console.WriteLine("Failed to find SendMessageW address.");
  48.                 return;
  49.             }
  50.             // 2. 初始化 MinHook
  51.             var status = MinHook.MH_Initialize();
  52.             if (status != MinHook.MH_STATUS.MH_OK)
  53.             {
  54.                 Console.WriteLine($"MH_Initialize failed: {status}");
  55.                 return;
  56.             }
  57.             // 3. 创建 Hook
  58.             var detourPtr = Marshal.GetFunctionPointerForDelegate(
  59.                 new SendMessageWDelegate(HookedSendMessageW));
  60.             status = MinHook.MH_CreateHook(_sendMessageWPtr, detourPtr, out var originalPtr);
  61.             if (status != MinHook.MH_STATUS.MH_OK)
  62.             {
  63.                 Console.WriteLine($"MH_CreateHook failed: {status}");
  64.                 return;
  65.             }
  66.             _originalSendMessageW = Marshal.GetDelegateForFunctionPointer<SendMessageWDelegate>(originalPtr);
  67.             // 4. 启用 Hook
  68.             status = MinHook.MH_EnableHook(_sendMessageWPtr);
  69.             if (status != MinHook.MH_STATUS.MH_OK)
  70.             {
  71.                 Console.WriteLine($"MH_EnableHook failed: {status}");
  72.                 return;
  73.             }
  74.             Console.WriteLine("SendMessageW hook installed successfully!");
  75.         }
  76.         public static void UninstallHook()
  77.         {
  78.             if (_sendMessageWPtr == IntPtr.Zero)
  79.                 return;
  80.             // 1. 禁用 Hook
  81.             var status = MinHook.MH_DisableHook(_sendMessageWPtr);
  82.             if (status != MinHook.MH_STATUS.MH_OK)
  83.                 Console.WriteLine($"MH_DisableHook failed: {status}");
  84.             // 2. 卸载 MinHook
  85.             status = MinHook.MH_Uninitialize();
  86.             if (status != MinHook.MH_STATUS.MH_OK)
  87.                 Console.WriteLine($"MH_Uninitialize failed: {status}");
  88.             _sendMessageWPtr = IntPtr.Zero;
  89.             Console.WriteLine("Hook uninstalled.");
  90.         }
  91.         private static IntPtr HookedSendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam)
  92.         {
  93.             Console.WriteLine($"[HOOK] SendMessageW: hWnd=0x{hWnd.ToInt64():X}, Msg=0x{Msg:X}");
  94.             // 获取窗口所属的线程和进程ID
  95.             uint processId = 0;
  96.             uint threadId = GetWindowThreadProcessId(hWnd, out processId);
  97.             // 使用 System.Diagnostics.Process 获取进程信息
  98.             string processName = "Unknown";
  99.             try
  100.             {
  101.                 var targetProcess = System.Diagnostics.Process.GetProcessById((int)processId);
  102.                 processName = targetProcess.ProcessName;
  103.                 Console.WriteLine($"Window belongs to - ThreadID: {threadId}, ProcessID: {processId}, ProcessName: {processName}");
  104.             }
  105.             catch (Exception ex)
  106.             {
  107.                 Console.WriteLine(ex.Message);
  108.             }
  109.             // 调用原始函数
  110.             return _originalSendMessageW(hWnd, Msg, wParam, lParam);
  111.         }
  112.         // 需要的Win32 API声明
  113.         [DllImport("user32.dll", SetLastError = true)]
  114.         static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
  115.     }
  116.     public static class MinHook
  117.     {
  118.         public enum MH_STATUS
  119.         {
  120.             MH_OK = 0,
  121.             MH_ERROR_ALREADY_INITIALIZED,
  122.             MH_ERROR_NOT_INITIALIZED,
  123.             // ... 其他状态码
  124.         }
  125.         [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
  126.         public static extern MH_STATUS MH_Initialize();
  127.         [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
  128.         public static extern MH_STATUS MH_Uninitialize();
  129.         [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
  130.         public static extern MH_STATUS MH_CreateHook(IntPtr pTarget, IntPtr pDetour, out IntPtr ppOriginal);
  131.         [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
  132.         public static extern MH_STATUS MH_EnableHook(IntPtr pTarget);
  133.         [DllImport("MinHook.x86.dll", CallingConvention = CallingConvention.Cdecl)]
  134.         public static extern MH_STATUS MH_DisableHook(IntPtr pTarget);
  135.         [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
  136.         public static extern IntPtr GetModuleHandle(string lpModuleName);
  137.         [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
  138.         public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
  139.     }
  140. }
复制代码
最核心的代码是上面的 HookedSendMessageW,大家可以多品鉴品鉴,接下来依次运行 WindowsProject1 和 ConsoleApplication 程序,输出如下:
2.png

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

虽然排查范围极大的缩小了,还但是有一点不完美,如果这个窗口是本进程创建的还好,如果不是本进程创建的,最好能抓到对方进程的dump那就真完美了。。。
接下来的问题是怎么抓对方进程的dump呢?为了确保通用性,我建议在本进程中调 procdump 自动捕获,参考代码如下:
  1. namespace ConsoleApplication
  2. {
  3.     public class DumpGen
  4.     {
  5.         // 生成进程 Dump 文件
  6.         public static void GenerateProcessDump(int processId, string dumpPath)
  7.         {
  8.             try
  9.             {
  10.                 // ProcDump 命令行参数:
  11.                 // -mm: 生成 MiniDump
  12.                 // -accepteula: 自动接受许可协议(避免首次运行时弹出提示)
  13.                 string procDumpPath = $@"{Environment.CurrentDirectory}\procdump.exe";
  14.                 string arguments = $"-accepteula -mm {processId} "{dumpPath}"";
  15.                 var startInfo = new ProcessStartInfo
  16.                 {
  17.                     FileName = procDumpPath,
  18.                     Arguments = arguments,
  19.                     UseShellExecute = false,
  20.                     CreateNoWindow = true,
  21.                     RedirectStandardOutput = true,
  22.                     RedirectStandardError = true
  23.                 };
  24.                 using (var proc = new Process { StartInfo = startInfo })
  25.                 {
  26.                     proc.Start();
  27.                     proc.WaitForExit();
  28.                     Console.WriteLine("Dump captured successfully");
  29.                 }
  30.             }
  31.             catch (Exception ex)
  32.             {
  33.                 Console.WriteLine($"Failed to launch ProcDump: {ex.Message}");
  34.             }
  35.         }
  36.     }
  37. }
复制代码
然后修改下 HookedSendMessageW 方法,如果 _originalSendMessageW 超时,将会自动抓取dump,当然这里只是一个简单的演示,更复杂的逻辑大家可以根据自己的情况编写,比如用一个 字典 来存放 hWnd,然后根据超时时间自动的抓取进程的dump,参考代码如下:
  1. private static IntPtr HookedSendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam)
  2. {
  3.     Console.WriteLine($"[HOOK] SendMessageW: hWnd=0x{hWnd.ToInt64():X}, Msg=0x{Msg:X}");
  4.     // 获取窗口所属的线程和进程ID
  5.     uint processId = 0;
  6.     uint threadId = GetWindowThreadProcessId(hWnd, out processId);
  7.     // 使用 System.Diagnostics.Process 获取进程信息
  8.     string processName = "Unknown";
  9.     try
  10.     {
  11.         var targetProcess = System.Diagnostics.Process.GetProcessById((int)processId);
  12.         processName = targetProcess.ProcessName;
  13.         Console.WriteLine($"Window belongs to - ThreadID: {threadId}, ProcessID: {processId}, ProcessName: {processName}");
  14.         //定时检测代码:如果超时自动抓取dump
  15.         Task.Run(() =>
  16.         {
  17.             Thread.Sleep(3000);
  18.             if (Msg == 0x0010)
  19.             {
  20.                 string dumpPath = Path.Combine(Environment.CurrentDirectory, $"ProcessDump_{processName}_{DateTime.Now:yyyyMMddHHmmss}.dmp");
  21.                 DumpGen.GenerateProcessDump(targetProcess.Id, dumpPath);
  22.                 Console.WriteLine($"Launching ProcDump to generate dump: {dumpPath}");
  23.             }
  24.         });
  25.     }
  26.     catch (Exception ex)
  27.     {
  28.         Console.WriteLine(ex.Message);
  29.     }
  30.     // 调用原始函数
  31.     return _originalSendMessageW(hWnd, Msg, wParam, lParam);
  32. }
复制代码
一切都搞定之后,运行下程序,截图如下:
3.png

打开生成好的dump文件,找到目标线程,参考如下:
  1. 0:000> ~
  2. .  0  Id: 4f34.338c Suspend: 0 Teb: 009c2000 Unfrozen
  3.    1  Id: 4f34.6470 Suspend: 0 Teb: 009d2000 Unfrozen
  4.    2  Id: 4f34.62a8 Suspend: 0 Teb: 009d6000 Unfrozen
  5. 0:000> ? 4f34 ; ? 338c; k
  6. Evaluate expression: 20276 = 00004f34
  7. Evaluate expression: 13196 = 0000338c
  8. # ChildEBP RetAddr      
  9. 00 00b3f910 77a23999     ntdll!NtDelayExecution+0xc
  10. 01 00b3f930 776a8760     ntdll!RtlDelayExecution+0xe9
  11. 02 00b3f998 776a86ff     KERNELBASE!SleepEx+0x50
  12. 03 00b3f9a8 00f81be3     KERNELBASE!Sleep+0xf
  13. 04 00b3fae8 76b36d13     WindowsProject1!WndProc+0x43 [D:\sources\woodpecker\ConsoleApplication\WindowsProject1\WindowsProject1.cpp @ 127]
  14. 05 00b3fb14 76b2540d     user32!_InternalCallWinProc+0x2b
  15. 06 00b3fc18 76b24eb0     user32!UserCallWinProcCheckWow+0x49d
  16. 07 00b3fc7c 76b31709     user32!DispatchClientMessage+0x190
  17. 08 00b3fcb8 77a0bb66     user32!__fnDWORD+0x39
  18. 09 00b3fcf0 76b33ef0     ntdll!KiUserCallbackDispatcher+0x36
  19. 0a 00b3fd2c 00f81e9b     user32!GetMessageW+0x30
  20. 0b 00b3fe44 00f8273d     WindowsProject1!wWinMain+0xbb [D:\sources\woodpecker\ConsoleApplication\WindowsProject1\WindowsProject1.cpp @ 46]
  21. 0c 00b3fe64 00f8258a     WindowsProject1!invoke_main+0x2d [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 123]
  22. 0d 00b3fec0 00f8241d     WindowsProject1!__scrt_common_main_seh+0x15a [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
  23. 0e 00b3fec8 00f827b8     WindowsProject1!__scrt_common_main+0xd [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331]
  24. 0f 00b3fed0 76705d49     WindowsProject1!wWinMainCRTStartup+0x8 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_wwinmain.cpp @ 17]
  25. 10 00b3fee0 779fcebb     kernel32!BaseThreadInitThunk+0x19
  26. 11 00b3ff38 779fce41     ntdll!__RtlUserThreadStart+0x2b
  27. 12 00b3ff48 00000000     ntdll!_RtlUserThreadStart+0x1b
复制代码
从卦中看,原来卡死是因为主线程正在 KERNELBASE!Sleep,无语了,到此为止,这次卡死事故真相大白于天下。
三:总结

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


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册