记一次 .NET某工控WPF程序被人恶搞的 卡死分析

net,wpf · 浏览次数 : 0

小编点评

**WaitSuspendEventsHelper 函数的代码解析:** WaitSuspendEventsHelper 函数是一个异步方法,它用于处理线程状态中的 `TS_DebugSuspendPending` 状态。该状态表示线程被调试器锁定,无法执行任何操作。 **代码分析:** 1. **判断当前状态**: - `if (m_State & TS_DebugSuspendPending)` 检查 `m_State` 中是否包含 `TS_DebugSuspendPending` 状态。 2. **进入循环**: - 如果 `TS_DebugSuspendPending` 状态中存在,则进入循环。 3. **获取旧状态**: - `oldState = m_State;` 获取 `m_State` 的旧值。 4. **尝试锁定旧状态**: - `if (InterlockedCompareExchange((LONG*)&m_State, newState, oldState) == (LONG)oldState)` 检查是否成功锁定旧状态。 - 如果锁定成功,则表示线程已经成功从调试器中获取并处理了事件。 5. **设置结果**: - 如果锁定失败,则设置 `result` 为 `WAIT_OBJECT_0`,表示线程没有完成等待操作。 6. **退出循环**: - 如果线程成功从调试器中获取并处理了事件,则退出循环。 7. **返回值**: - 返回 `result` 的值,表示线程的状态。 **调试器信息:** - `Debugger.IsAttached`:用于检查线程是否与调试器连接。 - `Debugger.IsDebuggerPresent()`:用于检查是否有调试器连接。 - `g_CORDebuggerControlFlags`:用于设置托管调试器的控制标志。 **其他信息:** - `WaitSuspendEventsHelper` 方法是线程的异步方法,因此需要使用 `InterlockedCompareExchange` 等方法来锁定旧状态。 - 如果调试器未连接,`IsDebuggerPresent()` 将返回 `false`,`WaitSuspendEventsHelper` 将返回 `WAIT_OBJECT_0`。 - 如果线程已从调试器中获取并处理了事件,`WaitSuspendEventsHelper` 将立即返回 `WAIT_OBJECT_0`。

正文

一:背景

1. 讲故事

这一期程序故障除了做原理分析,还顺带吐槽一下,熟悉我的朋友都知道我分析dump是免费的,但免费不代表可以滥用我的宝贵时间,我不知道有些人故意恶搞卡死是想干嘛,不得而知,希望后面类似的事情越来越少吧!废话不多说,我们来看看是如何被恶搞的。

二:WinDbg 分析

1. 程序是如何卡死的

既然是窗体程序自然就是看主线程,我们使用 k 命令即可。


0:000> k 
 # Child-SP          RetAddr               Call Site
00 00000036`e1f7da18 00007ffe`70bf30ce     ntdll!NtWaitForSingleObject+0x14
01 00000036`e1f7da20 00007ffd`eff74910     KERNELBASE!WaitForSingleObjectEx+0x8e
...
05 (Inline Function) --------`--------     coreclr!CLREventBase::Wait+0x12 [D:\a\_work\1\s\src\coreclr\vm\synch.cpp @ 412] 
06 00000036`e1f7db20 00007ffd`f00c9afa     coreclr!Thread::WaitSuspendEventsHelper+0xc8 [D:\a\_work\1\s\src\coreclr\vm\threadsuspend.cpp @ 4626] 
07 (Inline Function) --------`--------     coreclr!Thread::WaitSuspendEvents+0x8 [D:\a\_work\1\s\src\coreclr\vm\threadsuspend.cpp @ 4663] 
08 00000036`e1f7dbe0 00007ffd`f0009c80     coreclr!Thread::RareEnablePreemptiveGC+0x7d99a [D:\a\_work\1\s\src\coreclr\vm\threadsuspend.cpp @ 2414] 
09 (Inline Function) --------`--------     coreclr!Thread::EnablePreemptiveGC+0x16 [D:\a\_work\1\s\src\coreclr\vm\threads.h @ 2044] 
0a 00000036`e1f7dc20 00007ffd`f0075d10     coreclr!Thread::RareDisablePreemptiveGC+0xc8 [D:\a\_work\1\s\src\coreclr\vm\threadsuspend.cpp @ 2156] 
0b 00000036`e1f7dca0 00007ffd`f0096a5f     coreclr!JIT_ReversePInvokeEnterRare2+0x18 [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 5462] 
0c 00000036`e1f7dcd0 00007ffd`907afe09     coreclr!JIT_ReversePInvokeEnterTrackTransitions+0xaf [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 5509] 
0d 00000036`e1f7dd00 00007ffe`72e0e858     0x00007ffd`907afe09
0e 00000036`e1f7dd80 00007ffe`72e0e3dc     user32!UserCallWinProcCheckWow+0x2f8
0f 00000036`e1f7df10 00007ffe`72e20c93     user32!DispatchClientMessage+0x9c
10 00000036`e1f7df70 00007ffe`730f0e64     user32!_fnDWORD+0x33
11 00000036`e1f7dfd0 00007ffe`70b31104     ntdll!KiUserCallbackDispatcherContinue
12 00000036`e1f7e058 00007ffe`72e21c0e     win32u!NtUserGetMessage+0x14
13 00000036`e1f7e060 00007ffe`711e28f2     user32!GetMessageW+0x2e
...
1d 00000036`e1f7e460 00007ffd`f009ba53     xxx!xxx.App.Main+0x5e
...
2a 00000036`e1f7f140 00007ffe`4cd4e1e6     hostfxr!execute_app+0x2fa [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 145] 
...
34 00000036`e1f7f890 00000000`00000000     ntdll!RtlUserThreadStart+0x21

从卦中的调用栈可以看到,主线程通过 GetMessageW 从消息队列中拿到了数据,在处理时被 coreclr 押解到 WaitSuspendEventsHelper 处等待一个event事件,接下来看下这个方法的源码到底是怎么写的?简化后如下:


BOOL Thread::WaitSuspendEventsHelper(void)
{
    if (m_State & TS_DebugSuspendPending)
    {

        ThreadState oldState = m_State;

        while (oldState & TS_DebugSuspendPending)
        {

            ThreadState newState = (ThreadState)(oldState | TS_SyncSuspended);

            if (InterlockedCompareExchange((LONG*)&m_State, newState, oldState) == (LONG)oldState)
            {
                result = m_DebugSuspendEvent.Wait(INFINITE, FALSE);
                break;
            }

            oldState = m_State;
        }
    }
    return result != WAIT_OBJECT_0;
}

从上面的代码可以明显的看到当前Thread 处于 TS_DebugSuspendPending 状态,从名字上看貌似和调试有关,接下来查下这个枚举的注解。


enum ThreadState
{
    TS_DebugSuspendPending = 0x00000008,    // Is the debugger suspending threads?
}

从注解看是因为调试器阻塞了这个线程,我去,哪里来的调试器呢?这引起了我的巨大兴趣。。。

2. 哪里来的调试器

我刚开始是抱有巨大的善意,我以为他的程序被人恶意注入导致卡死,但分析下来我还是太单纯了,继续往下看吧,既然有调试器,一般来说PEB.BeingDebugged=Yes状态,命令的输出结果也得到了证实。


0:000> !peb
PEB at 00000036e1c11000
    ...
    BeingDebugged:            Yes
    ...
                    Base TimeStamp                     Module
            7ff7f5230000 65310000 Oct 19 18:08:00 2023 G:\xxx\C#\Projects\NugetApplications\Applications\xxx\bin\Debug\net6.0-windows\xxx.exe
            ...

我以为到这里就可以告诉朋友答案,你的程序可能被人恶意注入了,但眼尖的我发现了一点异常,他的 xxx.exe 启动目录怎么会有 \bin\Debug\net6.0-windows\ 呢? 很明显这是用 VS 直接跑起来的呀。。。。

3. 真的是 VS 跑起来的吗

首先大家要知道 Visual Studio 是托管调试器,托管调试器在调试程序的时候会在 coreclr 层面设置一个全局变量 g_CORDebuggerControlFlags=0x0200,这也是 Debugger.IsAttached 的底层判断依据,不相信的朋友可以看下这个方法的底层实现:


FCIMPL0(FC_BOOL_RET, DebugDebugger::IsDebuggerAttached)
{
    CORDebuggerAttached();
}

inline bool CORDebuggerAttached()
{
    return (g_CORDebuggerControlFlags & DBCF_ATTACHED);
}

enum DebuggerControlFlag
{
    DBCF_ATTACHED                   = 0x0200,
};

有了这些基础知识之后,接下来就可以用 x 命令去验证下。

看到上面的答案基本就可以实锤了,有些朋友可能还是不大相信,我们可以这样看,VisualStudio 是 WPF 写的,如果用 VS 来调试,那么线程栈里面应该会有很多类似的 VisualStudio 字符串。

从卦中看,你说正常发布的程序怎么可能会有 9 个 Microsoft.VisualStudio.DesignTools 字符串呢?

4. 这个dump是如何生成的

这个问题我相信可能有一些朋友会比较疑惑,其实没什么疑惑的,Visual Studio 有生成Dump的功能,操作步骤为: Debug -> Save Dump As... , 接下来通过一个小例子观察一下。


    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"托管调试器:{Debugger.IsAttached}");
            Console.WriteLine($"非托管调试器:{IsDebuggerPresent()}");

            Console.ReadLine();
        }

        [DllImport("kernel32.dll")]
        static extern bool IsDebuggerPresent();
    }

调试起来后,截图如下:

最后打开生成好的 Dump 文件,使用 kc 观察主线程的输出:


0:000> kc
 # Call Site
00 ntdll!NtWaitForSingleObject
01 KERNELBASE!WaitForSingleObjectEx
02 coreclr!CLREventWaitHelper2
03 coreclr!CLREventWaitHelper
04 coreclr!CLREventBase::WaitEx
05 coreclr!CLREventBase::Wait
06 coreclr!Thread::WaitSuspendEventsHelper
07 coreclr!Thread::WaitSuspendEvents
08 coreclr!Thread::RareEnablePreemptiveGC
09 coreclr!Thread::EnablePreemptiveGC
0a coreclr!Thread::RareDisablePreemptiveGC
0b coreclr!Thread::DisablePreemptiveGC
...
19 ConsoleApp4!ConsoleApp4.Program.Main
...
30 ntdll!RtlUserThreadStart

从卦中看,正是停留在 WaitSuspendEventsHelper 函数里,一摸一样,彻底被360°无死角的证实。。。。

三:总结

这是一次人为的故意恶搞,大概率就是想看下我到底是骡子是马。。。我分析的结果对他和他所在的公司来说没有任何价值,只能白白的浪费我的时间,真是分析多了,什么样的人都能遇到,谨以此文告知后来者,免费但请不要滥用!
图片名称

与记一次 .NET某工控WPF程序被人恶搞的 卡死分析相似的内容:

记一次 .NET某工控WPF程序被人恶搞的 卡死分析

一:背景 1. 讲故事 这一期程序故障除了做原理分析,还顺带吐槽一下,熟悉我的朋友都知道我分析dump是免费的,但免费不代表可以滥用我的宝贵时间,我不知道有些人故意恶搞卡死是想干嘛,不得而知,希望后面类似的事情越来越少吧!废话不多说,我们来看看是如何被恶搞的。 二:WinDbg 分析 1. 程序是如

记一次 .NET某工控视觉自动化系统 卡死分析

一:背景 1. 讲故事 今天分享的dump是训练营里一位学员的,从一个啥也不会到现在分析的有模有样,真的是看他成长起来的,调试技术学会了就是真真实实自己的,话不多说,上windbg说话。 二:WinDbg 分析 1. 为什么会卡死 这位学员是从事工控大类下的视觉自动化,也是目前.NET的主战场,这个

记一次.NET某工控图片上传CPU爆高分析

一:背景 1.讲故事 今天给大家带来一个入门级的 CPU 爆高案例,前段时间有位朋友找到我,说他的程序间歇性的 CPU 爆高,不知道是啥情况,让我帮忙看下,既然找到我,那就用 WinDbg 看一下。 二:WinDbg 分析 1. CPU 真的爆高吗 其实我一直都在强调,要相信数据,口说无凭,一定要亲

记一次 .NET 某工控软件 内存泄露分析

一:背景 1.讲故事 上个月 .NET调试训练营 里的一位老朋友给我发了一个 8G 的dump文件,说他的程序内存泄露了,一时也没找出来是哪里的问题,让我帮忙看下到底是怎么回事,毕竟有了一些调试功底也没分析出来,说明还是有一点复杂的,现实世界中的dump远比课上说的复杂的多。 还是那句话,找我分析是

记一次 .NET 某工控MES程序 崩溃分析

一:背景 1.讲故事 前几天有位朋友找到我,说他的程序出现了偶发性崩溃,已经抓到了dump文件,Windows事件日志显示的崩溃点在 clr.dll 中,让我帮忙看下是怎么回事,那到底怎么回事呢? 上 WinDbg 说话。 二:WinDbg 分析 1. 崩溃点在哪里 如果是托管代码引发的崩溃,在线程

记一次 .NET 某工控视觉系统 卡死分析

## 一:背景 ### 1. 讲故事 前段时间有位朋友找到我,说他们的工业视觉软件僵死了,让我帮忙看下到底是什么情况,哈哈,其实卡死的问题相对好定位,无非就是看主线程栈嘛,然后就是具体问题具体分析,当然难度大小就看运气了。 前几天看一篇文章说现在的 .NET程序员 不需要学习**WinDbg** ,

记一次 .NET某工厂报警监控设置 崩溃分析

一:背景 1. 讲故事 前些天有位朋友在微信上丢了一个崩溃的dump给我,让我帮忙看下为什么出现了崩溃,在 Windows 的事件查看器上显示的是经典的 访问违例 ,即 c0000005 错误码,不管怎么说有dump就可以上windbg开干了。 二:WinDbg 分析 1. 程序为谁崩溃了 在 Wi

记一次 .NET某工业设计软件 崩溃分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他的软件在客户那边不知道什么原因崩掉了,从windows事件日志看崩溃在 clr 里,让我能否帮忙定位下,dump 也抓到了,既然dump有了,接下来就上 windbg 分析吧。 二:WinDbg 分析 1. 为什么崩溃在 clr 一般来说崩溃在clr

记一次 .NET 某餐饮小程序 内存暴涨分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他的程序内存异常高,用 vs诊断工具 加载时间又太久,让我帮忙看一下到底咋回事,截图如下: 确实,如果dump文件超过 10G 之后,市面上那些可视化工具分析起来会让你崩溃的,除了时间久之外这些工具大多也不是用懒加载的方式,比如 dotmemory 会

记一次 .NET某报关系统 非托管泄露分析

## 一:背景 ### 1. 讲故事 前段时间有位朋友找到我,说他的程序内存会出现暴涨,让我看下是怎么事情?而且还告诉我是在 Linux 环境下,说实话在Linux上分析.NET程序难度会很大,难度大的原因在于Linux上的各种开源工具主要是针对 C/C++, 和 .NET 一毛钱关系都没有,说到底