<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Snow</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://snow-blog.pages.dev/</id>
  <link href="https://snow-blog.pages.dev/" rel="alternate"/>
  <link href="https://snow-blog.pages.dev/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Snow</rights>
  <subtitle>Windows Kernel Security · Anti-Rootkit · Reverse Engineering</subtitle>
  <title>Snow</title>
  <updated>2026-11-05T13:18:22.197Z</updated>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="SnowSword Development" scheme="https://snow-blog.pages.dev/categories/SnowSword-Development/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/tags/Windows-Kernel/"/>
    <category term="SnowSword" scheme="https://snow-blog.pages.dev/tags/SnowSword/"/>
    <category term="进程行为检测" scheme="https://snow-blog.pages.dev/tags/%E8%BF%9B%E7%A8%8B%E8%A1%8C%E4%B8%BA%E6%A3%80%E6%B5%8B/"/>
    <category term="安全检测" scheme="https://snow-blog.pages.dev/tags/%E5%AE%89%E5%85%A8%E6%A3%80%E6%B5%8B/"/>
    <category term="SnowWall" scheme="https://snow-blog.pages.dev/tags/SnowWall/"/>
    <content>
      <![CDATA[<h2 id="1-引言"><a href="#1-引言" class="headerlink" title="1. 引言"></a>1. 引言</h2><p>前面的文章主要记录 SnowSword 在进程、驱动和内存视角上的检测实践。这次把测试重点放到 <strong>SnowWall 对恶意行为链的检测</strong>：使用 SnowSword 在授权的隔离实验机中对 Windows 计算器进程执行 DLL 远程注入，然后观察 SnowWall 如何记录、关联并阻断这一串操作。</p><p>本文只讨论防御性检测和实验现象。远程线程注入也可能出现在调试、兼容性工具和安全软件中，因此“命中行为链”是需要进一步复核的高风险信号，而不是脱离上下文的恶意归因。</p><h2 id="2-实验环境与对照条件"><a href="#2-实验环境与对照条件" class="headerlink" title="2. 实验环境与对照条件"></a>2. 实验环境与对照条件</h2><p>实验对象是 Windows 11 中的 <code>CalculatorApp.exe</code>，日志记录的目标 PID 为 <code>4236</code>。发起方是桌面目录下未签名的 <code>SnowSword.exe</code>，PID 为 <code>7108</code>，父进程为 <code>explorer.exe</code>。</p><p>实验采用两组对照：</p><table><thead><tr><th>条件</th><th>观察结果</th></tr></thead><tbody><tr><td>SnowWall 开启</td><td>注入流程出现失败提示，日志记录跨进程内存操作和远程线程创建，并在行为阈值处阻断</td></tr><tr><td>SnowWall 关闭</td><td>注入流程出现成功提示，作为未启用防护时的对照现象</td></tr></tbody></table><p>两组截图的窗口布局基本一致，区别主要在 SnowWall 是否仍处于监控状态以及注入结果提示。这里的“成功&#x2F;失败”只表示本次测试程序报告的结果，最终判断仍以日志和目标进程状态为准。</p><h2 id="3-注入代码的行为拆解"><a href="#3-注入代码的行为拆解" class="headerlink" title="3. 注入代码的行为拆解"></a>3. 注入代码的行为拆解</h2><p><code>RemoteInjectDll</code> 的核心流程可以抽象为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">OpenProcess(PROCESS_ALL_ACCESS)</span><br><span class="line">        ↓</span><br><span class="line">VirtualAllocEx  分配目标进程内存</span><br><span class="line">        ↓</span><br><span class="line">WriteProcessMemory  写入 DLL 路径</span><br><span class="line">        ↓</span><br><span class="line">ZwCreateThreadEx  在目标进程创建线程</span><br><span class="line">                  └─ 线程入口 = LoadLibraryW</span><br></pre></td></tr></table></figure><p>代码首先通过 <code>OpenProcess(PROCESS_ALL_ACCESS, ...)</code> 获取目标进程句柄。随后按 DLL 路径长度调用 <code>VirtualAllocEx</code>，再用 <code>WriteProcessMemory</code> 把路径写入目标地址空间，并通过 <code>ReadProcessMemory</code> 做一次写入验证。</p><p>在 64 位分支中，由于 64 位 Windows 的 <code>kernel32.dll</code> 在系统范围内按相同基址加载，远程线程入口可以直接使用本进程解析到的 <code>LoadLibraryW</code> 地址，参数是目标进程中的路径地址。在 32 位分支中，代码额外获取目标进程中的函数地址，构造一段调用 <code>LoadLibraryW</code> 的 x86 shellcode，分配可执行内存后再通过 <code>ZwCreateThreadEx</code> 执行。两条分支的实现细节不同，但从防御侧看都形成了相同的高风险序列：<strong>跨进程分配、跨进程写入、跨进程线程创建</strong>。</p><p>这也是行为检测比只匹配某个 API 更有价值的地方：单独调用 <code>VirtualAllocEx</code> 可能有正常用途，但它与后续写入和远程线程创建在短时间内连续出现时，语义明显发生变化。</p><h2 id="4-SnowWall-日志中的行为链"><a href="#4-SnowWall-日志中的行为链" class="headerlink" title="4. SnowWall 日志中的行为链"></a>4. SnowWall 日志中的行为链</h2><p>日志时间集中在 <code>06:52:20</code> 至 <code>06:52:21</code>，关键记录如下：</p><h3 id="4-1-第一步：跨进程分配导致污点升级"><a href="#4-1-第一步：跨进程分配导致污点升级" class="headerlink" title="4.1 第一步：跨进程分配导致污点升级"></a>4.1 第一步：跨进程分配导致污点升级</h3><p>首先，<code>NtAllocateVirtualMemory</code> 被记录为规则评估事件，随后 SnowWall 输出 <code>CrossAlloc</code> 告警，原因是“污点等级升级为 1”，可疑度分数为 <code>15</code>，并标记为永久污染。</p><p>这一步并没有立即给出最终阻断结论，但它为后续行为建立了上下文状态。也就是说，SnowWall 并非把每一次系统调用都孤立处理，而是为进程累积行为标签。</p><h3 id="4-2-第二步：向-CalculatorApp-exe-写入内存"><a href="#4-2-第二步：向-CalculatorApp-exe-写入内存" class="headerlink" title="4.2 第二步：向 CalculatorApp.exe 写入内存"></a>4.2 第二步：向 CalculatorApp.exe 写入内存</h3><p>紧接着的 <code>NtWriteVirtualMemory</code> 事件显示：</p><ul><li>发起 PID：<code>7108</code>；</li><li>目标 PID：<code>4236</code>；</li><li>目标映像：<code>CalculatorApp.exe</code>；</li><li><code>CrossProcess=1</code>；</li><li>发起程序未签名，且没有可信祖先进程标记。</li></ul><p>该条记录中的 <code>rule_action=1</code> 和 <code>reason=RULE_ALLOW</code> 表示<strong>这一条规则评估本身没有阻断</strong>，不能理解成整个注入流程已经被 SnowWall 放行。它只是行为链中的中间事件。</p><h3 id="4-3-第三步：远程线程创建触发阈值"><a href="#4-3-第三步：远程线程创建触发阈值" class="headerlink" title="4.3 第三步：远程线程创建触发阈值"></a>4.3 第三步：远程线程创建触发阈值</h3><p>随后到达 <code>NtCreateThreadEx</code>——用户态调用 <code>ZwCreateThreadEx</code> 最终进入的正是这个内核系统服务，因此日志记录的是 <code>NtCreateThreadEx</code>。日志明确标出 <code>CrossThreadCreate</code>，匹配规则“远程线程注入链”，可疑度分数升至 <code>82.91</code>，并出现 <code>behavior_hit=1</code>、<code>reason=BEHAVIOR_THRESHOLD</code>。</p><p>之后 SnowWall 输出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">行为累积判定恶意: PID=7108 LastTag=CrossThreadCreate</span><br><span class="line">event=DECISION ... action=BLOCK reason=BEHAVIOR_THRESHOLD</span><br></pre></td></tr></table></figure><p>因此，本次实验的完整判定是：<strong>前面的单条规则评估允许记录继续进入决策流程；当跨进程分配、写入和远程线程创建形成可疑序列后，行为累计分数达到阈值，SnowWall 最终执行 BLOCK。</strong></p><h2 id="5-截图与日志的相互印证"><a href="#5-截图与日志的相互印证" class="headerlink" title="5. 截图与日志的相互印证"></a>5. 截图与日志的相互印证</h2><h3 id="5-1-SnowWall-开启：注入被阻断"><a href="#5-1-SnowWall-开启：注入被阻断" class="headerlink" title="5.1 SnowWall 开启：注入被阻断"></a>5.1 SnowWall 开启：注入被阻断</h3><p><img src="/images/SnowWall-%E6%A3%80%E6%B5%8B%E5%AE%9E%E6%B5%8B/SnowWall%E5%BC%80%E5%90%AF-%E6%8B%A6%E6%88%AA.png" alt="SnowWall 开启时拦截注入"></p><p>截图中可以看到 SnowSword 的目标进程列表、Windows 计算器窗口和 SnowWall 窗口，同时弹出“注入失败”的提示。它与日志最后的 <code>action=BLOCK</code> 相互印证：测试程序观察到失败，SnowWall 侧则给出了行为阈值阻断记录。</p><h3 id="5-2-SnowWall-关闭：作为对照的成功提示"><a href="#5-2-SnowWall-关闭：作为对照的成功提示" class="headerlink" title="5.2 SnowWall 关闭：作为对照的成功提示"></a>5.2 SnowWall 关闭：作为对照的成功提示</h3><p><img src="/images/SnowWall-%E6%A3%80%E6%B5%8B%E5%AE%9E%E6%B5%8B/SnowWall%E5%85%B3%E9%97%AD-%E6%88%90%E5%8A%9F%E6%B3%A8%E5%85%A5.png" alt="SnowWall 关闭后的成功注入对照"></p><p>关闭 SnowWall 后，界面弹出“注入成功”的提示。</p><h2 id="6-结果分析：为什么是“行为链”而不是单点告警"><a href="#6-结果分析：为什么是“行为链”而不是单点告警" class="headerlink" title="6. 结果分析：为什么是“行为链”而不是单点告警"></a>6. 结果分析：为什么是“行为链”而不是单点告警</h2><p>本次日志可以整理成一条简化链路：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">未签名 SnowSword.exe</span><br><span class="line">        │</span><br><span class="line">        ├─ NtAllocateVirtualMemory ── CrossAlloc，污点等级 1，分数 15</span><br><span class="line">        │</span><br><span class="line">        ├─ NtWriteVirtualMemory ───── CrossProcess=1，写入 PID 4236</span><br><span class="line">        │</span><br><span class="line">        └─ NtCreateThreadEx ───────── CrossThreadCreate，分数 82.91</span><br><span class="line">                                      ↓</span><br><span class="line">                              BEHAVIOR_THRESHOLD</span><br><span class="line">                                      ↓</span><br><span class="line">                                     BLOCK</span><br></pre></td></tr></table></figure><p>其中有三个值得注意的设计点：</p><ol><li><strong>状态累积</strong>：第一次分配操作先建立污点，后续事件可以继承上下文；</li><li><strong>目标关联</strong>：日志不仅记录 API，还记录目标 PID、目标映像和跨进程标志；</li><li><strong>分级决策</strong>：中间事件可以先记录或允许，最终由行为阈值统一决定是否阻断。</li></ol><p>这种策略比“看到 <code>OpenProcess</code> 就直接拦截”更适合降低误报，也比只等待 DLL 加载完成再处理更及时。</p><h2 id="7-工程观察与防御建议"><a href="#7-工程观察与防御建议" class="headerlink" title="7. 工程观察与防御建议"></a>7. 工程观察与防御建议</h2><p>从测试代码和日志可以得到几项工程上的观察：</p><ul><li><code>PROCESS_ALL_ACCESS</code> 权限过宽，防御产品可将句柄权限、调用者身份和目标进程保护级别纳入风险评分；</li><li><code>VirtualAllocEx</code> 与 <code>WriteProcessMemory</code> 的组合比单独的内存分配更值得关注；</li><li><code>NtCreateThreadEx</code> 是行为链中的关键收敛点，但检测不应只依赖函数名，还要核对入口地址、目标进程和前序事件；</li><li>32 位分支需要额外的可执行内存和 shellcode，64 位分支则直接将 <code>LoadLibraryW</code> 作为线程入口，跨架构差异可以作为日志字段而不是简单的放行条件；</li><li>测试程序在清理路径中关闭句柄，但还应确保远程内存和 shellcode 在所有失败分支都得到释放，避免实验代码自身引入泄漏或不稳定因素。</li></ul><p>对防御侧而言，较稳妥的策略是保留原始事件、建立短时间窗口内的行为关联，并把签名、路径、父子进程关系和目标映像作为辅助证据。最终动作可以按风险等级分为记录、告警、挂起或阻断，同时为调试工具和受信任软件保留明确的审计例外。</p><h2 id="8-当前局限与证据边界"><a href="#8-当前局限与证据边界" class="headerlink" title="8. 当前局限与证据边界"></a>8. 当前局限与证据边界</h2><p>本次实验仍有以下限制：</p><ol><li>只有一组目标进程和一次主要日志片段，尚未进行多次重复统计；</li><li>未覆盖签名的合法调试器、不同完整性级别和受保护进程；</li><li>未分别验证 32 位目标进程分支与 64 位目标进程分支的全部路径；</li><li>截图中的“注入成功”来自测试程序提示，尚未用独立工具确认 DLL 模块、入口执行和线程退出状态；</li><li>当前日志没有展示完整规则配置、阈值定义和所有前置事件，因此不能据此推断 SnowWall 在其他版本或其他策略下必然给出相同结果。</li></ol><p>换句话说，日志能够证明本次运行中 SnowWall 观察到了这些 API 事件，并在该策略下输出 <code>BLOCK</code>；截图能够证明两种开关状态下界面现象不同。至于检测覆盖率、误报率和跨版本稳定性，还需要更系统的实验设计。</p><h2 id="9-后续实验计划"><a href="#9-后续实验计划" class="headerlink" title="9. 后续实验计划"></a>9. 后续实验计划</h2><p>后续准备建立一个小型复现实验矩阵，至少保留以下字段：</p><ul><li>SnowWall 版本、规则版本和 Windows 版本；</li><li>发起进程签名、路径、完整性级别与父进程；</li><li>目标进程架构、保护属性和 PID；</li><li>每个 API 事件的时间戳、目标和规则动作；</li><li>最终动作、测试程序返回值和独立验证结果；</li><li>正常调试场景与注入测试场景的重复次数。</li></ul><p>在此基础上，再比较只调用单个 API、缺少前序事件以及完整行为链时的分数变化，才能更准确地评估 SnowWall 的阈值设计和误报边界。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这次实验展示了 SnowWall 如何从 <code>NtAllocateVirtualMemory</code>、<code>NtWriteVirtualMemory</code> 到 <code>NtCreateThreadEx</code> 逐步积累上下文，并将跨进程内存操作与远程线程创建关联为“远程线程注入链”。日志中的 <code>CrossAlloc</code>、<code>CrossThreadCreate</code>、<code>BEHAVIOR_THRESHOLD</code> 和最终 <code>BLOCK</code>，与 SnowWall 开启时的失败提示形成了较完整的证据链；关闭 SnowWall 后的成功提示则提供了直观对照。</p><p>这个结果说明，针对高风险进程行为，按序列关联和分级决策比单点 API 拦截更有解释力。但它仍然是一次受控环境下的功能验证，不能替代多版本、多样本和正常软件基线测试。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>Microsoft Learn：<code>OpenProcess</code>、<code>VirtualAllocEx</code>、<code>WriteProcessMemory</code>、<code>NtCreateThreadEx</code></li><li>Windows Internals, 7th Edition</li><li>本系列前篇：<code>日报_SnowSword隐藏驱动检测实测</code>、<code>SnowSword-隐藏驱动检测原理与工程实现</code></li><li>SnowSword 项目地址：<a href="https://gitee.com/le-ziyi/SnowSword">https://gitee.com/le-ziyi/SnowSword</a></li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/29/%E6%97%A5%E6%8A%A5_SnowWall%E6%A3%80%E6%B5%8B%E5%AE%9E%E6%B5%8B/</id>
    <link href="https://snow-blog.pages.dev/2026/08/29/%E6%97%A5%E6%8A%A5_SnowWall%E6%A3%80%E6%B5%8B%E5%AE%9E%E6%B5%8B/"/>
    <published>2026-08-29T00:00:00.000Z</published>
    <summary>记录 SnowWall 在隔离 Windows 11 实验环境中检测 SnowSword 对 CalculatorApp.exe 的远程注入行为，结合日志和对照截图还原行为链，并讨论证据边界与后续复核方向。</summary>
    <title>SnowWall 检测实测：从实验环境到结果复核</title>
    <updated>2026-11-05T13:18:22.197Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Reverse Engineering" scheme="https://snow-blog.pages.dev/categories/Reverse-Engineering/"/>
    <category term="Windows" scheme="https://snow-blog.pages.dev/tags/Windows/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="逆向分析" scheme="https://snow-blog.pages.dev/tags/%E9%80%86%E5%90%91%E5%88%86%E6%9E%90/"/>
    <category term="CTF Reverse" scheme="https://snow-blog.pages.dev/tags/CTF-Reverse/"/>
    <category term="Shellcode" scheme="https://snow-blog.pages.dev/tags/Shellcode/"/>
    <category term="反射加载" scheme="https://snow-blog.pages.dev/tags/%E5%8F%8D%E5%B0%84%E5%8A%A0%E8%BD%BD/"/>
    <category term="内存取证" scheme="https://snow-blog.pages.dev/tags/%E5%86%85%E5%AD%98%E5%8F%96%E8%AF%81/"/>
    <category term="ChaCha20" scheme="https://snow-blog.pages.dev/tags/ChaCha20/"/>
    <content>
      <![CDATA[<h2 id="1-缘起：为什么“入口”值得单独分析"><a href="#1-缘起：为什么“入口”值得单独分析" class="headerlink" title="1. 缘起：为什么“入口”值得单独分析"></a>1. 缘起：为什么“入口”值得单独分析</h2><p>恶意样本分析和 CTF Reverse 题目里经常遇到一类情况：<code>main</code> 函数看起来很正常，但程序真正的行为根本不在 <code>main</code> 里。攻击者或出题人会把真正要执行的代码藏起来，可能是加壳、可能是进程镂空、也可能是把一段 shellcode 复制到内存后再执行。</p><p>这次分析的 Hideit 是一道 CTF Reverse 题（湖湘杯 2021 Reverse），题目给出的提示只有一句话：<strong>“真正的入口往往不是表面上看到的那样。”</strong> 运行后程序只是打印：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Let&#x27;s start the game!</span><br><span class="line">First secret here:</span><br></pre></td></tr></table></figure><p>表面上看是一个等待输入的小程序，但用 IDA 打开后，<code>main</code> 只有短短几行，真正干活的函数是它调用的另一个函数。顺着这个函数往下看，才发现程序把一整段 shellcode 复制到内存并直接执行——这才是它真正的入口。</p><p>这篇文章不打算只写这道题的求解过程，而是想整理一条<strong>可复用的“伪装入口”逆向分析方法链</strong>：从识别入口伪装，到 dump 内存中的隐藏模块，再到还原隐藏 PE 里的校验逻辑。</p><h2 id="2-先看表面：main-是个幌子"><a href="#2-先看表面：main-是个幌子" class="headerlink" title="2. 先看表面：main 是个幌子"></a>2. 先看表面：main 是个幌子</h2><p><code>main</code> 的反编译非常短：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> __fastcall <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">const</span> <span class="type">char</span> **argv, <span class="type">const</span> <span class="type">char</span> **envp)</span></span><br><span class="line">&#123;</span><br><span class="line">  sub_140001000();</span><br><span class="line">  <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>真正的动作在 <code>sub_140001000</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">puts</span>(<span class="string">&quot;Let&#x27;s start the game!&quot;</span>);</span><br><span class="line">CurrentProcess = GetCurrentProcess();</span><br><span class="line">lpAddress = VirtualAllocEx(CurrentProcess, <span class="number">0</span>, <span class="number">0xBD85</span>, <span class="number">0x3000</span>, <span class="number">4</span>);</span><br><span class="line">WriteProcessMemory(CurrentProcess, lpAddress, &amp;unk_140003040, <span class="number">0xBC85</span>, <span class="number">0</span>);</span><br><span class="line"><span class="keyword">return</span> ((__int64 (*)(<span class="type">void</span>))lpAddress)();</span><br></pre></td></tr></table></figure><p>这段代码做了三件事：</p><ol><li>分配一块可写内存（<code>VirtualAllocEx</code>）；</li><li>把 <code>unk_140003040</code> 处的 0xBC85 字节复制进去（<code>WriteProcessMemory</code>）；</li><li>把执行流直接跳到这块内存（<code>call lpAddress</code>）。</li></ol><p>也就是说，<strong>程序真正的入口是数据段里的一段 shellcode</strong>，而不是编译出来的 <code>main</code>。这种手法在真实恶意样本和 CTF Reverse 题目里都很常见：静态分析只看到“分配内存 + 复制数据”，真正的行为要等运行时才出现。</p><blockquote><p>方法要点：遇到 <code>VirtualAlloc(Ex) + WriteProcessMemory/Memcpy + 调用指针</code> 的组合，基本可以断定存在“内存中执行”的隐藏代码。不要停在 <code>main</code>，要跟着这块内存走。</p></blockquote><h2 id="3-从静态文件到运行时内存：dump-隐藏-PE"><a href="#3-从静态文件到运行时内存：dump-隐藏-PE" class="headerlink" title="3. 从静态文件到运行时内存：dump 隐藏 PE"></a>3. 从静态文件到运行时内存：dump 隐藏 PE</h2><p>shellcode 本身是位置无关代码，直接线性反汇编会在 <code>call</code> 之后迷失——<code>call</code> 后面的字节并不是顺序执行的代码，而是加密&#x2F;压缩的数据，线性反汇编会把它们当作指令继续解析，产生垃圾代码。Hideit 的 blob 开头是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">e8 80 49 00 00    ; call $+0x4985</span><br></pre></td></tr></table></figure><p><code>call</code> 跳到 blob 内部的解包&#x2F;加载代码，后面跟的才是被保护的数据。顺着跳转目标分析，会发现这段 shellcode 是一个<strong>反射式 loader</strong>：它解析并映射一个内嵌的 PE，然后跳到 PE 的入口点继续执行。</p><p>到这里，静态分析已经比较吃力了。更直接的办法是<strong>动态 dump</strong>：让程序跑起来，停在输入提示处，然后扫描进程内存，把映射出来的隐藏 PE 完整读出来。</p><p>具体做法：</p><ol><li>用 <code>CreateProcess</code> 启动样本，保持 stdin 不关闭，让它停在 <code>First secret here:</code>；</li><li>用 <code>VirtualQueryEx</code> 遍历进程内存区域；</li><li>在每个可读区域里搜索 <code>MZ</code> + 合法 <code>PE\0\0</code> 头；</li><li>命中后把整个内存镜像写到文件。</li></ol><p>实践中会扫到很多系统 DLL，需要过滤：隐藏 PE 通常出现在低地址的私有内存区域，且<strong>镜像基址处直接就是 MZ</strong>（而不是在区域偏移处）。把 dump 下来的镜像交给 PE 解析工具，就能得到隐藏 PE 的节区、入口点和字符串。</p><blockquote><p>方法要点：静态分析遇到“自解密&#x2F;反射加载”时，动态内存 dump 往往比继续逆 loader 更快。扫描内存时优先找“MZ 位于区域起始处”的镜像，那通常是真正的 PE 基址。</p></blockquote><h2 id="4-隐藏-PE-里的两段校验"><a href="#4-隐藏-PE-里的两段校验" class="headerlink" title="4. 隐藏 PE 里的两段校验"></a>4. 隐藏 PE 里的两段校验</h2><p>Dump 出来的隐藏 PE 很小，静态 PE 工具看不到有效导入：导入项由外层反射式 loader 在运行时解析并填充，静态分析只能看到空&#x2F;无效的导入表结构，看不到实际 API 绑定。程序逻辑是一个“找秘密”的小程序：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">First secret here:  &lt;-- 读取输入</span><br></pre></td></tr></table></figure><p>隐藏 PE 对输入做了两段校验。</p><h3 id="4-1-第一段：魔改-TEA"><a href="#4-1-第一段：魔改-TEA" class="headerlink" title="4.1 第一段：魔改 TEA"></a>4.1 第一段：魔改 TEA</h3><p>第一段校验是一个自定义的 TEA 变体：</p><ul><li>密钥：<code>{ 0x72, 0x202, 0x13, 0x13 }</code>（4 个 32 位字，<code>0x202</code> 是字值而非单字节）</li><li>轮数：32 轮</li><li>常数：<code>0x9E3779B9</code>（TEA 的 delta）</li></ul><p>但它不是标准 TEA，混合函数被改过。从汇编里还原出来的轮函数大致是：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">mix</span>(<span class="params">x, s, k</span>):</span><br><span class="line">    <span class="keyword">return</span> ((((x &lt;&lt; <span class="number">2</span>) ^ (x &gt;&gt; <span class="number">5</span>)) + ((x &gt;&gt; <span class="number">3</span>) ^ (x &lt;&lt; <span class="number">4</span>)))</span><br><span class="line">            ^ ((k ^ x) + (x ^ s))) &amp; <span class="number">0xFFFFFFFF</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">encrypt</span>(<span class="params">v0, v1, key</span>):</span><br><span class="line">    s = <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">32</span>):</span><br><span class="line">        s = (s + DELTA) &amp; <span class="number">0xFFFFFFFF</span></span><br><span class="line">        v0 = (v0 + mix(v1, s, key[(s &gt;&gt; <span class="number">2</span>) &amp; <span class="number">3</span>])) &amp; <span class="number">0xFFFFFFFF</span></span><br><span class="line">        v1 = (v1 + mix(v0, s, key[((s &gt;&gt; <span class="number">2</span>) &amp; <span class="number">3</span>) ^ <span class="number">1</span>])) &amp; <span class="number">0xFFFFFFFF</span></span><br><span class="line">    <span class="keyword">return</span> v0, v1</span><br></pre></td></tr></table></figure><p>程序把输入的前 8 字节当作两个 dword，加密后与固定密文比较。逆推得到的 8 字节明文是 <code>dotitsit</code>——它并不是最终 flag，而是第二段校验要用到的 nonce。</p><blockquote><p>方法要点：看到 <code>0x9E3779B9</code> 不要直接套标准 TEA 解密，先确认轮函数是否被魔改。用“自洽加解密”验证：先按汇编实现加密，再用逆轮解密，最后用“加密(解密(cipher)) &#x3D;&#x3D; cipher”做自检。</p></blockquote><h3 id="4-2-第二段：ChaCha20"><a href="#4-2-第二段：ChaCha20" class="headerlink" title="4.2 第二段：ChaCha20"></a>4.2 第二段：ChaCha20</h3><p>第二段校验是 ChaCha20。隐藏 PE 在 <code>.rdata</code> 里存了 32 字节密文，同时运行时通过常量相减还原出 32 字节 key：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">key   = 0N3@aYI_M3l0dy_KurOm1_W_Suk1dqy0</span><br><span class="line">nonce = dotitsit</span><br></pre></td></tr></table></figure><p>用标准 ChaCha20 对密文解密，得到的就是最终秘密。这里有个很值得记录的坑：<strong>程序里存在一段被手工改写的 ChaCha20 轮函数，和标准 ChaCha20 并不完全一致</strong>，如果直接拿这段魔改函数当“解密 oracle”会对不上。但实际校验&#x2F;密文生成等价于标准 ChaCha20，因此从程序里提取 key、nonce、密文后，用标准库反而能直接解出——魔改实现更像是干扰项或未参与该校验路径。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> Crypto.Cipher <span class="keyword">import</span> ChaCha20</span><br><span class="line"></span><br><span class="line">key   = <span class="string">b&quot;0N3@aYI_M3l0dy_KurOm1_W_Suk1dqy0&quot;</span></span><br><span class="line">nonce = <span class="string">b&quot;dotitsit&quot;</span></span><br><span class="line">enc   = <span class="built_in">bytes</span>.fromhex(<span class="string">&quot;eb8e5ca562b41c845c59fc0d433cab20d8933313a19e39007614b504589d06b8&quot;</span>)</span><br><span class="line"></span><br><span class="line">flag = ChaCha20.new(key=key, nonce=nonce).decrypt(enc)</span><br></pre></td></tr></table></figure><blockquote><p>注意：<code>PyCryptodome</code> 的 <code>ChaCha20</code> 默认支持 8 字节 nonce（原始 ChaCha20，64 位 nonce + 64 位计数器；IETF 变体为 12 字节 nonce）。这里 nonce 是 8 字节，不要误改成 12 字节。</p></blockquote><blockquote><p>方法要点：不要假设程序里的密码学实现一定和标准库一致。提取出 key&#x2F;nonce&#x2F;密文后，用标准算法独立验证；如果对不上，再回去检查程序是否改了轮函数、字节序或状态布局。</p></blockquote><h2 id="5-可复用的“伪装入口”分析流水线"><a href="#5-可复用的“伪装入口”分析流水线" class="headerlink" title="5. 可复用的“伪装入口”分析流水线"></a>5. 可复用的“伪装入口”分析流水线</h2><p>这次分析可以总结成一条通用流水线：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">识别伪装入口</span><br><span class="line">  → VirtualAlloc/WriteProcessMemory/调用指针</span><br><span class="line">  → 提取并分析 shellcode</span><br><span class="line">  → 识别反射式 loader</span><br><span class="line">  → 动态 dump 内存中的隐藏 PE</span><br><span class="line">  → 解析隐藏 PE（节区/字符串/导入项）</span><br><span class="line">  → 还原自定义校验算法</span><br><span class="line">  → 用标准算法独立验证</span><br></pre></td></tr></table></figure><p>每一步都有对应的“为什么”：</p><ul><li><strong>识别伪装入口</strong>是为了不被 <code>main</code> 带偏；</li><li><strong>动态 dump</strong>是为了绕过自解密&#x2F;反射加载；</li><li><strong>隐藏 PE 分析</strong>是为了拿到真正的逻辑；</li><li><strong>独立实现算法</strong>是为了避免被程序自身的 bug 或魔改实现误导。</li></ul><p>工具在这里只是加速器，真正兜底的是对 Windows 内存执行、PE 结构、反射加载和常见密码学算法的理解。</p><h2 id="6-与真实恶意样本分析-ARK-检测的关联"><a href="#6-与真实恶意样本分析-ARK-检测的关联" class="headerlink" title="6. 与真实恶意样本分析 &#x2F; ARK 检测的关联"></a>6. 与真实恶意样本分析 &#x2F; ARK 检测的关联</h2><h3 id="6-1-对真实恶意样本分析的意义"><a href="#6-1-对真实恶意样本分析的意义" class="headerlink" title="6.1 对真实恶意样本分析的意义"></a>6.1 对真实恶意样本分析的意义</h3><p>Hideit 演示的“隐藏入口”本质上就是真实恶意软件常用的<strong>内存执行</strong>手法：</p><ul><li>把 shellcode 或完整 PE 放在数据段&#x2F;资源里；</li><li>运行时分配 RWX 内存并复制过去；</li><li>通过反射式 loader 映射执行；</li><li>进程内存里出现一个文件系统里不存在的 PE 镜像。</li></ul><p>如果只分析磁盘文件，很容易漏掉这类内存执行手法；必须结合<strong>内存视角</strong>才能看到真正执行的代码。</p><h3 id="6-2-对-SnowSword-ARK-检测的启发"><a href="#6-2-对-SnowSword-ARK-检测的启发" class="headerlink" title="6.2 对 SnowSword &#x2F; ARK 检测的启发"></a>6.2 对 SnowSword &#x2F; ARK 检测的启发</h3><p>这和 SnowSword 做隐藏驱动&#x2F;进程检测的思路是相通的：</p><ul><li>攻击者隐藏的是“文件里看不到，但内存里真实存在”的对象；</li><li>检测方不能只依赖单一数据源（模块列表、对象目录、磁盘文件）；</li><li>内存扫描（找 MZ&#x2F;PE 头、扫描可执行内存）是发现隐藏模块的重要补充。</li></ul><p>Hideit 的隐藏 PE 在内存里有完整的 MZ&#x2F;PE 头，如果检测工具具备“扫描内存中的 PE 镜像”能力，就能在运行时发现它。反过来，这也说明攻击者只要抹掉 PE 头或把内存改成非可执行属性，就能进一步提高检测门槛——检测与对抗始终是动态博弈。</p><h2 id="7-遇到的问题与局限"><a href="#7-遇到的问题与局限" class="headerlink" title="7. 遇到的问题与局限"></a>7. 遇到的问题与局限</h2><ol><li><strong><code>call</code> 跳板后面的数据不能线性反汇编</strong>：blob 开头是 <code>call $+0x4985</code>，目标之后的字节不是顺序代码，直接线性反汇编会把数据解析成垃圾指令；需要先识别跳板，再跟着控制流走。</li><li><strong>内存里出现多个 MZ</strong>：进程里有大量系统 DLL，扫描时会命中很多假阳性。需要根据“镜像基址是否就是 MZ”“是否位于私有内存区域”等条件过滤。</li><li><strong>静态看不到隐藏 PE 的有效导入</strong>：隐藏 PE 的导入项由反射式 loader 在运行时填充，静态 PE 工具只能看到空&#x2F;无效的导入表结构，需要结合动态 dump 和代码分析。</li><li><strong>程序内存在魔改 ChaCha20 干扰项</strong>：直接拿魔改函数当解密 oracle 会对不上；实际校验等价于标准 ChaCha20，提取 key&#x2F;nonce&#x2F;密文后用标准实现即可验证。</li><li><strong>依赖运行时行为</strong>：如果样本检测到调试器&#x2F;沙箱就不执行隐藏代码，动态 dump 可能失败；需要配合反反调试或直接静态还原 loader。</li></ol><h2 id="8-总结与展望"><a href="#8-总结与展望" class="headerlink" title="8. 总结与展望"></a>8. 总结与展望</h2><p>Hideit 这道题的价值不在于“解出最终字符串”，而在于完整演示了 CTF Reverse 与真实恶意样本中常见的入口伪装手法：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">main → VirtualAlloc → WriteProcessMemory → shellcode → 反射加载隐藏 PE</span><br></pre></td></tr></table></figure><p>分析这类程序时，最重要的习惯是<strong>不要信任表面入口</strong>。看到内存分配和指针调用，就要意识到真正的代码可能在内存里。把“静态识别 + 动态 dump + 隐藏 PE 还原 + 算法独立验证”这条流水线跑通，比记住单个样本的答案更有价值。</p><p>后续可以继续完善的方向：</p><ul><li>把内存 dump 流程脚本化：自动过滤系统模块、自动定位隐藏 PE、自动修复内存镜像；</li><li>增加对更多反射加载&#x2F;进程镂空变体的识别（如清除 PE 头、手动映射、无 MZ 内存）；</li><li>把“内存 PE 扫描”作为 SnowSword 的一个检测视角，和模块列表、对象目录交叉验证；</li><li>对自定义密码学实现做自动化比对：识别 TEA&#x2F;ChaCha20 变体、提取轮函数参数。</li></ul><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>ChaCha20 标准文档（RFC 8439）</li><li>TEA &#x2F; XTEA 算法参考实现</li><li>Microsoft Learn：<code>VirtualAllocEx</code>、<code>WriteProcessMemory</code>、PE 格式</li><li>湖湘杯 2021 Reverse 相关 writeup（用于交叉验证 key&#x2F;nonce 提取结果）</li><li>本系列前篇：<code>第十二篇日报_PyInstaller样本逆向学习记录</code>、<code>日报_PEPEPE_已知明文攻击</code></li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/28/%E7%AC%AC%E5%8D%81%E4%B8%89%E7%AF%87%E6%97%A5%E6%8A%A5_%E4%BC%AA%E8%A3%85%E5%85%A5%E5%8F%A3%E4%B8%8E%E9%9A%90%E8%97%8FPE%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</id>
    <link href="https://snow-blog.pages.dev/2026/08/28/%E7%AC%AC%E5%8D%81%E4%B8%89%E7%AF%87%E6%97%A5%E6%8A%A5_%E4%BC%AA%E8%A3%85%E5%85%A5%E5%8F%A3%E4%B8%8E%E9%9A%90%E8%97%8FPE%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/"/>
    <published>2026-08-28T14:00:00.000Z</published>
    <summary>以 CTF Reverse 题目 Hideit 为例，记录“真正入口不在 main”的逆向分析方法：识别 VirtualAlloc/WriteProcessMemory 伪装入口、从运行进程内存中 dump 隐藏 PE、分析自定义 TEA 与 ChaCha20 校验，并总结可复用的“伪装入口”逆向分析流水线（同样适用于真实恶意样本分析）。</summary>
    <title>伪装入口与内存中的隐藏 PE：Hideit 样本逆向分析记录</title>
    <updated>2026-09-21T04:20:05.619Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Malware Analysis" scheme="https://snow-blog.pages.dev/categories/Malware-Analysis/"/>
    <category term="Windows" scheme="https://snow-blog.pages.dev/tags/Windows/"/>
    <category term="恶意样本" scheme="https://snow-blog.pages.dev/tags/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="SnowSword" scheme="https://snow-blog.pages.dev/tags/SnowSword/"/>
    <category term="虚拟机检测" scheme="https://snow-blog.pages.dev/tags/%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%A3%80%E6%B5%8B/"/>
    <category term="代码完整性" scheme="https://snow-blog.pages.dev/tags/%E4%BB%A3%E7%A0%81%E5%AE%8C%E6%95%B4%E6%80%A7/"/>
    <category term="SiPolicy" scheme="https://snow-blog.pages.dev/tags/SiPolicy/"/>
    <content>
      <![CDATA[<h2 id="1-缘起：从-Edge-下载的“deepseek-电脑版”"><a href="#1-缘起：从-Edge-下载的“deepseek-电脑版”" class="headerlink" title="1. 缘起：从 Edge 下载的“deepseek 电脑版”"></a>1. 缘起：从 Edge 下载的“deepseek 电脑版”</h2><p>这次分析的样本来自 Edge 搜索下载的所谓“deepseek 电脑版”。把它放进隔离虚拟机后，表面上几乎没有任何动作：没有明显窗口、没有稳定的网络行为、连预期中的安装流程也没有出现。</p><p>起初怀疑是样本损坏。后来结合静态分析的入口结构和系统重启后出现的异常，才怀疑它存在虚拟机环境检测，并进一步发现它释放了驱动级组件、修改了系统代码完整性策略。整个过程并不顺利，但留下的证据链比较完整，值得整理成一篇日报。</p><h2 id="2-启动路径：一段可疑的运行时初始化"><a href="#2-启动路径：一段可疑的运行时初始化" class="headerlink" title="2. 启动路径：一段可疑的运行时初始化"></a>2. 启动路径：一段可疑的运行时初始化</h2><p>样本的入口 <code>start</code> 非常短，只做了三件事：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">start</span><br><span class="line">  ├─ sub rsp, 28h</span><br><span class="line">  ├─ call sub_14001CE6C   ; 初始化运行时值</span><br><span class="line">  ├─ add rsp, 28h</span><br><span class="line">  └─ jmp sub_140001106   ; 进入真正的启动逻辑</span><br></pre></td></tr></table></figure><p><code>sub_14001CE6C</code> 依次调用 <code>GetSystemTimeAsFileTime</code>、<code>GetCurrentProcessId</code>、<code>GetCurrentThreadId</code>、<code>GetTickCount</code> 和 <code>QueryPerformanceCounter</code>，把这些结果异或组合成一个运行时值，保存在 <code>qword_14003D0A8</code>，同时在 <code>qword_14003D0B0</code> 保存它的按位取反结果。</p><p>单看这段代码，它更像安全 cookie &#x2F; 运行时值初始化，并不能单独证明存在虚拟机检测。真正引起怀疑的是后面的启动路径：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sub_140001106</span><br><span class="line">  └─ call sub_14000113C</span><br><span class="line">      ├─ rax != 0 → 继续进入后续逻辑</span><br><span class="line">      └─ rax == 0 → jz short loc_14000112F，走退出路径</span><br></pre></td></tr></table></figure><p>在虚拟机中运行原始文件时，样本没有继续表现出明显行为。为了确认这个条件分支的含义，我在隔离环境中做了一次最小 patch 实验。</p><h2 id="3-验证分支含义：一次最小-patch-实验"><a href="#3-验证分支含义：一次最小-patch-实验" class="headerlink" title="3. 验证分支含义：一次最小 patch 实验"></a>3. 验证分支含义：一次最小 patch 实验</h2><p>方案 A 很直接：把 <code>test rax, rax</code> 后面的</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jz short loc_14000112F</span><br></pre></td></tr></table></figure><p>改成两个字节的 <code>NOP NOP</code>（<code>90 90</code>），让“检测失败”也不再跳向退出分支：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">call sub_14000113C</span><br><span class="line">test rax, rax</span><br><span class="line">nop</span><br><span class="line">nop                     ; jz 被抹掉，不再跳转退出分支</span><br><span class="line">jmp loc_1400014E0       ; 无条件进入真实逻辑</span><br></pre></td></tr></table></figure><p>最初的担心是样本存在内存完整性校验：进程启动后会校验 <code>.text</code> 段内存是否与磁盘原始内容一致，一旦发现被 patch 就判定篡改并退出。实际测试发现该样本没有 CRC 校验，修改后成功在虚拟机中继续运行。</p><p>调试器中的观察也印证了这一点：在 <code>install_q0_3_02+0x166a8</code> 下断，单步进入 <code>sub_14001CE6C</code> 后 <code>rax=00002b992ddfa233</code>，<code>qword_14003D0A8</code> 与取反值均已初始化；随后跳入 <code>sub_140001106</code>，当 <code>sub_14000113C</code> 返回 <code>rax=2</code> 时，<code>test rax, rax</code> 后不会走退出分支。</p><p>这个实验支持“启动条件判断 &#x2F; 环境检测”的假设，但必须保持克制：它只能证明该分支控制样本是否继续执行，不能单独断言样本只检测虚拟机。条件判断可能混合了调试器、系统版本、权限、网络或其他运行环境特征。</p><h2 id="4-一次代价很高的失误"><a href="#4-一次代价很高的失误" class="headerlink" title="4. 一次代价很高的失误"></a>4. 一次代价很高的失误</h2><p>确认启动分支后，我继续运行样本，却没有提前在关键位置设置足够的断点和行为监控。结果样本跑飞，感染了虚拟机并自动重启。</p><p>这次失误的教训很直接：<strong>“能跑起来”并不等于“值得继续跑”</strong>。对驱动级样本，运行前必须准备好快照、行为监控和关键点断点，否则一次误操作就会污染整个实验环境，让后续分析从“分析样本”变成“处理事故”。</p><h2 id="5-重启后的异常：驱动加载被代码完整性策略拦截"><a href="#5-重启后的异常：驱动加载被代码完整性策略拦截" class="headerlink" title="5. 重启后的异常：驱动加载被代码完整性策略拦截"></a>5. 重启后的异常：驱动加载被代码完整性策略拦截</h2><p>重启之后，虚拟机中的 SnowSword 驱动加载出现异常，用户态调用 <code>ZwLoadDriver</code> 返回 <code>0xC0E90002</code>，但用户态功能尚可使用。同时，干掉了银狐驱动 <code>ranchserv.jpg</code> 的服务后，它还会被自动拉起。</p><p>奇怪的是，挂上 WinDbg 后，同一个驱动又表现为可以加载。这个差异不能简单归结为“驱动文件时好时坏”。沿着内核加载路径下断点，经过 <code>NtLoadDriver → IopLoadDriverImage → IopLoadDriver → MmLoadSystemImage</code> 后，WinDbg 直接给出了关键提示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">violated SI policy, will fail to load for scenario 0 !</span><br><span class="line">It is now allowed to load because BD is attached.</span><br></pre></td></tr></table></figure><p>这说明加载失败是由 SI（代码完整性）策略拦截导致的，而不是文件损坏或注册表缺失；<code>BD</code> 可以理解为内核调试器附加状态。调试器附加后允许加载，恰好解释了“普通启动失败、挂 WinDbg 成功”的表象，但它不能证明驱动签名有效，也不能证明系统策略已经恢复。</p><h2 id="6-CodeIntegrity-日志与-SiPolicy-p7b-时间戳"><a href="#6-CodeIntegrity-日志与-SiPolicy-p7b-时间戳" class="headerlink" title="6. CodeIntegrity 日志与 SiPolicy.p7b 时间戳"></a>6. CodeIntegrity 日志与 SiPolicy.p7b 时间戳</h2><p>在 <code>Microsoft-Windows-CodeIntegrity/Operational</code> 日志中，<code>SnowSword.sys</code> 对应了两类关键事件：</p><ul><li><strong>事件 3077</strong>：代码完整性判断该驱动不满足 Authenticode 签名级别要求，或违反了当前代码完整性策略（Policy ID：<code>{a244370e-44c9-4c06-b551-f6016e563076}</code>）；</li><li><strong>事件 3004</strong>：系统无法验证映像完整性，提示找不到文件哈希，可能是签名错误、文件损坏或未知来源软件。</li></ul><p>日志时间覆盖 2026&#x2F;8&#x2F;27 16:29、17:23 和 2026&#x2F;8&#x2F;28 7:27、7:30，被加载对象均为 <code>C:\Users\21607\Desktop\SnowSword.sys</code>。</p><p>查看 <code>System32\CodeIntegrity</code> 目录时，<code>SiPolicy.p7b</code> 的大小为 24,972 字节，<code>LastWriteTime</code> 为 <code>2026/8/27 17:54:50</code>，明显晚于同目录下其他代码完整性策略文件。结合样本已经具备修改文件、注册表和内核状态的高风险能力，可以提出一个较强的分析判断：<strong>系统代码完整性策略文件被样本篡改，或者至少被置于与干净系统明显不同的状态</strong>。不过，在没有同版本干净系统基线、文件哈希和签名链之前，仍应把它表述为“基于现有证据的高度疑似篡改”，而不是绝对结论。</p><p>如果需要恢复，正确做法是从干净的同版本 Windows 10 机器导出出厂默认策略，替换虚拟机中被篡改的 <code>SiPolicy.p7b</code>。同时要注意 <strong>EFI 分区中仍保留 <code>SiPolicy.p7b</code> 的副本，需要一并修改</strong>，否则启动链可能继续使用被篡改的策略。</p><h2 id="7-伪装驱动-ranchserv-jpg-的接口"><a href="#7-伪装驱动-ranchserv-jpg-的接口" class="headerlink" title="7. 伪装驱动 ranchserv.jpg 的接口"></a>7. 伪装驱动 ranchserv.jpg 的接口</h2><p>样本释放出的一个关键文件名是 <code>ranchserv.jpg</code>。从扩展名看它像图片，但实际被当作驱动文件加载。对这个驱动的派遣函数反编译后，可以看到它根据 <code>IRP_MJ_DEVICE_CONTROL</code> 的控制码分派不同功能，其中三个接口尤其值得关注：</p><table><thead><tr><th>控制码</th><th>行为</th><th>潜在风险</th></tr></thead><tbody><tr><td><code>0x22E04C</code></td><td>接收文件路径，打开文件后执行未公开操作</td><td>可篡改文件内容、清零 PE 头或签名，导致目标驱动被内核判定为无效镜像</td></tr><tr><td><code>0x22E048</code></td><td>打开并操作注册表键</td><td>可删除或修改安全工具的服务项，或修改驱动加载相关注册表配置</td></tr><tr><td><code>0x22E014</code>（材料标注）</td><td>向某个内核结构中的函数指针数组写入任意值</td><td>若该结构是文件系统过滤回调、映像加载通知或对象管理器回调，可挂钩或破坏驱动加载路径，使安全工具驱动加载被拦截或返回错误</td></tr></tbody></table><p>此外，<code>0x22E044</code> 附近还有版本查询分支，会向调用方返回 <code>2.0.2</code> 之类的字符串；其余分支则处理数据复制、结构初始化等操作。</p><p>这个接口集合解释了后续现象：如果攻击者通过用户态客户端向该驱动传入 ARK&#x2F;HIPS 驱动文件路径，它可以清零 PE 头或篡改签名；如果传入注册表路径，它可以破坏安全工具的服务项；而内核结构写入能力则可以直接干预驱动加载路径，使安全工具在加载阶段就被拦截。</p><h2 id="8-隐藏与对抗：安全模式下仍然看不见"><a href="#8-隐藏与对抗：安全模式下仍然看不见" class="headerlink" title="8. 隐藏与对抗：安全模式下仍然看不见"></a>8. 隐藏与对抗：安全模式下仍然看不见</h2><p>分析过程中还发现了一个隐蔽手法：在安全模式下，即使勾选“显示隐藏文件”，<code>C:\ProgramData</code> 的普通 <code>dir</code> 也看不到银狐的可执行文件，只能看到 <code>Dbg</code>、<code>Huorong</code>、<code>Microsoft</code> 等常规目录。</p><p>使用 <code>dir /a</code> 后，才暴露出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">C:\ProgramData&gt;dir /a</span><br><span class="line">2026/08/27  15:02    &lt;DIR&gt;          ePq5kdxo</span><br><span class="line">2026/08/27  15:02             2,949,632 Oaxs10zK.exe</span><br><span class="line">2026/08/27  15:02    &lt;DIR&gt;          VMHKT1Zo</span><br></pre></td></tr></table></figure><p>原因是被设置了“系统”+“隐藏”双重属性：只勾选“显示隐藏文件”不足以看到它们，必须同时勾选“显示系统文件”才会显形。</p><p>处置时，删除文件后不能直接留空，否则服务或残留进程可能会把它重新创建。更稳妥的做法是：删除后用同名空目录占位，取消 SYSTEM、Everyone 和当前账户对该空目录的全部访问权限，并把目录所有者设为 SYSTEM，让恶意组件既无法重建文件，也无法通过目录权限写入新副本。</p><h2 id="9-处置思路与证据保全"><a href="#9-处置思路与证据保全" class="headerlink" title="9. 处置思路与证据保全"></a>9. 处置思路与证据保全</h2><p>样本已经执行过驱动级动作并触发自动重启后，继续在原虚拟机中“修修补补”并不是优先选择。更稳妥的顺序是：</p><ol><li>立即断开网络，保存虚拟磁盘、内存和事件日志副本；</li><li>记录服务项、驱动文件路径、文件哈希、创建时间和签名验证结果；</li><li>对 <code>CodeIntegrity/Operational</code>、System、Service Control Manager 和启动日志做时间线整理；</li><li>使用可信来源重新建立同版本系统快照，再对比 <code>System32\CodeIntegrity</code> 及 EFI 分区中的策略文件；</li><li>只有在证据已经保全后，才在副本上进行策略恢复和驱动加载验证。</li></ol><p>恢复策略时应使用与目标系统版本和补丁级别匹配的可信基线，并先确认恢复操作不会破坏取证证据。EFI 分区中的副本也需要纳入检查范围；只替换系统目录中的单个文件，不能自动证明启动链已经恢复。恢复后应重新启动并复核代码完整性事件、策略状态和驱动签名，而不是只看一次 <code>ZwLoadDriver</code> 返回值。</p><h2 id="10-IOC-与复核清单"><a href="#10-IOC-与复核清单" class="headerlink" title="10. IOC 与复核清单"></a>10. IOC 与复核清单</h2><p>目前能确认或需要重点复核的线索包括：</p><table><thead><tr><th>类型</th><th>线索</th></tr></thead><tbody><tr><td>伪装驱动</td><td><code>ranchserv.jpg</code>，实际按驱动用途加载；对应服务被删除后会被自动拉起</td></tr><tr><td>可疑样本</td><td>从 Edge 搜索下载的所谓“deepseek 电脑版”安装包（调试符号名 <code>install_q0_3_02</code>）及其释放文件</td></tr><tr><td>落地文件</td><td><code>C:\ProgramData\Oaxs10zK.exe</code>（2,949,632 字节）</td></tr><tr><td>随机目录</td><td><code>C:\ProgramData\ePq5kdxo</code>、<code>C:\ProgramData\VMHKT1Zo</code></td></tr><tr><td>系统影响</td><td>自动重启、<code>ZwLoadDriver</code> 返回 <code>0xC0E90002</code>、SnowSword 驱动加载异常、<code>ranchserv.jpg</code> 服务自动拉起</td></tr><tr><td>完整性日志</td><td>CodeIntegrity 事件 3004、3077（Policy ID：<code>{a244370e-44c9-4c06-b551-f6016e563076}</code>）</td></tr><tr><td>关键策略文件</td><td><code>System32\CodeIntegrity\SiPolicy.p7b</code>（24,972 字节，2026&#x2F;8&#x2F;27 17:54:50）及 EFI 分区对应副本</td></tr></tbody></table><p>上表中的路径和文件名来自本次环境记录，不能直接当作通用 IOC。实际处置时还应补充 SHA-256、签名证书链、服务注册表路径、父子进程关系和网络连接记录，并在多个时间点重复采集，避免把残留文件误当成当前活动组件。</p><h2 id="11-复盘：这次不会忘记的几件事"><a href="#11-复盘：这次不会忘记的几件事" class="headerlink" title="11. 复盘：这次不会忘记的几件事"></a>11. 复盘：这次不会忘记的几件事</h2><ul><li>在运行样本前先做快照，并准备第二份只读分析副本；</li><li>先建立基线，再运行样本，至少记录驱动、服务、代码完整性策略和启动项；</li><li>关键分支、服务创建、驱动加载和重启触发点必须提前布置观测手段；</li><li>不把反编译器命名当成事实，所有高风险结论都要结合交叉引用、调用栈和系统日志；</li><li>“挂调试器后可以加载”只说明调试上下文改变了系统行为，不表示样本或驱动可信；</li><li>对驱动级样本，恢复快照通常比在已感染系统中继续操作更可靠；</li><li>排查隐藏文件时，要同时勾选“显示系统文件”，不要只依赖“显示隐藏文件”。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>从目前证据看，这个银狐样本至少表现出三条值得警惕的线索：使用图片扩展名伪装驱动、通过高权限接口修改文件&#x2F;注册表&#x2F;内核状态，以及根据运行环境决定是否继续执行。随后出现的自动重启和 SnowSword 驱动加载失败，则把样本行为与系统代码完整性策略异常联系到了同一条证据链上：<code>SiPolicy.p7b</code> 的时间戳晚于其他策略文件，CodeIntegrity 日志持续报告 3077&#x2F;3004，WinDbg 明确提示 SI policy 拦截，而 <code>ranchserv.jpg</code> 恰好具备能够造成这些影响的文件、注册表和内核写入能力。</p><p>安全分析仍然需要克制。一次虚拟机实验可以提出假设，却不能替代完整取证；一次日志告警可以指出策略冲突，却不能单凭时间戳证明所有文件都由同一个样本修改。只有把静态分析、运行时观测、代码完整性日志、可信基线和快照前后差异放在同一条证据链上，才能更接近可靠结论。</p><p>下一步会在全新快照中补齐文件哈希、服务时间线和策略文件签名信息，并继续完善 SnowSword 对异常驱动和代码完整性事件的关联展示。</p>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/28/yinhu-vm-analysis-diary/</id>
    <link href="https://snow-blog.pages.dev/2026/08/28/yinhu-vm-analysis-diary/"/>
    <published>2026-08-28T12:00:00.000Z</published>
    <summary>记录银狐木马样本在隔离虚拟机中的分析过程：从启动分支 patch 验证虚拟机检测假设，到定位 ranchserv.jpg 伪装驱动的文件/注册表/内核接口，再结合 CodeIntegrity 日志与 SiPolicy.p7b 时间戳还原代码完整性策略被篡改的证据链，并总结处置与证据保全方法。</summary>
    <title>银狐木马虚拟机分析日报</title>
    <updated>2026-09-21T04:20:05.612Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Malware Analysis" scheme="https://snow-blog.pages.dev/categories/Malware-Analysis/"/>
    <category term="Windows" scheme="https://snow-blog.pages.dev/tags/Windows/"/>
    <category term="恶意样本" scheme="https://snow-blog.pages.dev/tags/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="逆向分析" scheme="https://snow-blog.pages.dev/tags/%E9%80%86%E5%90%91%E5%88%86%E6%9E%90/"/>
    <category term="PyInstaller" scheme="https://snow-blog.pages.dev/tags/PyInstaller/"/>
    <category term="反混淆" scheme="https://snow-blog.pages.dev/tags/%E5%8F%8D%E6%B7%B7%E6%B7%86/"/>
    <category term="Python" scheme="https://snow-blog.pages.dev/tags/Python/"/>
    <content>
      <![CDATA[<h2 id="1-缘起：为什么值得单独写一篇-PyInstaller-样本分析"><a href="#1-缘起：为什么值得单独写一篇-PyInstaller-样本分析" class="headerlink" title="1. 缘起：为什么值得单独写一篇 PyInstaller 样本分析"></a>1. 缘起：为什么值得单独写一篇 PyInstaller 样本分析</h2><p>在 Windows 恶意样本里，Python 打包程序已经不算少见。攻击者把 Python 写的 loader、窃密器、键盘记录器用 PyInstaller 打成 exe，既能跨机器运行，又能把真正的逻辑藏进字节码里。对恶意样本分析师来说，<strong>把 PyInstaller 里的 Python 字节码完整提取出来</strong>往往是分析的第一步。</p><p>这次遇到的样本表面上是一个普通 Windows 程序：运行后提示 <code>Enter input:</code>，输入错误就输出 <code>Wrong input</code>。但用 IDA 打开后，入口逻辑并不是常见的 C&#x2F;C++ 业务代码，而是 PyInstaller bootloader 的特征——它真正的逻辑在附加数据段里。更值得注意的是，这个样本对通用提取工具做了两处明显的对抗：</p><ol><li>PyInstaller 的 cookie 魔数被改写，导致 <code>pyinstxtractor</code> 直接报 “Missing cookie”；</li><li>CArchive 里的 Python 模块数据被自定义加密，即使绕过 cookie 检查，解压也会失败。</li></ol><p>这两处对抗单独看都不难，但组合起来正好可以整理出一条可复用的 <strong>PyInstaller 恶意样本静态提取方法链</strong>。</p><h2 id="2-先建立整体认知：PyInstaller-程序长什么样"><a href="#2-先建立整体认知：PyInstaller-程序长什么样" class="headerlink" title="2. 先建立整体认知：PyInstaller 程序长什么样"></a>2. 先建立整体认知：PyInstaller 程序长什么样</h2><p>PyInstaller 打包出来的 Windows 程序可以粗略分成两层：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">┌─────────────────────────────────────────────┐</span><br><span class="line">│ PE 启动器（bootloader）                      │</span><br><span class="line">│   原生 C/C++ 代码，负责解包、加载 Python     │</span><br><span class="line">├─────────────────────────────────────────────┤</span><br><span class="line">│ overlay：追加在 PE 文件尾部的 CArchive       │</span><br><span class="line">│   ├── TOC：条目名称 / 偏移 / 大小 / 类型     │</span><br><span class="line">│   ├── 条目类型：                            │</span><br><span class="line">│   │   s/m  → Python 字节码（模块/入口脚本）  │</span><br><span class="line">│   │   z    → PYZ.pyz（压缩的 Python 模块库） │</span><br><span class="line">│   │   b    → DLL / PYD / 数据文件            │</span><br><span class="line">│   └── cookie：位于文件尾部，用于定位 CArchive│</span><br><span class="line">└─────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p>关键点有两个：</p><ul><li><strong>cookie</strong>：一小段固定魔数，通常长这样：<code>MEI\x0c\x0b\x0a\x0b\x0e</code>。bootloader 从文件尾部往回搜索这段魔数，找到后解析后面的 CArchive 偏移和长度。</li><li><strong>CArchive 条目</strong>：<code>s</code> 类型是入口脚本（如 <code>main.py</code>），<code>m</code> 类型是被 import 的模块，<code>z</code> 类型是 PYZ。它们大多数情况下是 zlib 压缩后的 Python marshal 数据。</li></ul><p>通用提取器（如 <code>pyinstxtractor</code>）就是按这个结构工作的：先找 cookie，再读 TOC，再逐个解压条目。</p><h2 id="3-对抗点一：cookie-魔数被改写"><a href="#3-对抗点一：cookie-魔数被改写" class="headerlink" title="3. 对抗点一：cookie 魔数被改写"></a>3. 对抗点一：cookie 魔数被改写</h2><p>用 <code>pyinstxtractor</code> 处理样本时，最先遇到的就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[!] Error : Missing cookie, unsupported pyinstaller version or not a pyinstaller archive</span><br></pre></td></tr></table></figure><p>直接在文件里搜标准魔数 <code>MEI\x0c\x0b\x0a\x0b\x0e</code>，确实搜不到。但文件里存在大量 PyInstaller 特征字符串（<code>pyi-python-flag</code>、<code>PyRun_SimpleStringFlags</code> 等），说明它仍然是 PyInstaller 程序，只是 cookie 被改过。</p><p>继续搜索后发现，文件尾部 cookie 位置的实际字节是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">73 77 49 0c 0b 0a 0b 0e</span><br><span class="line">= swI\x0c\x0b\x0a\x0b\x0e</span><br></pre></td></tr></table></figure><p>也就是说，作者把标准魔数里的 <code>MEI</code> 改成了 <code>swI</code>。</p><p>如果只看工具报错，很容易卡在“工具不支持”上。正确的做法是回到 bootloader 本身：<strong>程序能运行，说明它运行时一定能找到某个魔数，去启动器里看它到底在找什么。</strong></p><p>在 IDA 里定位到 bootloader 的归档加载函数后，能看到它并不是直接比较一段明文魔数，而是通过常量加载、移位、加法等操作在运行时构造搜索模式。反汇编里类似这样的片段：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mov rcx, cs:qword_140030E40</span><br><span class="line">mov [rsp+Buffer], rcx</span><br><span class="line">mov r8d, 8                ; 搜索长度 8 字节</span><br><span class="line">shr rcx, 18h</span><br><span class="line">add cl, 0Dh               ; 修正第 4 个字节</span><br><span class="line">mov [rsp+Buffer+3], cl</span><br><span class="line">call sub_1400084F0        ; 从文件尾部反向搜索魔数</span><br></pre></td></tr></table></figure><p>顺着这个逻辑确认，bootloader 实际接受的 cookie 就是 <code>swI\x0c\x0b\x0a\x0b\x0e</code>。启动器里甚至还保留了另一个变体（<code>swI\x0d...</code>），用于外部 <code>.pkg</code> 归档的加载路径。</p><p><strong>修复方法</strong>：把文件尾部 cookie 的 <code>swI</code> 改回 <code>MEI</code>，字节数不变，不会影响任何偏移。修改后 <code>pyinstxtractor</code> 就能正常识别：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">data = <span class="built_in">bytearray</span>(<span class="built_in">open</span>(<span class="string">&#x27;sample.exe&#x27;</span>, <span class="string">&#x27;rb&#x27;</span>).read())</span><br><span class="line">idx = data.rfind(<span class="string">b&#x27;swI\x0c\x0b\x0a\x0b\x0e&#x27;</span>)</span><br><span class="line">data[idx:idx+<span class="number">3</span>] = <span class="string">b&#x27;MEI&#x27;</span></span><br><span class="line"><span class="built_in">open</span>(<span class="string">&#x27;sample_patched.exe&#x27;</span>, <span class="string">&#x27;wb&#x27;</span>).write(data)</span><br></pre></td></tr></table></figure><blockquote><p>方法要点：通用提取器是基于“标准结构”写的，遇到魔数被改时，不要急着放弃或猜，先去 bootloader 里确认它运行时使用的真实魔数。</p></blockquote><h2 id="4-对抗点二：Python-模块数据被自定义加密"><a href="#4-对抗点二：Python-模块数据被自定义加密" class="headerlink" title="4. 对抗点二：Python 模块数据被自定义加密"></a>4. 对抗点二：Python 模块数据被自定义加密</h2><p>cookie 修好后再提取，<code>pyinstxtractor</code> 能解析出 58 个 CArchive 条目，但关键模块全部解压失败：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[!] Error : Failed to decompress pyimod01_archive</span><br><span class="line">[!] Error : Failed to decompress pyimod02_importers</span><br><span class="line">[!] Error : Failed to decompress CreackMe&amp;&amp;FxxMe</span><br></pre></td></tr></table></figure><p>把 <code>pyimod01_archive</code> 的原始数据前几个字节打出来看：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">73 77 64 64 d2 70 f7 25 ...</span><br><span class="line">= swdd....（不是 zlib 头）</span><br></pre></td></tr></table></figure><p>正常的 zlib 流应该以 <code>78 9c</code> 或 <code>78 da</code> 开头，而这里以 <code>swdd</code> 开头。很明显，作者在压缩前对数据做了处理，并且给每个加密条目都加了 <code>swdd</code> 前缀。</p><p>解密逻辑仍然藏在 bootloader 里。定位到负责解压条目的函数 <code>sub_1400011F0</code>，反编译后能看到一段非常直白的处理：</p><ol><li>如果读到的数据以 <code>swdd</code> 开头，先去掉前 4 字节；</li><li>对剩余数据逐字节 XOR <code>0xAA</code>；</li><li>把处理后的数据交给 inflate 解压。</li></ol><p>对应的 Python 验证脚本：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> zlib</span><br><span class="line"></span><br><span class="line">raw = <span class="built_in">open</span>(<span class="string">&#x27;pyimod01_archive.bin&#x27;</span>, <span class="string">&#x27;rb&#x27;</span>).read()</span><br><span class="line"><span class="keyword">assert</span> raw[:<span class="number">4</span>] == <span class="string">b&#x27;swdd&#x27;</span></span><br><span class="line"></span><br><span class="line">data = <span class="built_in">bytes</span>(b ^ <span class="number">0xAA</span> <span class="keyword">for</span> b <span class="keyword">in</span> raw[<span class="number">4</span>:])   <span class="comment"># 去掉 swdd 前缀后整体 XOR 0xAA</span></span><br><span class="line">plain = zlib.decompress(data)             <span class="comment"># 得到 marshal 代码对象</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">len</span>(plain), plain[:<span class="number">16</span>].<span class="built_in">hex</span>())</span><br></pre></td></tr></table></figure><p>第一次验证就成功解出 2999 字节，说明加密算法就是“前缀 + 固定单字节 XOR”，没有额外密钥或复杂状态。对于恶意样本分析来说这是好消息：<strong>固定 XOR 的混淆只能挡住不会看数据的工具，挡不住愿意往下跟一层的人。</strong></p><h2 id="5-字节码恢复与反编译"><a href="#5-字节码恢复与反编译" class="headerlink" title="5. 字节码恢复与反编译"></a>5. 字节码恢复与反编译</h2><p>解压出来的数据是 Python 3.8 的 marshal 代码对象（以 <code>e3 00 00 00</code> 开头），不是完整的 <code>.pyc</code> 文件。要反编译，需要补上 Python 3.8 的 pyc 头：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">55 0d 0d 0a 00 00 00 00 00 00 00 00 00 00 00 00</span><br></pre></td></tr></table></figure><p>补完头之后，用 <code>uncompyle6</code> 就能直接恢复主脚本源码。恢复出来的核心逻辑是一个自定义实现的 RC4 类加解密函数：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">arc_cipher</span>(<span class="params">data: <span class="built_in">bytes</span>, key: <span class="built_in">bytes</span></span>) -&gt; <span class="built_in">bytes</span>:</span><br><span class="line">    <span class="comment"># 自实现 RC4 加密/解密</span></span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">bytes</span>(out)</span><br><span class="line"></span><br><span class="line">key = <span class="string">b&#x27;NSSCTF&#x27;</span></span><br><span class="line">user_input = <span class="built_in">input</span>(<span class="string">&quot;Enter input: &quot;</span>).encode()</span><br><span class="line">encrypted = arc_cipher(user_input, key)</span><br><span class="line"><span class="keyword">if</span> encrypted == target_bytes:</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;Your are right, flag is NSSCTF&#123;&#123;<span class="subst">&#123;user_input.decode()&#125;</span>&#125;&#125;&quot;</span>)</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;Wrong input&quot;</span>)</span><br></pre></td></tr></table></figure><p>到这里，样本的完整行为已经清楚了：它读入一段输入，用固定 key 做自实现 RC4，和内置密文比较。由于 RC4 是对称算法，用同一函数处理密文就能还原出正确输入。</p><p>验证时只需要跑一遍本地还原逻辑，确认“恢复出的输入能通过程序校验”即可。本文不展开具体输入值，因为重点不是这道题本身，而是这条提取链。</p><h2 id="6-对恶意样本分析的启发"><a href="#6-对恶意样本分析的启发" class="headerlink" title="6. 对恶意样本分析的启发"></a>6. 对恶意样本分析的启发</h2><h3 id="6-1-通用提取器失效时，方法链比工具重要"><a href="#6-1-通用提取器失效时，方法链比工具重要" class="headerlink" title="6.1 通用提取器失效时，方法链比工具重要"></a>6.1 通用提取器失效时，方法链比工具重要</h3><p>这次分析真正有价值的地方，不是“把 <code>swI</code> 改成 <code>MEI</code>”，而是面对失效时的思考顺序：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">识别打包器</span><br><span class="line">  → 理解标准结构（cookie / CArchive / PYZ / pyc）</span><br><span class="line">  → 工具报错时，回到 bootloader 确认真实约定</span><br><span class="line">  → 定位自定义处理逻辑（魔数构造、解密、解压）</span><br><span class="line">  → 手工恢复数据</span><br><span class="line">  → 补结构头，交给反编译器</span><br></pre></td></tr></table></figure><p>工具负责加速，但<strong>结构知识负责兜底</strong>。PyInstaller 的 cookie、TOC、条目类型、pyc 头这些都是公开且固定的，只要 bootloader 还在按这套结构运行，就能手工还原。</p><h3 id="6-2-静态分析的边界"><a href="#6-2-静态分析的边界" class="headerlink" title="6.2 静态分析的边界"></a>6.2 静态分析的边界</h3><p>这个样本展示了静态分析的一个典型困境：字符串、魔数、数据头都可以被改写，甚至模块数据可以被整体加密。如果只依赖“搜字符串”和“跑通用提取器”，很容易得出错误结论。</p><p>但静态分析仍然有价值：bootloader 是原生代码，它必须实现解密逻辑，才能让 Python 运行时加载模块。只要找到 bootloader 里 inflate 之前的处理代码，就能还原整个数据变换。<strong>混淆可以藏数据，但藏不住处理数据的代码。</strong></p><h3 id="6-3-与-ARK-SnowSword-检测思维的关联"><a href="#6-3-与-ARK-SnowSword-检测思维的关联" class="headerlink" title="6.3 与 ARK &#x2F; SnowSword 检测思维的关联"></a>6.3 与 ARK &#x2F; SnowSword 检测思维的关联</h3><p>这个样本的对抗方式，和 Rootkit 隐藏自己的思路很像：</p><ul><li>恶意样本改掉 PyInstaller cookie 魔数 → 通用提取器失效；</li><li>Rootkit 摘掉 <code>ActiveProcessLinks</code> &#x2F; 模块链表 → 通用枚举工具失效。</li></ul><p>两者的共同点是：<strong>攻击者只需要让“单一数据源&#x2F;单一工具”失效，就能显著提高分析门槛。</strong> 这也解释了为什么 SnowSword 做检测时要坚持多视角交叉验证——模块列表、对象目录、内存特征、运行时行为互相印证，而不是把某一个列表当成绝对事实。逆向分析同理：不能因为一个提取器报错就判定“不是 PyInstaller”，也不能因为一个字符串匹配就认定样本行为。</p><h2 id="7-遇到的问题与局限"><a href="#7-遇到的问题与局限" class="headerlink" title="7. 遇到的问题与局限"></a>7. 遇到的问题与局限</h2><ol><li><strong>Python 版本不匹配</strong>：当前环境是 Python 3.11，样本是 Python 3.8，<code>pyinstxtractor</code> 对 PYZ 的自动提取会因 marshal 版本不匹配而跳过。主脚本在 CArchive 里不受影响，但 PYZ 内模块需要额外处理。</li><li><strong>方法依赖具体 bootloader 修改</strong>：本次的 <code>swdd + XOR 0xAA</code> 是针对这个样本的定制逻辑；换一个样本，前缀、密钥、算法位置都可能变化，不能直接套脚本。</li><li><strong>手工解析容易踩偏移坑</strong>：CArchive 的 TOC 条目是定长头 + 变长名字，解析时如果字段偏移算错，后面的条目会全部错位。写脚本时要先用已知 DLL 条目（能正常 zlib 解压）做自检。</li><li><strong>反编译质量依赖工具</strong>：<code>uncompyle6</code> 对 Python 3.8 支持尚可，但遇到复杂控制流仍可能失败；必要时需要结合字节码 <code>dis</code> 手工分析。</li></ol><h2 id="8-总结与展望"><a href="#8-总结与展望" class="headerlink" title="8. 总结与展望"></a>8. 总结与展望</h2><p>这次分析形成了一条可以复用的 PyInstaller 恶意样本静态提取流水线：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">PE 启动器识别</span><br><span class="line">  → cookie 修复</span><br><span class="line">  → CArchive 手工解析</span><br><span class="line">  → 定位 bootloader 自定义解密</span><br><span class="line">  → 恢复 Python 字节码</span><br><span class="line">  → 补 pyc 头 + 反编译</span><br></pre></td></tr></table></figure><p>后续可以把这套流程脚本化、半自动化：自动扫描 cookie 魔数变体、自动识别 <code>swdd</code> 这类前缀、自动尝试常见 XOR 密钥，并把 PYZ 内模块的提取也补全。再往后，还可以把同样的思路扩展到 PyArmor、Nuitka 等其它 Python 打包&#x2F;加固方案，并尝试与动态行为分析结合，验证静态还原出的逻辑和实际运行行为是否一致。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>PyInstaller 官方文档：<a href="https://pyinstaller.org/">https://pyinstaller.org/</a></li><li>pyinstxtractor：<a href="https://github.com/extremecoders-re/pyinstxtractor">https://github.com/extremecoders-re/pyinstxtractor</a></li><li>uncompyle6：<a href="https://github.com/rocky/python-uncompyle6">https://github.com/rocky/python-uncompyle6</a></li><li>Microsoft PE Format：<a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format">https://learn.microsoft.com/en-us/windows/win32/debug/pe-format</a></li><li>本系列前篇：<code>恶意样本逆向分析学习记录</code>、<code>日报_PEPEPE_已知明文攻击</code></li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/23/%E7%AC%AC%E5%8D%81%E4%BA%8C%E7%AF%87%E6%97%A5%E6%8A%A5_PyInstaller%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</id>
    <link href="https://snow-blog.pages.dev/2026/08/23/%E7%AC%AC%E5%8D%81%E4%BA%8C%E7%AF%87%E6%97%A5%E6%8A%A5_PyInstaller%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/"/>
    <published>2026-08-23T12:00:00.000Z</published>
    <summary>以一次 PyInstaller 加固样本分析为例，记录从 PE 启动器识别、CArchive 魔数修复、自定义 XOR 解密、pyc 恢复与反编译的完整方法链，并总结 PyInstaller 恶意样本分析中的可复用经验。</summary>
    <title>PyInstaller 加固样本逆向：从 CArchive 修复到 pyc 反编译</title>
    <updated>2026-09-21T04:20:05.620Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="SnowSword Development" scheme="https://snow-blog.pages.dev/categories/SnowSword-Development/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="Anti-Rootkit" scheme="https://snow-blog.pages.dev/tags/Anti-Rootkit/"/>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/tags/Windows-Kernel/"/>
    <category term="SnowSword" scheme="https://snow-blog.pages.dev/tags/SnowSword/"/>
    <category term="驱动检测" scheme="https://snow-blog.pages.dev/tags/%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B/"/>
    <category term="对象管理器" scheme="https://snow-blog.pages.dev/tags/%E5%AF%B9%E8%B1%A1%E7%AE%A1%E7%90%86%E5%99%A8/"/>
    <content>
      <![CDATA[<h2 id="1-为什么需要多视角检测"><a href="#1-为什么需要多视角检测" class="headerlink" title="1. 为什么需要多视角检测"></a>1. 为什么需要多视角检测</h2><p>前面的文章分别讨论过模块列表、对象目录和内存特征扫描。把这些内容放在一起后，可以得到一个更清晰的结论：<strong>驱动检测的难点不是读取某一个列表，而是判断多个独立视角是否能够相互解释。</strong></p><p>正常加载的驱动通常同时具有映像、驱动对象和运行时行为等信息。攻击者如果只修改其中一处登记信息，其他视角仍可能留下痕迹。因此，SnowSword 的设计目标不是寻找一个“绝对可信”的 API，而是把多个来源关联起来，降低单一数据源被篡改带来的影响。</p><p>这里的“隐藏驱动”是检测语境中的结果标签，不等于已经完成恶意归因。一个条目被标记为异常后，还需要结合系统版本、加载方式、对象关系和调试器结果进行人工复核。</p><h2 id="2-威胁模型：驱动可能藏在哪里"><a href="#2-威胁模型：驱动可能藏在哪里" class="headerlink" title="2. 威胁模型：驱动可能藏在哪里"></a>2. 威胁模型：驱动可能藏在哪里</h2><p>本文只讨论防御性检测和实验验证，不实现或复现隐藏驱动代码。为了理解检测边界，可以把攻击者可能修改的登记位置抽象成下面几类：</p><table><thead><tr><th>视角</th><th>可能的异常现象</th><th>检测盲区</th></tr></thead><tbody><tr><td>模块加载器</td><td>映像不再出现在 <code>PsLoadedModuleList</code> 对应的模块结果中</td><td>只查询 <code>SystemModuleInformation</code> 时无法发现摘链对象</td></tr><tr><td>对象管理器</td><td>驱动对象未出现在预期目录，或对象与映像信息无法关联</td><td>只枚举 <code>\\Driver</code> 时无法覆盖不注册对象的场景</td></tr><tr><td>内存映像</td><td>内核可执行区域存在 PE 特征，但找不到已知模块归属</td><td>PE 头被清除、扫描范围不完整时可能漏检</td></tr><tr><td>运行时行为</td><td>存在可疑回调、设备通信或异常内存操作</td><td>单凭行为难以确定对应的驱动映像</td></tr></tbody></table><p>这几类现象并不互相等价。例如，模块列表中没有某个基址，可能是摘链，也可能是结构读取失败或版本适配问题。检测程序应该输出证据和置信等级，而不是直接输出“恶意”。</p><h2 id="3-SnowSword-的总体架构"><a href="#3-SnowSword-的总体架构" class="headerlink" title="3. SnowSword 的总体架构"></a>3. SnowSword 的总体架构</h2><p>SnowSword 的检测流程可以拆成三个层次：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">用户态界面与调度</span><br><span class="line">        │</span><br><span class="line">        │ IOCTL</span><br><span class="line">        ▼</span><br><span class="line">内核态采集与安全读取</span><br><span class="line">        │</span><br><span class="line">        ├─ 模块列表集合</span><br><span class="line">        ├─ DRIVER_OBJECT 集合</span><br><span class="line">        └─ 内存 PE 候选集合</span><br><span class="line">                │</span><br><span class="line">                ▼</span><br><span class="line">        用户态按基址关联、分级展示和人工复核</span><br></pre></td></tr></table></figure><p>内核态适合访问对象管理器和内核地址空间，但需要严格处理 IRQL、对象引用和异常读取；用户态适合做集合运算、路径格式化、结果排序和界面展示。把这两类职责分开，既能减少驱动中的复杂逻辑，也方便后续替换某一种采集方式。</p><p>一个统一的检测结果至少应包含以下字段：</p><ul><li>映像基址和大小；</li><li>驱动名称或路径（如果能够获得）；</li><li>来源视角：模块列表、对象目录或内存扫描；</li><li>是否能与其他视角按基址关联；</li><li>当前状态：已知模块、对象关联、游离 PE 或需要人工复核；</li><li>目标系统版本和扫描时间。</li></ul><h2 id="4-第一视角：枚举内核模块列表"><a href="#4-第一视角：枚举内核模块列表" class="headerlink" title="4. 第一视角：枚举内核模块列表"></a>4. 第一视角：枚举内核模块列表</h2><p>SnowSword 已有的模块枚举使用 <code>SystemModuleInformation</code>。这类信息通常来自内核维护的已加载模块链表，可以得到驱动路径、映像基址和映像大小。</p><p>由于返回缓冲区大小不固定，常见实现是两阶段查询：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 伪代码：只展示查询流程，不代表完整的错误处理</span></span><br><span class="line">status = ZwQuerySystemInformation(</span><br><span class="line">    SystemModuleInformation,</span><br><span class="line">    <span class="literal">NULL</span>,</span><br><span class="line">    <span class="number">0</span>,</span><br><span class="line">    &amp;requiredSize</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (status != STATUS_INFO_LENGTH_MISMATCH || requiredSize == <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> status;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">buffer = AllocateNonPagedPool(requiredSize);</span><br><span class="line"><span class="keyword">if</span> (buffer == <span class="literal">NULL</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> STATUS_INSUFFICIENT_RESOURCES;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">status = ZwQuerySystemInformation(</span><br><span class="line">    SystemModuleInformation,</span><br><span class="line">    buffer,</span><br><span class="line">    requiredSize,</span><br><span class="line">    &amp;requiredSize</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>实际代码还需要处理大小变化、内存释放、返回结构版本和路径编码。用户态拿到结果后，可以将 <code>ImageName</code> 的最后一段作为显示名，但不能只依赖名称进行关联，因为名称可能被截断、重复或格式化不同。</p><h3 id="4-1-这一视角的优点和局限"><a href="#4-1-这一视角的优点和局限" class="headerlink" title="4.1 这一视角的优点和局限"></a>4.1 这一视角的优点和局限</h3><p>优点是接口相对稳定，能够快速得到已加载映像的基址和大小，适合建立系统基线。局限也很明显：如果模块链表中的记录被摘除，查询结果就不会包含它；因此，模块列表更适合当作一个参考集合，而不是唯一事实来源。</p><h2 id="5-第二视角：对象管理器中的驱动对象"><a href="#5-第二视角：对象管理器中的驱动对象" class="headerlink" title="5. 第二视角：对象管理器中的驱动对象"></a>5. 第二视角：对象管理器中的驱动对象</h2><p>驱动加载后，通常会在对象管理器命名空间中留下 <code>DRIVER_OBJECT</code>。普通驱动常见于 <code>\\Driver</code>，文件系统相关驱动还需要关注 <code>\\FileSystem</code> 及其可能的子目录。漏掉子目录会使检测视野不完整。</p><h3 id="5-1-枚举流程"><a href="#5-1-枚举流程" class="headerlink" title="5.1 枚举流程"></a>5.1 枚举流程</h3><p>工程上可以按以下步骤组织对象枚举：</p><ol><li>打开目标目录对象；</li><li>将句柄转换为内核对象引用；</li><li>遍历目录项并取得对象指针；</li><li>用 <code>ObGetObjectType</code> 判断对象是否为驱动对象；</li><li>在确认对象有效后读取 <code>DriverSection</code> 和 <code>DllBase</code>；</li><li>将基址、名称和对象来源写入结果集合；</li><li>严格配对所有对象引用。</li></ol><p>某些实现会直接遍历 <code>OBJECT_DIRECTORY</code> 的哈希桶，以取得对象指针。需要强调的是，<code>OBJECT_DIRECTORY</code>、目录项布局和哈希桶数量都不是适合无条件硬编码的公开稳定接口。已有研究中常见的 37 个桶不能直接视为所有 Windows 版本的通用结论；目标系统必须通过符号、调试器或版本适配逻辑确认布局。</p><h3 id="5-2-基址关联"><a href="#5-2-基址关联" class="headerlink" title="5.2 基址关联"></a>5.2 基址关联</h3><p><code>DRIVER_OBJECT</code> 的 <code>DriverSection</code> 通常可以关联到加载器数据表项，再从 <code>DllBase</code> 得到映像基址。这个基址是连接两个视角的关键字段：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">对象目录中的 DRIVER_OBJECT</span><br><span class="line">              │ DriverSection-&gt;DllBase</span><br><span class="line">              ▼</span><br><span class="line">模块列表中的 ImageBase</span><br></pre></td></tr></table></figure><h3 id="5-3-引用计数纪律"><a href="#5-3-引用计数纪律" class="headerlink" title="5.3 引用计数纪律"></a>5.3 引用计数纪律</h3><p>遍历对象时，引用生命周期比查找逻辑更容易出错：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">ObReferenceObject</span><br><span class="line">        │</span><br><span class="line">        ├─ 非目标对象：ObDereferenceObject</span><br><span class="line">        └─ 目标对象：把引用明确转交给调用者</span><br><span class="line">                         │</span><br><span class="line">                         └─ 使用结束后 ObDereferenceObject</span><br></pre></td></tr></table></figure><p>漏释放会造成对象引用泄漏，多释放或在引用转交前释放则可能导致后续访问失效。驱动端应该为“本次遍历持有的引用”和“返回给上层的引用”建立清晰的所有权约定，并在错误路径中逐一回收。</p><h2 id="6-第三视角：内存中的-PE-特征"><a href="#6-第三视角：内存中的-PE-特征" class="headerlink" title="6. 第三视角：内存中的 PE 特征"></a>6. 第三视角：内存中的 PE 特征</h2><p>如果一个可执行映像不再出现在模块列表，且没有可枚举的驱动对象，前两种视角可能同时失效。作为补充，SnowSword 的设计还考虑在受控的内核内存范围内寻找 PE 镜像特征。</p><h3 id="6-1-两级特征校验"><a href="#6-1-两级特征校验" class="headerlink" title="6.1 两级特征校验"></a>6.1 两级特征校验</h3><p>驱动映像通常具有 DOS 头的 <code>MZ</code> 签名，并能通过 <code>e_lfanew</code> 定位到 <code>PE\\0\\0</code> 签名。扫描时应该先进行轻量检查，再读取后续头部：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 伪代码：候选识别，不等于恶意判定</span></span><br><span class="line"><span class="keyword">if</span> (ReadSafely(address, &amp;dosHeader, <span class="keyword">sizeof</span>(dosHeader)) != STATUS_SUCCESS) &#123;</span><br><span class="line">    <span class="keyword">return</span> CandidateReadFailed;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) &#123;</span><br><span class="line">    <span class="keyword">return</span> NotPeImage;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!IsReasonableOffset(dosHeader.e_lfanew)) &#123;</span><br><span class="line">    <span class="keyword">return</span> InvalidPeOffset;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (ReadSafely(address + dosHeader.e_lfanew,</span><br><span class="line">               &amp;signature, <span class="keyword">sizeof</span>(signature)) != STATUS_SUCCESS) &#123;</span><br><span class="line">    <span class="keyword">return</span> CandidateReadFailed;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> signature == IMAGE_NT_SIGNATURE ? PeCandidate : NotPeImage;</span><br></pre></td></tr></table></figure><p>这里的 <code>ReadSafely</code> 代表安全的内核拷贝。</p><h3 id="6-2-扫描范围不是“全内存”"><a href="#6-2-扫描范围不是“全内存”" class="headerlink" title="6.2 扫描范围不是“全内存”"></a>6.2 扫描范围不是“全内存”</h3><p>全地址空间逐字节扫描既不现实，也会带来严重的性能和稳定性问题。当前设计优先考虑内核模块附近的受控范围，并把非分页池、高地址内存和已清除 PE 头列为盲区。扫描范围必须页对齐，地址和结果字段必须使用足够宽的 64 位类型。</p><p>早期实现可以由用户态按页发起 IOCTL，但这会产生大量上下文切换。正式版本应考虑批量传递地址区间，让驱动一次处理一批页，并只返回候选结果。无论采用哪种方式，都不能把“命中 PE 头”直接等同于隐藏驱动：正常模块、残留映像或数据误匹配都可能产生误报。</p><h2 id="7-结果关联与分级判定"><a href="#7-结果关联与分级判定" class="headerlink" title="7. 结果关联与分级判定"></a>7. 结果关联与分级判定</h2><p>三个集合可以抽象为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">A = 模块列表中的基址集合</span><br><span class="line">B = 对象目录关联到的 DRIVER_OBJECT 基址集合</span><br><span class="line">C = 内存扫描得到的 PE 候选基址集合</span><br></pre></td></tr></table></figure><p>按基址关联后，可以采用更保守的状态分类：</p><table><thead><tr><th>状态</th><th>典型条件</th><th>建议处理</th></tr></thead><tbody><tr><td>已知模块</td><td><code>A</code> 中存在，且能与 <code>B</code> 关联</td><td>作为正常基线记录</td></tr><tr><td>对象可见但模块缺失</td><td><code>B</code> 中存在而 <code>A</code> 中不存在</td><td>标记为疑似摘链，人工复核</td></tr><tr><td>游离 PE 候选</td><td><code>C</code> 中存在，但无法归属于 <code>A</code></td><td>标记为疑似游离映像，不直接判恶意</td></tr><tr><td>模块存在但对象未关联</td><td><code>A</code> 中存在而 <code>B</code> 中不存在</td><td>结合启动阶段、驱动类型和系统基线判断</td></tr><tr><td>读取或版本不确定</td><td>结构读取失败、布局未确认或信息不完整</td><td>标记为无法判断，不强行归类</td></tr></tbody></table><p><code>A</code> 有而 <code>B</code> 无<code>并不天然表示隐藏。早期启动驱动、没有典型设备对象的驱动以及版本差异，都可能导致这种结果。相反，</code>B<code>有而</code>A&#96; 无也只能说明两个视角不一致，最终结论还需要 WinDbg、文件签名、加载日志和运行时行为等独立证据。</p><h2 id="8-工程实现中的几个关键问题"><a href="#8-工程实现中的几个关键问题" class="headerlink" title="8. 工程实现中的几个关键问题"></a>8. 工程实现中的几个关键问题</h2><h3 id="8-1-IOCTL-边界"><a href="#8-1-IOCTL-边界" class="headerlink" title="8.1 IOCTL 边界"></a>8.1 IOCTL 边界</h3><p>用户态传入的地址、长度、线程数量和输出缓冲区都必须做范围检查。不能信任用户态传来的指针，也不能让单次请求覆盖过大的扫描区间。对于输出结果，要明确结构版本、元素数量和总字节数，避免整数溢出和越界写入。</p><h3 id="8-2-安全读取与执行级别"><a href="#8-2-安全读取与执行级别" class="headerlink" title="8.2 安全读取与执行级别"></a>8.2 安全读取与执行级别</h3><p>访问内核地址前，需要确认当前代码运行在允许的 IRQL，并使用适合当前环境的安全拷贝方式。<code>MmCopyMemory</code>、异常保护和地址有效性检查各自解决的问题不同，不能把某一个检查当成“绝对安全”的保证。读取私有结构时还要考虑对象并发销毁和字段被修改的情况。</p><h3 id="8-3-线程与资源管理"><a href="#8-3-线程与资源管理" class="headerlink" title="8.3 线程与资源管理"></a>8.3 线程与资源管理</h3><p>用户态多线程可以缩短实验扫描时间，但共享结果数组、停止事件和驱动句柄必须有明确的同步策略。驱动卸载时要停止工作线程、完成未决 IRP、释放池内存和对象引用。调试输出只能在命中或关键错误时记录，不能在逐页循环中无条件打印。</p><h3 id="8-4-版本适配"><a href="#8-4-版本适配" class="headerlink" title="8.4 版本适配"></a>8.4 版本适配</h3><p><code>OBJECT_DIRECTORY</code>、<code>LDR_DATA_TABLE_ENTRY</code> 以及其他内核私有结构都可能随 Windows 版本变化。工程上应把版本识别、符号验证和字段读取失败作为正常分支，而不是假设所有系统都使用同一组偏移。对不确定的数据，宁可返回“未知”，也不要用危险的硬编码继续访问。</p><h2 id="9-当前实测边界"><a href="#9-当前实测边界" class="headerlink" title="9. 当前实测边界"></a>9. 当前实测边界</h2><p>已有测试使用自写 <code>DriverLoader</code> 加载 <code>TestHideDriver1.sys</code>，并通过 DebugView 观察到 <code>Driver is alive!</code> 等输出。在当时的 SnowSword 界面中，该驱动显示了名称、基址 <code>0xFFFFF80267140000</code>、大小 <code>0x1000</code> 和桌面测试路径，同时被红色标记。</p><p>同一篇记录还保留了 OpenArk 的对比截图：截图中的 OpenArk 驱动列表没有显示 <code>TestHideDriver1.sys</code>，而 SnowSword 显示了该项。这个结果能够证明两个工具在该测试状态下的枚举结果不同，但不能单凭截图断言某个工具一定正确，也不能据此推出对所有隐藏方式都有效。</p><p>目前仍缺少以下数据：</p><ul><li>干净系统基线和多次重复测试；</li><li>模块、对象目录和内存候选的逐项对照；</li><li>扫描耗时、CPU 占用和 IOCTL 次数；</li><li>非分页池、清除 PE 头和不同 Windows 版本的验证。</li></ul><h2 id="10-后续实验计划"><a href="#10-后续实验计划" class="headerlink" title="10. 后续实验计划"></a>10. 后续实验计划</h2><p>后续可以按照下面的顺序完善证据链：</p><table><thead><tr><th>阶段</th><th>实验内容</th><th>需要保存的证据</th></tr></thead><tbody><tr><td>基线</td><td>干净快照扫描</td><td>系统版本、模块集合、对象集合、耗时</td></tr><tr><td>正常驱动</td><td>加载普通测试驱动</td><td>加载日志、基址、大小、三视角结果</td></tr><tr><td>摘链场景</td><td>在隔离虚拟机中使用已授权测试样本</td><td>前后集合差异、WinDbg 复核、SnowSword 日志</td></tr><tr><td>性能</td><td>比较逐页和批量 IOCTL</td><td>扫描时间、CPU、请求数量、候选数量</td></tr><tr><td>兼容性</td><td>对多个 Windows 版本重复</td><td>结构验证结果、误报和漏报记录</td></tr></tbody></table><p>实现层面的 v2 方向包括：批量化内存扫描、研究非分页池定位、为 <code>DRIVER_OBJECT</code> 增加多特征交叉校验，以及把 WinDbg 验证信息纳入结果报告。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>SnowSword 隐藏驱动检测的核心不是某个特殊 API，而是把模块加载器、对象管理器和内存特征三个视角放到同一条证据链中：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">不同来源采集</span><br><span class="line">      ↓</span><br><span class="line">按基址和名称关联</span><br><span class="line">      ↓</span><br><span class="line">输出差异与证据</span><br><span class="line">      ↓</span><br><span class="line">人工复核后再做结论</span><br></pre></td></tr></table></figure><p>这种设计无法保证发现所有内核隐蔽技术，但能让攻击者必须同时处理更多独立痕迹，也让检测结果比单一列表更容易解释和复核。对于 ARK 工具，工程上的稳定性、版本适配和误报控制，与“能否扫到一个对象”同样重要。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>Windows Internals, 7th Edition（对象管理器、加载器、驱动对象和内存管理章节）</li><li>Microsoft Learn：<code>ZwQuerySystemInformation</code>、<code>ZwOpenDirectoryObject</code>、<code>ObReferenceObjectByHandle</code>、<code>ObGetObjectType</code>、<code>MmCopyMemory</code></li><li>本系列文章：<code>隐藏驱动检测学习记录</code>、<code>内存暴力搜索隐藏驱动</code>、<code>SnowSword 隐藏驱动检测实测</code></li><li>SnowSword 项目地址：<a href="https://gitee.com/le-ziyi/SnowSword">https://gitee.com/le-ziyi/SnowSword</a></li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/23/SnowSword-%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B%E5%8E%9F%E7%90%86%E4%B8%8E%E5%B7%A5%E7%A8%8B%E5%AE%9E%E7%8E%B0/</id>
    <link href="https://snow-blog.pages.dev/2026/08/23/SnowSword-%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B%E5%8E%9F%E7%90%86%E4%B8%8E%E5%B7%A5%E7%A8%8B%E5%AE%9E%E7%8E%B0/"/>
    <published>2026-08-23T08:00:00.000Z</published>
    <summary>总结 SnowSword 隐藏驱动检测的多视角设计、内核与用户态的数据流及工程实现注意事项，并明确当前实验和跨版本验证的边界。</summary>
    <title>SnowSword 隐藏驱动检测原理与工程实现：从多视角枚举到结果关联</title>
    <updated>2026-11-05T13:18:22.196Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="SnowSword Development" scheme="https://snow-blog.pages.dev/categories/SnowSword-Development/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="Anti-Rootkit" scheme="https://snow-blog.pages.dev/tags/Anti-Rootkit/"/>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/tags/Windows-Kernel/"/>
    <category term="SnowSword" scheme="https://snow-blog.pages.dev/tags/SnowSword/"/>
    <category term="驱动检测" scheme="https://snow-blog.pages.dev/tags/%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B/"/>
    <content>
      <![CDATA[<h2 id="1-引言"><a href="#1-引言" class="headerlink" title="1. 引言"></a>1. 引言</h2><p>上一篇文章介绍了 SnowSword 的隐藏驱动检测思路：同时观察模块列表、对象目录和内存中的 PE 特征，避免只依赖单一数据源。<br>这次准备了一个测试驱动 <code>TestHideDriver1.sys</code>，尝试验证 SnowSword 能否在测试过程中标记出异常驱动。目前同时保留了 SnowSword 检测结果和 OpenArk 的枚举对比截图，本文只记录截图中能够确认的现象。</p><h2 id="2-检测思路"><a href="#2-检测思路" class="headerlink" title="2. 检测思路"></a>2. 检测思路</h2><p>SnowSword 的驱动检测可以简单理解为三个视角：</p><table><thead><tr><th>视角</th><th>观察内容</th></tr></thead><tbody><tr><td>模块列表</td><td>驱动名称、映像基址、映像大小和路径</td></tr><tr><td>对象目录</td><td><code>\\Driver</code>、<code>\\FileSystem</code> 中的驱动对象</td></tr><tr><td>内存特征</td><td>内存中是否存在带有 PE 特征的可执行映像</td></tr></tbody></table><p>正常情况下，这些信息应该能够相互对应。如果某个驱动只在一个视角中出现，或者它的基址、大小和路径与其他信息对不上，就应该进入人工复核流程。<br>本次实验的重点是观察 SnowSword 对测试驱动的标记结果，而不是证明 SnowSword 的全能性。</p><h2 id="3-实验过程"><a href="#3-实验过程" class="headerlink" title="3. 实验过程"></a>3. 实验过程</h2><p>实验过程中使用自写 DriverLoader 加载测试驱动，并通过 DebugView 观察驱动输出。测试驱动加载后，在 SnowSword 的“内核模块”页面查看当前模块列表。</p><p>从截图可以看到，列表中出现了：</p><ul><li>驱动名称：<code>TestHideDriver1.sys</code>；</li><li>映像基址：<code>0xFFFFF80267140000</code>；</li><li>映像大小：<code>0x1000</code>；</li><li>路径位于用户桌面测试目录；</li><li>该行以红色显示，与周围普通模块的显示效果不同。</li></ul><h2 id="4-SnowSword-检测结果"><a href="#4-SnowSword-检测结果" class="headerlink" title="4. SnowSword 检测结果"></a>4. SnowSword 检测结果</h2><p><img src="/images/SnowSword-%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B/Screenshot%E6%91%98%E9%93%BE%E9%A9%B1%E5%8A%A8.png" alt="SnowSword 检测到测试驱动"></p><p>图中同时保留了测试过程中的几个窗口：</p><ol><li><strong>SnowSword</strong>：右侧显示内核模块列表，<code>TestHideDriver1.sys</code> 被红色标记；</li><li><strong>DriverLoader</strong>：中间窗口用于加载测试驱动；</li><li><strong>DebugView</strong>：左侧可以看到测试驱动输出的 <code>Driver is alive!</code> 等调试信息。</li></ol><p>从现象上看，测试驱动已经成功加载并运行，SnowSword 也将它与普通内核模块区分显示。这说明当前检测模块至少能够获取到该测试驱动的模块信息，并根据自身规则给出异常标记。</p><h2 id="5-与-OpenArk-的枚举结果对比"><a href="#5-与-OpenArk-的枚举结果对比" class="headerlink" title="5. 与 OpenArk 的枚举结果对比"></a>5. 与 OpenArk 的枚举结果对比</h2><p><img src="/images/SnowSword-%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B/OpenArk%E5%AF%B9%E6%AF%94.png" alt="SnowSword 与 OpenArk 驱动枚举结果对比"></p><table><thead><tr><th>工具</th><th>截图中的结果</th></tr></thead><tbody><tr><td>SnowSword</td><td>可以看到 <code>TestHideDriver1.sys</code>，并以红色标记；同时显示其基址、大小和桌面测试路径</td></tr><tr><td>OpenArk</td><td>当前驱动列表中未看到 <code>TestHideDriver1.sys</code>，只能看到 <code>SnowSword.sys</code> 和 <code>OpenArkDrv64.sys</code> 等模块</td></tr></tbody></table><h2 id="6-结果分析"><a href="#6-结果分析" class="headerlink" title="6. 结果分析"></a>6. 结果分析</h2><ul><li><code>TestHideDriver1.sys</code> 已经进入 SnowSword 的内核模块检测结果；</li><li>SnowSword 能显示它的基址、大小和路径；</li><li>SnowSword 对该测试驱动进行了红色高亮，说明它满足当前隐藏驱动的标记条件；</li><li>OpenArk 的当前驱动列表中未显示该测试驱动，形成了与 SnowSword 的枚举差异；</li><li>DriverLoader 和 DebugView 的窗口可以作为驱动加载和运行状态的辅助证据。</li></ul><h2 id="7-为什么不能只看一个列表"><a href="#7-为什么不能只看一个列表" class="headerlink" title="7. 为什么不能只看一个列表"></a>7. 为什么不能只看一个列表</h2><p>驱动检测和进程隐藏检测一样，不能把单一枚举结果当成绝对事实。<br>如果只看模块列表，攻击者可以尝试摘除模块链表记录；如果只看对象目录，也可能遇到未注册对象或结构变化；如果只扫 PE 特征，又可能把残留映像、缓存或版本差异误判为异常。</p><p>更可靠的判断方式是把多个结果关联起来：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">驱动名称 / 基址 / 大小</span><br><span class="line">          |</span><br><span class="line">          +-- 是否存在于模块列表</span><br><span class="line">          +-- 是否能关联到 DRIVER_OBJECT</span><br><span class="line">          +-- 是否位于合法映像范围</span><br><span class="line">          +-- 是否有对应的运行时行为</span><br></pre></td></tr></table></figure><h2 id="8-当前局限"><a href="#8-当前局限" class="headerlink" title="8. 当前局限"></a>8. 当前局限</h2><p>本次实验仍然比较初步，主要局限包括：</p><ol><li>目前只有两张截图，没有干净系统和多次重复测试数据；</li><li>尚未记录完整的扫描范围、耗时、CPU 占用和 IOCTL 次数；</li><li>尚未对 <code>\\Driver</code>、<code>\\FileSystem</code> 和内存 PE 扫描结果进行逐项对照；</li><li>尚未验证非分页池、清除 PE 头以及跨 Windows 版本等场景；</li><li>红色高亮的具体触发规则还需要结合 SnowSword 源码进一步说明。</li></ol><h2 id="9-后续计划"><a href="#9-后续计划" class="headerlink" title="9. 后续计划"></a>9. 后续计划</h2><p>后续补充实验时，准备保留以下信息：</p><ul><li>测试前后的模块列表对比；</li><li>测试驱动是否仍能在对象目录中找到；</li><li>内存扫描命中的地址和模块归属结果；</li><li>SnowSword 的原始调试日志；</li><li>不同 Windows 版本下的重复测试结果。</li></ul><p>这样可以把“截图中看到驱动被标红”进一步扩展为完整的证据链，减少单一结果带来的误判。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本次实验中，<code>TestHideDriver1.sys</code> 成功加载并运行，SnowSword 在内核模块列表中对它进行了红色标记，而 OpenArk 的当前驱动列表中没有显示该项。这个结果说明 SnowSword 与 OpenArk 对该测试状态的枚举结果存在差异，也说明多视角交叉检测能够发现单一工具不一定展示的对象，但这只是一次功能验证，而不是完整的隐藏驱动检测实验。</p><p>对于 ARK 工具来说，发现异常只是第一步。只有把模块列表、对象目录、内存特征和运行时行为结合起来，并经过独立工具复核，才能逐步提高检测结果的可信度。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>Windows Internals, 7th Edition</li><li>Microsoft Learn：<code>IMAGE_NT_HEADERS</code>、<code>MmCopyMemory</code></li><li>本系列前篇：<code>日报_隐藏驱动检测_改进版</code>、<code>日报_内存暴力搜索DRIVER_OBJECT_改进版</code></li><li>SnowSword 项目地址：<a href="https://gitee.com/le-ziyi/SnowSword">https://gitee.com/le-ziyi/SnowSword</a></li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/21/%E6%97%A5%E6%8A%A5_SnowSword%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B%E5%AE%9E%E6%B5%8B/</id>
    <link href="https://snow-blog.pages.dev/2026/08/21/%E6%97%A5%E6%8A%A5_SnowSword%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B%E5%AE%9E%E6%B5%8B/"/>
    <published>2026-08-21T12:00:00.000Z</published>
    <summary>记录 SnowSword 对测试隐藏驱动的检测过程，通过内核模块列表和测试窗口观察异常驱动，并与 OpenArk 的枚举结果进行对比。</summary>
    <title>SnowSword 隐藏驱动检测实测：摘链驱动的识别</title>
    <updated>2026-11-05T13:18:22.197Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/categories/Windows-Kernel/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="Anti-Rootkit" scheme="https://snow-blog.pages.dev/tags/Anti-Rootkit/"/>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/tags/Windows-Kernel/"/>
    <category term="SnowSword" scheme="https://snow-blog.pages.dev/tags/SnowSword/"/>
    <category term="驱动检测" scheme="https://snow-blog.pages.dev/tags/%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B/"/>
    <category term="内存特征扫描" scheme="https://snow-blog.pages.dev/tags/%E5%86%85%E5%AD%98%E7%89%B9%E5%BE%81%E6%89%AB%E6%8F%8F/"/>
    <content>
      <![CDATA[<h2 id="1-伏笔：上一篇留下的那个盲区"><a href="#1-伏笔：上一篇留下的那个盲区" class="headerlink" title="1. 伏笔：上一篇留下的那个盲区"></a>1. 伏笔：上一篇留下的那个盲区</h2><p>上一篇（隐藏驱动检测）我讲了两个视角的交叉验证：模块列表（<code>PsLoadedModuleList</code>）和对象目录（<code>\Driver</code>、<code>\FileSystem</code>）。写完我顺手模拟了一下攻击者视角，发现一个问题：</p><blockquote><p>如果攻击者 <strong>摘链 <code>PsLoadedModuleList</code></strong> + <strong>不注册 <code>\Driver</code> 对象</strong>，两个视角会同时落空——驱动”理论上不存在”，但它明明在内核里跑着。</p></blockquote><p>这就是所谓的<strong>纯内存驱动</strong>（memory-only driver）：不落地、不注册对象，代码直接拷进内核内存执行。传统的驱动检测手段对它全部失效。</p><p>我当时在上一篇结尾留了个伏笔：<code>DRIVER_OBJECT</code> 是真实存在于内核内存里的结构，能不能<strong>直接扫内存把它捞出来</strong>？这篇就来填这个坑。</p><h2 id="2-攻击者模型：驱动到底怎么”藏”"><a href="#2-攻击者模型：驱动到底怎么”藏”" class="headerlink" title="2. 攻击者模型：驱动到底怎么”藏”"></a>2. 攻击者模型：驱动到底怎么”藏”</h2><p>先明确一下我们要对抗的威胁模型。一个驱动想隐藏，需要同时做两件事：</p><table><thead><tr><th>视角</th><th>隐藏手段</th></tr></thead><tbody><tr><td>模块列表（<code>PsLoadedModuleList</code>）</td><td>摘链：从双向链表里摘掉自己的 <code>LDR_DATA_TABLE_ENTRY</code></td></tr><tr><td>对象目录（<code>\Driver</code> 等）</td><td>不注册 <code>DRIVER_OBJECT</code> 到对象管理器命名空间</td></tr></tbody></table><p>注意一个关键点：<strong>这两个手段都是在”登记处”动手脚，而不是销毁对象本身</strong>。驱动要跑，<code>DRIVER_OBJECT</code> 这个结构体就必须活在内存里——它是 I&#x2F;O 子系统操作驱动的唯一凭据（派发例程、卸载例程都挂在上面）。</p><p>所以问题变成了：<strong>怎么找到这块”没登记但真实存在”的可执行内存？</strong></p><blockquote><p><strong>如果我是攻击者，怎么让检测失效？</strong></p><ol><li><strong>双杀</strong>：摘链 <code>PsLoadedModuleList</code> + 同时从 <code>\Driver</code> 目录删除对象（需调用 <code>ObMakeTemporaryObject</code> 等，但也会留下新的痕迹）；</li><li><strong>不注册 DRIVER_OBJECT</strong>：纯内存执行，两个视角都看不到 → 引出本篇方案；</li><li><strong>伪造 DriverSection-&gt;DllBase</strong>：指向一个合法模块的基址，交叉比对时被”吸附”到合法基址，产生假阴性；</li><li><strong>彻底 shellcode 化</strong>：无 PE 头、无驱动对象，纯手工 hook 内核代码——这是 inline hook 检测的战场（见本系列后续篇）。</li></ol><p>检测的本质不是”找到所有坏人”，而是<strong>提高攻击者的隐藏成本</strong>。</p></blockquote><h2 id="3-方案对比：DRIVER-OBJECT-特征-vs-PE-头特征"><a href="#3-方案对比：DRIVER-OBJECT-特征-vs-PE-头特征" class="headerlink" title="3. 方案对比：DRIVER_OBJECT 特征 vs PE 头特征"></a>3. 方案对比：DRIVER_OBJECT 特征 vs PE 头特征</h2><p>理论上可以直接扫描 <code>DRIVER_OBJECT</code> 结构体特征，但我最终选择了 PE 头特征。以下是两条路线的对比和取舍过程。</p><h3 id="3-1-DRIVER-OBJECT-特征（精确但脆）"><a href="#3-1-DRIVER-OBJECT-特征（精确但脆）" class="headerlink" title="3.1 DRIVER_OBJECT 特征（精确但脆）"></a>3.1 DRIVER_OBJECT 特征（精确但脆）</h3><p><code>DRIVER_OBJECT</code> 是驱动在内核里的身份证。它在 x64 上是 0x150 字节，关键字段如下（以 Win10 1809 x64 实测布局为准，我用 IDA 反编译 afd.sys 的 <code>DriverEntry</code> 验证过字段用法）：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> _<span class="title">DRIVER_OBJECT</span> &#123;</span></span><br><span class="line">    CSHORT Type;                    <span class="comment">// +0x00  ← 类型魔数 IO_TYPE_DRIVER (0x0B)</span></span><br><span class="line">    CSHORT Size;                    <span class="comment">// +0x02  ← 结构大小 0x150</span></span><br><span class="line">    PDEVICE_OBJECT DeviceObject;    <span class="comment">// +0x08</span></span><br><span class="line">    ULONG Flags;                    <span class="comment">// +0x10</span></span><br><span class="line">    PVOID DriverStart;              <span class="comment">// +0x18  ← 驱动代码基址</span></span><br><span class="line">    ULONG DriverSize;               <span class="comment">// +0x20</span></span><br><span class="line">    PVOID DriverSection;            <span class="comment">// +0x28  ← LDR_DATA_TABLE_ENTRY*（模块链表节点）</span></span><br><span class="line">    PDRIVER_EXTENSION DriverExtension;  <span class="comment">// +0x30 ← 扩展结构，内含回指</span></span><br><span class="line">    UNICODE_STRING DriverName;      <span class="comment">// +0x38  ← 驱动名字符串</span></span><br><span class="line">    PUNICODE_STRING HardwareDatabase; <span class="comment">// +0x48</span></span><br><span class="line">    PFAST_IO_DISPATCH FastIoDispatch; <span class="comment">// +0x50</span></span><br><span class="line">    PVOID DriverInit;               <span class="comment">// +0x58</span></span><br><span class="line">    PVOID DriverStartIo;            <span class="comment">// +0x60</span></span><br><span class="line">    PVOID DriverUnload;             <span class="comment">// +0x68</span></span><br><span class="line">    PVOID MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + <span class="number">1</span>]; <span class="comment">// +0x70，共 28 个</span></span><br><span class="line">&#125; DRIVER_OBJECT;</span><br></pre></td></tr></table></figure><blockquote><p>💡 <strong>类比理解</strong>：<code>MajorFunction</code> 就是一张 28 格子的”接线表”，每个格子对应一类 IRP，填的是处理函数地址。驱动要处理什么请求，就往对应格子塞函数指针。这张表就是驱动功能的索引——攻击者想改驱动行为，或者我们想识别驱动，都得看它。</p></blockquote><p><strong>三个最值钱的”指纹”</strong>（组合起来几乎不可能被自然数据巧合满足）：</p><ol><li><strong><code>Type == IO_TYPE_DRIVER (0x0B)</code></strong>：I&#x2F;O 子系统初始化驱动对象时写入的魔数；</li><li><strong><code>DriverExtension-&gt;DriverObject == 自身</code></strong>：<code>_DRIVER_EXTENSION</code> 第一个字段回指驱动对象指针。<strong>这是最强的判据</strong>——相互引用、自我指涉的数据结构，随机数据几乎不可能形成这种闭环；</li><li><strong><code>MajorFunction[0..27]</code> 全部指向内核代码区</strong>：28 个指针都必须落在内核可执行地址范围内。</li></ol><p><strong>但这条路我最终没选</strong>，原因很现实：</p><ul><li><strong>布局跨版本敏感</strong>：字段偏移、<code>IO_TYPE_DRIVER</code> 魔数在不同 Windows 版本可能不同；</li><li><strong>扫描范围难定</strong>：对象在非分页池，而 Win10 1809+ 非分页池定位本身就很麻烦；</li><li><strong>实现成本高</strong>：需精确结构布局 + 池定位，维护负担重。</li></ul><blockquote><p><strong>结论</strong>：DRIVER_OBJECT 四关卡校验留着当<strong>升级方案</strong>，先不实现。</p></blockquote><h3 id="3-2-PE-头特征（粗糙但稳）"><a href="#3-2-PE-头特征（粗糙但稳）" class="headerlink" title="3.2 PE 头特征（粗糙但稳）"></a>3.2 PE 头特征（粗糙但稳）</h3><p>驱动是一个 <strong>PE 镜像</strong>，必然以 <code>MZ</code> 头（<code>IMAGE_DOS_SIGNATURE</code>）+ <code>PE</code> 签名（<code>IMAGE_NT_SIGNATURE</code>）开头。这个特征：</p><ul><li><strong>跨版本稳定</strong>（PE 格式几十年没变过）；</li><li><strong>只需读内存开头几十字节</strong>（实现成本低）；</li><li><strong>配合模块列表差集</strong>就能区分正常驱动和隐藏驱动。</li></ul><blockquote><p><strong>设计取舍</strong>：与其”扫结构体特征”（精确但脆），不如”扫 PE 镜像特征 + 和模块列表做差集”（粗糙但稳）——<strong>凡是带合法 PE 头、又不在模块列表里的可执行页 &#x3D; 疑似隐藏驱动</strong>。</p></blockquote><table><thead><tr><th>维度</th><th>PE 头特征（本篇实现）</th><th>DRIVER_OBJECT 特征（升级方案）</th></tr></thead><tbody><tr><td>特征位置</td><td>镜像开头（MZ&#x2F;PE 签名）</td><td>结构体内部（Type&#x2F;回指&#x2F;MajorFunction）</td></tr><tr><td>跨版本稳定性</td><td>高（PE 格式恒定）</td><td>低（结构布局随版本变）</td></tr><tr><td>能抓到</td><td>PE 形态的隐藏驱动</td><td>有完整驱动对象结构的隐藏驱动</td></tr><tr><td>实现成本</td><td>低（读几十字节）</td><td>高（需精确布局+池定位）</td></tr></tbody></table><h2 id="4-扫描范围与性能考量"><a href="#4-扫描范围与性能考量" class="headerlink" title="4. 扫描范围与性能考量"></a>4. 扫描范围与性能考量</h2><p>这一步我纠结了最久，也是我觉得这篇最有价值的部分——<strong>怎么在合理时间内扫完内核内存</strong>，以下都是我的试错过程。</p><h3 id="4-1-无脑方案：全地址空间逐字节扫"><a href="#4-1-无脑方案：全地址空间逐字节扫" class="headerlink" title="4.1 无脑方案：全地址空间逐字节扫"></a>4.1 无脑方案：全地址空间逐字节扫</h3><p>x64 内核地址空间约 <strong>128 TB</strong>。按 16 字节步进 &#x3D; <strong>8 万亿次检查</strong>，而且大部分地址是无效映射，直接读会触发页面错误蓝屏。这个方案在第一分钟就该被毙掉。</p><h3 id="4-2-改进：按页遍历-先验页有效性"><a href="#4-2-改进：按页遍历-先验页有效性" class="headerlink" title="4.2 改进：按页遍历 + 先验页有效性"></a>4.2 改进：按页遍历 + 先验页有效性</h3><p>128 TB &#x2F; 4 KB &#x3D; <strong>340 亿页</strong>，每页一次 <code>MmIsAddressValid</code>，哪怕单次 100ns 也要近 1 小时。作为 ARK 的”开机扫描”仍然不可接受。</p><h3 id="4-3-最终选择：先扫”内核驱动加载区”（有已知盲区）"><a href="#4-3-最终选择：先扫”内核驱动加载区”（有已知盲区）" class="headerlink" title="4.3 最终选择：先扫”内核驱动加载区”（有已知盲区）"></a>4.3 最终选择：先扫”内核驱动加载区”（有已知盲区）</h3><p>穷举地址空间不现实，但纯内存驱动的代码总不能凭空出现——它要么拷进<strong>驱动加载区</strong>（ntoskrnl 附近的系统模块区域），要么落在<strong>非分页池</strong>。</p><p>我选择先扫 <code>[ntoskrnl基址 - 0x70000000, ntoskrnl基址]</code> 的驱动加载区：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27; 扫描范围 = [ntoskrnl基址 - 1.75GB, ntoskrnl基址]</span></span><br><span class="line"><span class="string">ulonglong endAddr = GetKernelModuleBase(&quot;ntoskrnl.exe&quot;, &amp;ModuleSize);</span></span><br><span class="line"><span class="string">uonglong startAddr = EndAddr - 0x70000000;</span></span><br></pre></td></tr></table></figure><p>共约 <strong>44.8 万页</strong>，比 128 TB 全空间小了四个数量级，从”扫不完”变成”可接受”。</p><p><strong>已知盲区</strong>：</p><ul><li>攻击者若将驱动代码拷到<strong>非分页池高地址段</strong>，本版扫不到——<strong>这是当前实现最现实的绕过路径</strong>；</li><li>v2 计划补全非分页池定位（见第 8 节）。</li></ul><h3 id="4-4-多线程分片"><a href="#4-4-多线程分片" class="headerlink" title="4.4 多线程分片"></a>4.4 多线程分片</h3><p>44.8 万页不能单线程傻扫。解决方式是<strong>多线程分片</strong>：把 <code>[StartAddr, EndAddr]</code> 按线程数均分，每个线程扫一段，互斥锁保护共享结果数组。</p><p><strong>关键：区间边界必须按 <code>PAGE_SIZE</code> 对齐</strong>，否则 <code>StartAddr + i * addrRange</code> 落在页中间，漏掉页首的 MZ 头（PE 头在镜像基址&#x3D;页首）。我一开始没对齐，扫出来的结果少了一大截，debug 半天才发现。</p><h3 id="4-5-性能优化（架构设计阶段即考虑）"><a href="#4-5-性能优化（架构设计阶段即考虑）" class="headerlink" title="4.5 性能优化（架构设计阶段即考虑）"></a>4.5 性能优化（架构设计阶段即考虑）</h3><p>第一版实现是<strong>每页一次 IOCTL</strong>——44.8 万页就是 44.8 万次用户态↔内核态往返。作为验证思路的第一版能接受，但正式功能必须优化：</p><ul><li><strong>批量化</strong>：把一批页地址塞进一个缓冲区，一次 IOCTL 让驱动循环检查，回传结果数组——IOCTL 次数从 44.8 万降到几十；</li><li><strong>驱动内差集</strong>：干脆驱动直接拿 <code>SystemModuleInformation</code> 缓存模块基址集合，在驱动内完成差集，用户态只收最终结果。</li></ul><blockquote><p>本版先跑通每页 IOCTL 的验证逻辑，优化在 v2 实现。</p></blockquote><h2 id="5-特征校验：PE-头-模块列表差集"><a href="#5-特征校验：PE-头-模块列表差集" class="headerlink" title="5. 特征校验：PE 头 + 模块列表差集"></a>5. 特征校验：PE 头 + 模块列表差集</h2><p>范围扫到了，接下来是”怎么算命中”。命中判定拆成两半：<strong>驱动端认 PE 头</strong>，<strong>用户态认模块归属</strong>。</p><h3 id="5-1-驱动端：MZ-→-PE-签名两步走"><a href="#5-1-驱动端：MZ-→-PE-签名两步走" class="headerlink" title="5.1 驱动端：MZ → PE 签名两步走"></a>5.1 驱动端：MZ → PE 签名两步走</h3><p>内核里逐页检查，读页首数据验证 PE 头。安全读是核心——<strong>内核地址不是都能直接解引用的</strong>，读无效页会直接蓝屏，所以必须先预检再拷贝：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ⚠️ 以下为核心逻辑示意，非完整可编译代码</span></span><br><span class="line"><span class="comment">// 实际实现需自行补全安全拷贝、异常处理、入参校验</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 1. 页对齐粗检 + ProbeForRead 快速预检（比直接拷贝快一个量级）</span></span><br><span class="line"><span class="comment">// 2. 安全拷贝 DOS 头（MZ 魔数）——必须用 MmCopyMemory/VxkCopyMemory，不能直接解引用</span></span><br><span class="line"><span class="keyword">if</span> (dosHeader.e_magic != IMAGE_DOS_SIGNATURE)   <span class="comment">// &#x27;MZ&#x27;</span></span><br><span class="line">    <span class="keyword">return</span> Normal;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. MZ 命中，再安全拷贝 PE 头（e_lfanew 指向 PE 签名）</span></span><br><span class="line"><span class="keyword">if</span> (ntHeader.Signature == IMAGE_NT_SIGNATURE) &#123;  <span class="comment">// &#x27;PE\0\0&#x27;</span></span><br><span class="line">    <span class="comment">// 4. 疑似 PE 镜像 → 标记为候选隐藏驱动，交用户态做模块归属差集</span></span><br><span class="line">    <span class="keyword">return</span> HideDriver;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>💡 <strong>类比理解</strong>：这就像保安查门禁——每个进楼的人都要带工牌（PE 头），保安只拦”没登记的人”（模块列表里没有的 PE）。工牌本身人人都有，关键在登记簿上有没有你的名字。</p></blockquote><h3 id="5-2-用户态：和模块列表做差集"><a href="#5-2-用户态：和模块列表做差集" class="headerlink" title="5.2 用户态：和模块列表做差集"></a>5.2 用户态：和模块列表做差集</h3><p>驱动端只负责”这页长得像 PE 镜像”，<strong>它不区分正常驱动和隐藏驱动</strong>（正常驱动也是 PE）。区分工作交给用户态：命中页地址用 <code>GetDriverPathByAddr</code> 反查它属于哪个已加载模块——</p><ul><li><strong>能查到路径</strong> → 是已知模块的镜像，正常；</li><li><strong>查不到</strong> → 游离 PE 镜像 &#x3D; 疑似隐藏驱动，记录地址，之后和模块列表交叉复核。</li></ul><h2 id="6-实现：驱动端扫-用户态多线程调度"><a href="#6-实现：驱动端扫-用户态多线程调度" class="headerlink" title="6. 实现：驱动端扫 + 用户态多线程调度"></a>6. 实现：驱动端扫 + 用户态多线程调度</h2><h3 id="6-1-总体架构"><a href="#6-1-总体架构" class="headerlink" title="6.1 总体架构"></a>6.1 总体架构</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">用户态（FreeBasic）                    驱动端（SnowSword.sys）</span><br><span class="line">┌─────────────────────┐   IOCTL    ┌─────────────────────────┐</span><br><span class="line">│ 多线程分片扫描        │ ─────────▶ │ OptimizedScanKernelMemory│</span><br><span class="line">│ 每页发一次 IOCTL 询问  │ ◀───────── │ 逐页 PE 特征检查          │</span><br><span class="line">│ 互斥锁保护结果数组     │           └─────────────────────────┘</span><br><span class="line">│ GetDriverPathByAddr  │</span><br><span class="line">│ 差集 → 展示          │</span><br><span class="line">└─────────────────────┘</span><br></pre></td></tr></table></figure><h3 id="6-2-用户态线程工作函数（核心逻辑）"><a href="#6-2-用户态线程工作函数（核心逻辑）" class="headerlink" title="6.2 用户态线程工作函数（核心逻辑）"></a>6.2 用户态线程工作函数（核心逻辑）</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27; ⚠️ 以下为核心逻辑示意，非完整可编译代码</span></span><br><span class="line"><span class="string">&#x27;</span> 实际实现需自行补全错误处理、内存管理、类型安全</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (ulonglong addr = p-&gt;startAddr ; ddr &lt;= p-&gt;endAddr ; addr += PAGE_SIZE) &#123;</span><br><span class="line">    stMemory.addr   = (PVOID)addr;</span><br><span class="line">    stMemory.dwSize = PAGE_SIZE;</span><br><span class="line"></span><br><span class="line">    <span class="string">&#x27; 每页发一次 IOCTL，让驱动检查这页是不是 PE 镜像</span></span><br><span class="line"><span class="string">    if (IoControl(hDrv, IOCTL_ScanKernelMemory, @stMemory, SizeOf(MemoryStruct),</span></span><br><span class="line"><span class="string">                 @result, SizeOf(MEMORY_SCAN_RESULT)) &lt;&gt; 0) &#123;</span></span><br><span class="line"><span class="string">        if (result &lt;&gt; Normal) &#123;</span></span><br><span class="line"><span class="string">            &#x27;</span> 命中：互斥锁保护下记录结果</span><br><span class="line">            MutexLock(p-&gt;hMutex);</span><br><span class="line">            <span class="string">&#x27; 记录 Addr 到共享数组</span></span><br><span class="line"><span class="string">            MutexUnlock(p-&gt;hMutex);</span></span><br><span class="line"><span class="string">        &#125;</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">&#125;</span></span><br></pre></td></tr></table></figure><p>命中后用户态再调 <code>GetDriverPathByAddr(Addr)</code> 判断模块归属：有路径 → 正常模块；无路径 → 疑似隐藏驱动。</p><h2 id="7-实验计划（待执行）"><a href="#7-实验计划（待执行）" class="headerlink" title="7. 实验计划（待执行）"></a>7. 实验计划（待执行）</h2><blockquote><p><strong>⚠️ 本文是设计稿，实验结果将在后续补充。</strong></p></blockquote><table><thead><tr><th>步骤</th><th>内容</th><th>状态</th></tr></thead><tbody><tr><td>Step 1</td><td>干净系统自检：扫描驱动区，所有 PE 命中都应能归属到已知模块</td><td>⏳ 待执行</td></tr><tr><td>Step 2</td><td>对抗测试：构造纯内存驱动样本（摘链 + 不注册对象），验证能否检出</td><td>⏳ 待执行</td></tr></tbody></table><h3 id="Step-1：特征自检（先证明扫描没瞎）"><a href="#Step-1：特征自检（先证明扫描没瞎）" class="headerlink" title="Step 1：特征自检（先证明扫描没瞎）"></a>Step 1：特征自检（先证明扫描没瞎）</h3><p>在干净的 Win10 1809 虚拟机里，加载 SnowSword 驱动跑扫描：</p><ol><li>扫描 <code>[ntoskrnl基址 - 0x70000000, ntoskrnl基址]</code> 驱动区，收集所有 PE 特征命中页；</li><li>对每个命中地址调 <code>GetDriverPathByAddr</code> 反查模块归属，与 <code>SystemModuleInformation</code> 模块列表交叉；</li><li><strong>预期</strong>：正常系统里，所有命中都应能归属到已知模块（<code>ntoskrnl</code>、<code>hal</code>、<code>afd</code> 等）；<strong>归属不到模块的命中数为 0</strong>——这是自检通过的第一道标准；</li><li>用 windbg <code>lm</code> &#x2F; <code>!drvobj</code> 抽查几个命中地址，确认 PE 头校验没有把数据区误认成镜像。</li></ol><blockquote><p>自检通过的标准：<strong>命中全部可解释</strong>——每个 PE 命中都能对应一个已知模块，没有莫名其妙的多余项，也没有把正常驱动漏掉。</p></blockquote><h3 id="Step-2：对抗测试（证明能揪出隐藏驱动）"><a href="#Step-2：对抗测试（证明能揪出隐藏驱动）" class="headerlink" title="Step 2：对抗测试（证明能揪出隐藏驱动）"></a>Step 2：对抗测试（证明能揪出隐藏驱动）</h3><p>构造一个”纯内存驱动”样本：</p><ol><li>写一个普通驱动，<code>DriverEntry</code> 里自摘链：把自己的 <code>DriverSection-&gt;InLoadOrderLinks</code> 从 <code>PsLoadedModuleList</code> 摘掉；</li><li>不调用 <code>IoCreateDevice</code>（不产生设备对象），也不依赖 <code>\Driver</code> 目录注册的常规路径——模拟”双视角皆盲”；</li><li>再跑一遍扫描，<strong>预期</strong>：模块列表和对象目录都看不到它，但 PE 扫描按镜像特征把它捞出来——<code>GetDriverPathByAddr</code> 查不到归属 &#x3D; 命中列表里出现一个”游离 PE”；</li></ol><h2 id="8-已知局限与后续计划"><a href="#8-已知局限与后续计划" class="headerlink" title="8. 已知局限与后续计划"></a>8. 已知局限与后续计划</h2><h3 id="8-1-当前局限"><a href="#8-1-当前局限" class="headerlink" title="8.1 当前局限"></a>8.1 当前局限</h3><ol><li><strong>扫描范围盲区</strong>：只扫驱动加载区，非分页池高地址段未覆盖——攻击者把代码拷到非分页池即可绕过；</li><li><strong>IOCTL 频率</strong>：每页一次 IOCTL，44.8 万次上下文切换，全扫分钟级；</li><li><strong>PE 头可抹除</strong>：攻击者加载后擦掉 MZ&#x2F;PE 魔数即可让本方案失效（但会破坏 I&#x2F;O 子系统的加载假设，纯 shellcode 才能这么干）。</li></ol><h3 id="8-2-后续计划"><a href="#8-2-后续计划" class="headerlink" title="8.2 后续计划"></a>8.2 后续计划</h3><ul><li><strong>v2</strong>：补全非分页池定位（Win10 1809+ 无 <code>MmNonPagedPoolStart/End</code> 导出，需特征扫 <code>_POOL_DESCRIPTOR</code> 或借 windbg&#x2F;PDB）；</li><li><strong>升级方案</strong>：DRIVER_OBJECT 四关卡校验（Type + 回指 + MajorFunction），覆盖”抹掉 PE 头但保留驱动对象”的场景。</li></ul><h2 id="9-遇到的问题与注意事项"><a href="#9-遇到的问题与注意事项" class="headerlink" title="9. 遇到的问题与注意事项"></a>9. 遇到的问题与注意事项</h2><ol><li><strong>页对齐</strong>：扫描区间边界必须按 <code>PAGE_SIZE</code> 对齐，否则漏掉页首的 MZ 头（4.4 节有详细说明）；</li><li><strong>地址类型宽度</strong>：内核地址是 64 位的，用户态保存扫描范围&#x2F;结果时必须用 <code>ULongInt</code>，不能用 <code>ULong</code>。截断后起始地址直接错位，扫出来全是噪音；</li><li><strong>安全读内核内存</strong>：不能直接解引用，必须先 <code>ProbeForRead</code> 预检 + <code>MmCopyMemory</code> 安全拷贝 + <code>__try/__except</code> 兜底——缺一步都可能蓝屏；</li><li><strong>IOCTL 频率</strong>：每页一次 IOCTL 有几十万次上下文切换，全扫分钟级；正式功能必须批量化（见 4.5）；</li><li><strong>调试输出</strong>：<code>DbgPrint</code> 别放在逐页循环里，命中才会打还好，每页都打能把调试器刷到卡死；</li><li><strong>误报</strong>：PE 头校验命中的”游离镜像”只是疑似，需和模块列表差集 + 人工复核，不自动判定恶意。</li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>Windows Internals, 7th Edition（加载器 &#x2F; 驱动对象 &#x2F; 内存管理章节）</li><li>Microsoft Learn： IMAGE_NT_HEADERS</li><li>本系列前篇：日报_隐藏驱动检测_改进版（本篇的伏笔出处）、SnowSword–进程隐藏检测实现</li><li>实验佐证：IDA 反编译 win10 1809 afd.sys DriverEntry（DRIVER_OBJECT 字段用法，为升级方案做的预研）</li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/16/%E6%97%A5%E6%8A%A5_%E5%86%85%E5%AD%98%E6%9A%B4%E5%8A%9B%E6%90%9C%E7%B4%A2DRIVER_OBJECT_%E6%94%B9%E8%BF%9B%E7%89%88/</id>
    <link href="https://snow-blog.pages.dev/2026/08/16/%E6%97%A5%E6%8A%A5_%E5%86%85%E5%AD%98%E6%9A%B4%E5%8A%9B%E6%90%9C%E7%B4%A2DRIVER_OBJECT_%E6%94%B9%E8%BF%9B%E7%89%88/"/>
    <published>2026-08-16T09:50:00.000Z</published>
    <summary>承接上一篇&quot;隐藏驱动检测&quot;的盲区：如果攻击者既摘链 PsLoadedModuleList、又不注册 \Driver 对象，模块列表和对象目录两个视角全部落空。本文探索第三条路——暴力扫描内核驱动区内存，凡是&quot;带 PE 头却不在模块列表里&quot;的可执行镜像就是疑似纯内存隐藏驱动。记录我从 DRIVER_OBJECT 特征扫描转向 PE 特征扫描的设计取舍、多线程扫描实现，以及可复现的实验方案。</summary>
    <title>内存暴力搜索隐藏驱动 —— PE 特征扫描 + 模块列表差集</title>
    <updated>2026-09-21T04:20:05.616Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/categories/Windows-Kernel/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="Anti-Rootkit" scheme="https://snow-blog.pages.dev/tags/Anti-Rootkit/"/>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/tags/Windows-Kernel/"/>
    <category term="SnowSword" scheme="https://snow-blog.pages.dev/tags/SnowSword/"/>
    <category term="驱动检测" scheme="https://snow-blog.pages.dev/tags/%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B/"/>
    <category term="对象管理器" scheme="https://snow-blog.pages.dev/tags/%E5%AF%B9%E8%B1%A1%E7%AE%A1%E7%90%86%E5%99%A8/"/>
    <content>
      <![CDATA[<h2 id="1-缘起：ARK-为什么必须查隐藏驱动"><a href="#1-缘起：ARK-为什么必须查隐藏驱动" class="headerlink" title="1. 缘起：ARK 为什么必须查隐藏驱动"></a>1. 缘起：ARK 为什么必须查隐藏驱动</h2><p>之前几篇都在做进程隐藏检测（PID 暴力扫描、PspCidTable 遍历），核心哲学是<strong>多视角交叉验证</strong>——Windows 里没有绝对可信的单一视角，攻击者篡改一个数据源容易，同时篡改多个独立数据源成本急剧上升。</p><p>同样的道理完全适用于<strong>驱动</strong>。恶意驱动（Rootkit 的加载器、内核后门）想隐藏自己，最常见的手段是<strong>摘链 <code>PsLoadedModuleList</code></strong>——这是 Windows 内核维护的已加载模块双向链表，摘链后普通工具（任务管理器、<code>driverquery</code>、甚至 <code>NtQuerySystemInformation(SystemModuleInformation)</code>）就看不到它了。</p><p>但问题来了：驱动真的”消失”了吗？没有。只要它注册了 <code>DRIVER_OBJECT</code>，它就在对象管理器的命名空间里留下了痕迹。这就是本篇文章要用的<strong>第二个视角</strong>。</p><blockquote><p>核心思路：检测进程时可以同时看句柄表、PspCidTable、模块链表；检测驱动，同样可以同时看「模块加载器视角」和「对象管理器视角」。</p></blockquote><h2 id="2-驱动在内核里的两种”存在形式”"><a href="#2-驱动在内核里的两种”存在形式”" class="headerlink" title="2. 驱动在内核里的两种”存在形式”"></a>2. 驱动在内核里的两种”存在形式”</h2><p>一个正常加载的驱动，会在内核里同时留下两种记录：</p><p><strong>视角 1：模块加载器（<code>PsLoadedModuleList</code>）</strong></p><ul><li><code>NtQuerySystemInformation(SystemModuleInformation)</code> 返回的模块列表就来自这张链表；</li><li>SnowSword 里 <code>GetKernelModuleList</code> 已经用它枚举驱动（路径 + 基址 + 大小）；</li><li>攻击者摘链 → 这个视角就看不到了。</li></ul><p><strong>视角 2：对象管理器命名空间（<code>\Driver</code>、<code>\FileSystem</code>）</strong></p><ul><li>驱动加载时会在对象管理器根目录下注册一个 <code>DRIVER_OBJECT</code>，名字通常是 <code>\Driver\驱动名</code>；</li><li>文件系统驱动注册在 <code>\FileSystem</code>，文件系统过滤驱动在 <code>\FileSystem\Filters</code>；</li><li>这套命名空间由对象管理器独立维护，<strong>与 <code>PsLoadedModuleList</code> 是两套互不相干的机制</strong>。</li></ul><p>攻击者如果只摘链模块链表，对象管理器目录里的 <code>DRIVER_OBJECT</code> 依然在——这就是检出点。</p><h2 id="3-视角-1-回顾：模块列表枚举（已有实现）"><a href="#3-视角-1-回顾：模块列表枚举（已有实现）" class="headerlink" title="3. 视角 1 回顾：模块列表枚举（已有实现）"></a>3. 视角 1 回顾：模块列表枚举（已有实现）</h2><p>SnowSword 里已有实现，核心是两段式调用：</p><figure class="highlight vb"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&#x27; 第一遍：查询需要的缓冲区大小</span></span><br><span class="line">st = ZwQuerySystemInformation(SystemModuleInformation, buffer, bufferSize, @bufferSize)</span><br><span class="line"><span class="keyword">If</span> (st = STATUS_INFO_LENGTH_MISMATCH) <span class="keyword">Then</span> buffer = Reallocate(buffer, bufferSize)</span><br><span class="line"></span><br><span class="line"><span class="comment">&#x27; 第二遍：取实际数据</span></span><br><span class="line">st = ZwQuerySystemInformation(SystemModuleInformation, buffer, bufferSize, NULL)</span><br></pre></td></tr></table></figure><p>拿到 <code>SYSTEM_MODULE_INFORMATION</code> 后遍历 <code>Modules(i)</code>，用 <code>ImageName</code> 取驱动路径、路径最后一段作为驱动名。</p><h2 id="4-视角-2：枚举-Driver-与-FileSystem"><a href="#4-视角-2：枚举-Driver-与-FileSystem" class="headerlink" title="4. 视角 2：枚举 \Driver 与 \FileSystem"></a>4. 视角 2：枚举 <code>\Driver</code> 与 <code>\FileSystem</code></h2><p>这是本篇文章的新内容。对象管理器命名空间像文件系统一样组织目录，核心思路分三步：</p><h3 id="4-1-打开目录对象"><a href="#4-1-打开目录对象" class="headerlink" title="4.1 打开目录对象"></a>4.1 打开目录对象</h3><p>用 <code>ZwOpenDirectoryObject</code> 打开 <code>\Driver</code> 或 <code>\FileSystem</code> 目录，拿到句柄后再用 <code>ObReferenceObjectByHandle</code> 转成内核态的 <code>OBJECT_DIRECTORY*</code> 结构指针，之后关闭句柄保留对象引用——后续操作都基于结构指针，不走句柄。</p><h3 id="4-2-遍历哈希桶（核心逻辑）"><a href="#4-2-遍历哈希桶（核心逻辑）" class="headerlink" title="4.2 遍历哈希桶（核心逻辑）"></a>4.2 遍历哈希桶（核心逻辑）</h3><p><code>OBJECT_DIRECTORY</code> 内部用 <strong>37 个 <code>HashBuckets</code> 哈希桶</strong>组织目录项，每个桶是一条 <code>ChainLink</code> 链表。遍历所有桶 &#x3D; 枚举目录下所有对象。这是比 <code>ZwQueryDirectoryObject</code> 更底层的做法（公开接口只能拿名字，直接读结构能拿到对象指针）。</p><p><strong>关键遍历逻辑：</strong></p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ⚠️ 以下为核心逻辑示意，非完整可编译代码</span></span><br><span class="line"><span class="comment">// 实际实现需自行补全错误处理、引用计数、结构体验证</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; BUCKET_COUNT; i++) &#123;  <span class="comment">// BUCKET_COUNT 需验证，通常为 37</span></span><br><span class="line">    <span class="keyword">for</span> (entry = DirObj-&gt;HashBuckets[i]; </span><br><span class="line">         entry; </span><br><span class="line">         entry = entry-&gt;ChainLink) &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 1. 增加引用计数，防止遍历期间对象被释放</span></span><br><span class="line">        ObReferenceObject(entry-&gt;Object);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2. 判断是否为 DRIVER_OBJECT</span></span><br><span class="line">        <span class="keyword">if</span> (ObGetObjectType(entry-&gt;Object) == *IoDriverObjectType) &#123;</span><br><span class="line">            PDRIVER_OBJECT drv = (PDRIVER_OBJECT)entry-&gt;Object;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 3. 通过 DriverSection-&gt;DllBase 获取模块基址</span></span><br><span class="line">            <span class="comment">//    这是与视角1（模块列表）交叉验证的关键纽带</span></span><br><span class="line">            PLDR_DATA_TABLE_ENTRY ldr = drv-&gt;DriverSection;</span><br><span class="line">            <span class="keyword">if</span> (ldr) &#123;</span><br><span class="line">                PVOID base = ldr-&gt;DllBase;  <span class="comment">// ← 与 SystemModuleInformation 的基址比对</span></span><br><span class="line">                <span class="comment">// 收集 base，后续与模块列表做差集分析</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4. 释放引用计数（匹配对象除外，由上层管理生命周期）</span></span><br><span class="line">        ObDereferenceObject(entry-&gt;Object);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p><strong>为什么是 37 个桶？</strong> 37 是质数，哈希取模时分布更均匀。但<strong>这个数字未文档化</strong>，不同 Windows 版本可能变化（Win7 确认是 37，Win10&#x2F;11 需自行验证）。硬编码此值在跨版本工具中存在兼容性风险，建议通过符号解析或特征扫描动态获取。</p></blockquote><h3 id="4-3-引用计数纪律（最容易蓝屏的地方）"><a href="#4-3-引用计数纪律（最容易蓝屏的地方）" class="headerlink" title="4.3 引用计数纪律（最容易蓝屏的地方）"></a>4.3 引用计数纪律（最容易蓝屏的地方）</h3><p>遍历过程中<strong>每个对象都必须 <code>ObReferenceObject</code> &#x2F; <code>ObDereferenceObject</code> 配对</strong>，这是这段代码最容易出错的地方：</p><table><thead><tr><th>操作</th><th>后果</th></tr></thead><tbody><tr><td>漏释放（非匹配对象未 Dereference）</td><td>对象永远无法回收，内存泄漏</td></tr><tr><td>多释放</td><td>直接 BSOD</td></tr><tr><td>匹配对象未转交引用给上层</td><td>对象可能被提前释放，后续访问蓝屏</td></tr></tbody></table><p><strong>状态流转：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ObReferenceObject (引用+1)</span><br><span class="line">    ↓</span><br><span class="line">判断对象类型</span><br><span class="line">    ├─ 是 DRIVER_OBJECT 且匹配目标 → 保留引用，返回给调用者</span><br><span class="line">    └─ 其他情况 → ObDereferenceObject（引用恢复）</span><br></pre></td></tr></table></figure><p>匹配对象把引用转交给调用者后，<strong>调用者用完必须 <code>ObDereferenceObject</code></strong>，否则同样泄漏。</p><h3 id="4-4-类型判断与基址关联"><a href="#4-4-类型判断与基址关联" class="headerlink" title="4.4 类型判断与基址关联"></a>4.4 类型判断与基址关联</h3><ul><li>用 <code>ObGetObjectType() == *IoDriverObjectType</code> 判断驱动对象，走导出 API 而非手动访问对象头，避免踩到无效对象头。</li><li><code>drvObj-&gt;DriverSection</code>（<code>LDR_DATA_TABLE_ENTRY</code>）的 <code>DllBase</code> 与模块列表的基址比对——这一步把「对象管理器视角」和「模块加载器视角」用<strong>基址</strong>串了起来，正是交叉验证的基础。</li><li><strong>注意</strong>：不是每个 <code>DRIVER_OBJECT</code> 都有有效的 <code>LDR_DATA_TABLE_ENTRY</code>（<code>DriverSection</code> 可能为 NULL），访问前必须判空。</li></ul><h2 id="5-交叉验证：两个视角的差集"><a href="#5-交叉验证：两个视角的差集" class="headerlink" title="5. 交叉验证：两个视角的差集"></a>5. 交叉验证：两个视角的差集</h2><p>把两个视角的结果并排对比：</p><table><thead><tr><th>数据源</th><th>枚举方式</th><th>攻击者篡改方式</th></tr></thead><tbody><tr><td>模块列表 A</td><td><code>SystemModuleInformation</code>（源自 <code>PsLoadedModuleList</code>）</td><td>摘链模块链表</td></tr><tr><td>对象目录 B</td><td>遍历 <code>OBJECT_DIRECTORY</code> 哈希桶，枚举 <code>\Driver</code> + <code>\FileSystem</code></td><td>删除&#x2F;不注册 <code>DRIVER_OBJECT</code></td></tr></tbody></table><p>两个视角的数据源不同，但都能拿到<strong>驱动基址</strong>：视角 A 直接给 <code>dwBase</code>，视角 B 通过 <code>DriverSection-&gt;DllBase</code> 取。所以交叉验证可以<strong>按基址对齐</strong>：</p><ol><li>遍历 <code>\Driver</code>、<code>\FileSystem</code> 目录，收集所有 <code>DRIVER_OBJECT</code> 的基址；</li><li>对每个基址与模块列表 A 的基址集合比对；</li><li><strong>基址不在 A 中 &#x3D; 模块列表里没有这个驱动 &#x3D; 疑似摘链隐藏驱动</strong>——模块链表上看不到，但对象管理器里还留着 <code>DRIVER_OBJECT</code>，这就是检出点。</li></ol><p>反过来，<strong>A 有而 B 无</strong>的条目要小心：不是所有模块都会注册 <code>DRIVER_OBJECT</code>（早期 boot 加载的驱动、未创建设备对象的驱动），直接报”隐藏”会误报，需要先收集正常基线再判断。</p><h3 id="5-1-攻击者视角：如何绕过？"><a href="#5-1-攻击者视角：如何绕过？" class="headerlink" title="5.1 攻击者视角：如何绕过？"></a>5.1 攻击者视角：如何绕过？</h3><blockquote><p>如果我是攻击者，怎么让这套检测失效？</p></blockquote><ol><li><strong>双杀</strong>：摘链 <code>PsLoadedModuleList</code> + 同时从 <code>\Driver</code> 目录删除对象（需调用 <code>ObMakeTemporaryObject</code> 或类似操作，但会留下新的痕迹）；</li><li><strong>不注册 DRIVER_OBJECT</strong>：纯内存执行，两个视角都看不到 → 引出第九篇；</li><li><strong>伪造 DriverSection-&gt;DllBase</strong>：指向一个合法模块的基址，交叉比对时被”吸附”到合法基址，产生假阴性。</li></ol><p>检测的本质不是”找到所有坏人”，而是<strong>提高攻击者的隐藏成本</strong>。</p><h2 id="6-盲区与第九篇伏笔：内存暴力搜索-DRIVER-OBJECT"><a href="#6-盲区与第九篇伏笔：内存暴力搜索-DRIVER-OBJECT" class="headerlink" title="6. 盲区与第九篇伏笔：内存暴力搜索 DRIVER_OBJECT"></a>6. 盲区与第九篇伏笔：内存暴力搜索 DRIVER_OBJECT</h2><p>如果攻击者连 <code>\Driver</code> 对象都不注册，两个视角都会落空。但 <code>DRIVER_OBJECT</code> 是真实存在于内核内存里的结构，它有一个特征——结构体首部的 <code>Type</code> 字段（<code>IO_TYPE_DRIVER</code>）。所以理论上可以<strong>扫描内核内存区域，按特征寻找所有 <code>DRIVER_OBJECT</code></strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">暴力扫描内核内存</span><br><span class="line">  ↓ 校验结构特征（Type 魔数、DriverName 有效性、MajorFunction 指针合法性）</span><br><span class="line">  ↓ 找到未被任何链表/目录登记的 DRIVER_OBJECT</span><br><span class="line">纯内存隐藏驱动</span><br></pre></td></tr></table></figure><h2 id="7-遇到的问题与注意事项"><a href="#7-遇到的问题与注意事项" class="headerlink" title="7. 遇到的问题与注意事项"></a>7. 遇到的问题与注意事项</h2><ol><li><p><strong><code>OBJECT_DIRECTORY</code> 是未导出结构</strong>：<code>HashBuckets</code> 的桶数（37 个）和 <code>OBJECT_DIRECTORY_ENTRY</code> 的布局依赖系统版本，需要在自己目标系统上验证结构布局。验证方法：</p><ul><li>Windbg: <code>dt nt!_OBJECT_DIRECTORY</code></li><li>对比不同 Windows 版本的 <code>_OBJECT_DIRECTORY</code> 定义</li><li>特征扫描：Directory 对象包含固定数量的指针数组模式</li></ul></li><li><p><strong>引用计数准则</strong>：遍历中每个对象都要 <code>ObReferenceObject</code> &#x2F; <code>ObDereferenceObject</code> 配对——漏释放会引用泄漏（对象永远无法回收），多释放会直接崩溃。匹配对象把引用转交给调用者后，调用者用完必须 <code>ObDereferenceObject</code>。</p></li><li><p><strong><code>DriverSection</code> 判空</strong>：不是每个 <code>DRIVER_OBJECT</code> 都有有效的 <code>LDR_DATA_TABLE_ENTRY</code>（<code>DriverSection</code> 可能为 NULL），访问前必须判空，否则蓝屏。</p></li><li><p><strong>误报处理</strong>：<code>A 有 B 无</code> 的条目需要基线（boot 驱动、无设备对象驱动都正常），不能直接当隐藏报。</p></li><li><p><strong><code>\FileSystem\Filters</code> 子目录</strong>：MiniFilter 的驱动对象在子目录里，漏枚举会导致视野不全。</p></li><li><p><strong>BUCKET_COUNT 硬编码风险</strong>：如前所述，37 这个数字未文档化，跨版本使用需验证。</p></li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>Windows Internals, 7th Edition（对象管理器 &#x2F; 驱动对象 &#x2F; PsLoadedModuleList 章节）</li><li>Microsoft Learn：ZwOpenDirectoryObject &#x2F; ObReferenceObjectByHandle &#x2F; ObGetObjectType &#x2F; IoDriverObjectType</li><li>本系列前篇：SnowSword–进程隐藏检测实现、PspCidTable 进程枚举学习记录</li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/15/%E6%97%A5%E6%8A%A5_%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B_%E6%94%B9%E8%BF%9B%E7%89%88/</id>
    <link href="https://snow-blog.pages.dev/2026/08/15/%E6%97%A5%E6%8A%A5_%E9%9A%90%E8%97%8F%E9%A9%B1%E5%8A%A8%E6%A3%80%E6%B5%8B_%E6%94%B9%E8%BF%9B%E7%89%88/"/>
    <published>2026-08-15T10:11:00.000Z</published>
    <summary>记录 SnowSword 检测隐藏驱动的新视角：除了 SystemModuleInformation 模块列表（源自 PsLoadedModuleList，可被摘链隐藏），再通过枚举对象管理器 \\Driver 与 \\FileSystem 目录获取已注册的驱动对象，两个视角交叉验证找出隐藏驱动，并引出内存暴力搜索 DRIVER_OBJECT 的第九篇伏笔。</summary>
    <title>隐藏驱动检测学习记录 —— 枚举 \Driver 与 \FileSystem 的多视角交叉验证</title>
    <updated>2026-09-21T04:20:05.618Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/categories/Windows-Kernel/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="Anti-Rootkit" scheme="https://snow-blog.pages.dev/tags/Anti-Rootkit/"/>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/tags/Windows-Kernel/"/>
    <category term="PspCidTable" scheme="https://snow-blog.pages.dev/tags/PspCidTable/"/>
    <category term="进程枚举" scheme="https://snow-blog.pages.dev/tags/%E8%BF%9B%E7%A8%8B%E6%9E%9A%E4%B8%BE/"/>
    <category term="内核逆向" scheme="https://snow-blog.pages.dev/tags/%E5%86%85%E6%A0%B8%E9%80%86%E5%90%91/"/>
    <content>
      <![CDATA[<h2 id="1-缘起：为什么还需要第二种进程枚举视角"><a href="#1-缘起：为什么还需要第二种进程枚举视角" class="headerlink" title="1. 缘起：为什么还需要第二种进程枚举视角"></a>1. 缘起：为什么还需要第二种进程枚举视角</h2><p>在之前为 SnowSword 实现隐藏进程检测时，我用的方案是<strong>暴力扫描 PID</strong>：以 4 为步长枚举 <code>0 ~ 0x20000</code>，逐个调用 <code>PsLookupProcessByProcessId</code> 反查 EPROCESS。这个方案能用，但有几个明显的局限：</p><ol><li><strong>扫描范围是”假设”不是”机制”</strong>——<code>0x20000</code> 上限写死，高 PID 进程可能漏检；</li><li><strong>大量无效查询</strong>——有效 PID 中仍有大量未分配槽位，每次都要完整走一次查询；</li><li><strong>依赖 API 语义</strong>——<code>PsLookupProcessByProcessId</code> 本身是导出函数，如果攻击者 Hook 了内核查询路径，返回结果同样不可信。</li></ol><p>由此想到一个问题：能不能<strong>不经过任何查询 API，直接读取 Windows 内核自己维护的 PID → 进程对象映射</strong>？这个数据结构就是 <code>PspCidTable</code>。</p><blockquote><p>核心思路：公开 API 是访问内核数据结构的一个入口，直接遍历 PspCidTable 可以降低对单一查询 API 的依赖；但它不能保证绕过对内核数据结构本身的篡改，也不能消除私有结构随系统版本变化带来的兼容性风险。</p></blockquote><h2 id="2-PspCidTable-是什么"><a href="#2-PspCidTable-是什么" class="headerlink" title="2. PspCidTable 是什么"></a>2. PspCidTable 是什么</h2><p><code>PspCidTable</code> 是 <code>ntoskrnl.exe</code> 的私有全局变量，也是进程和线程共用的 <strong>CID（Client ID）表</strong>。PID 与 TID 都在其中。它不是普通用户态进程句柄表，下面的字段和编码说明也必须以具体 Windows 版本的实测结果为准。</p><p>它的类型是 <code>HANDLE_TABLE</code>（未导出结构），遍历的关键字段是 <code>TableCode</code>：</p><ul><li><strong>低 2 位</strong>：表级数——<code>0</code> 单级、<code>1</code> 二级、<code>2</code> 三级；</li><li><strong>高位</strong>：表基址（读取时用 <code>TableCode &amp; ~3</code> 抹零低 2 位）。</li></ul><p>单级&#x2F;二级&#x2F;三级结构的关系：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">单级： TableCode → Entry[]（直接索引）</span><br><span class="line">二级： TableCode → 二级表指针[512] → Entry[]</span><br><span class="line">三级： TableCode → 三级表指针[512] → 二级表指针[512] → Entry[]</span><br></pre></td></tr></table></figure><p>其中 Entry 每项 16 字节（x64）。x64 Win10 上 PspCidTable 通常为三级结构。</p><h2 id="3-如何定位-PspCidTable"><a href="#3-如何定位-PspCidTable" class="headerlink" title="3. 如何定位 PspCidTable"></a>3. 如何定位 PspCidTable</h2><p>PspCidTable 是未导出变量，不能直接引用。最直接的办法是从公开导出函数反向追踪。我这里用的是<strong>两步跳板定位法</strong>：</p><ol><li>在 <code>PsLookupProcessByProcessId</code> 开头 <code>0x100</code> 字节内搜特征码 <code>0xE8</code>（<code>call</code> 指令），解析相对偏移，得到它内部调用的 <code>PspReferenceCidTableEntry</code>；</li><li>在 <code>PspReferenceCidTableEntry</code> 内搜特征码 <code>48 8B 05</code>（<code>mov rax, [rip+disp32]</code>），再解析相对偏移，就得到 <code>PspCidTable</code> 变量的运行时地址。</li></ol><p>调试时的核心反汇编输出如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">fffff806`ca714a2e 488b0583126b00  mov rax,qword ptr [nt!PspCidTable (fffff806`cadc5cb8)]</span><br><span class="line">...</span><br><span class="line">fffff803`16425cc4 e8a7010000      call nt!PspReferenceCidTableEntry (fffff803`16425e70)</span><br></pre></td></tr></table></figure><blockquote><p>小技巧：<code>48 8B 05</code> 在函数里可能命中多处，先通过 <code>call</code> 跳板锁定 <code>PspReferenceCidTableEntry</code>，再在其内部搜 <code>mov</code>，命中率更高——把调用链当路标。</p></blockquote><p>核心实现：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">PVOID <span class="title function_">GetPspCidTable</span><span class="params">()</span> &#123;</span><br><span class="line">    PVOID PsLookupProcessByProcessIdAddr = (PVOID)PsLookupProcessByProcessId;</span><br><span class="line">    PUCHAR StartSearchAddress = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 第一步：在 PsLookupProcessByProcessId 内搜索 call 指令（0xE8）</span></span><br><span class="line">    UCHAR pSpecialCode1[<span class="number">1</span>] = &#123; <span class="number">0xE8</span> &#125;;</span><br><span class="line">    PVOID result = SearchSpecialCode(PsLookupProcessByProcessIdAddr, <span class="number">0x100</span>, pSpecialCode1, <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">if</span> (result != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="comment">// call 相对偏移：目标地址 = 指令地址 + 5 + disp32</span></span><br><span class="line">        LONG offset = *(PLONG)((PUCHAR)result + <span class="number">1</span>);</span><br><span class="line">        StartSearchAddress = (PUCHAR)((PUCHAR)result + <span class="number">5</span> + offset);  <span class="comment">// 得到 PspReferenceCidTableEntry</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        StartSearchAddress = (PUCHAR)PsLookupProcessByProcessIdAddr;  <span class="comment">// 降级：直接从头搜索</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 第二步：在 PspReferenceCidTableEntry 内搜索 mov rax, [rip+disp32]（48 8B 05）</span></span><br><span class="line">    UCHAR pSpecialCode2[<span class="number">3</span>] = &#123; <span class="number">0x48</span>, <span class="number">0x8B</span>, <span class="number">0x05</span> &#125;;</span><br><span class="line">    result = SearchSpecialCode(StartSearchAddress, <span class="number">0x100</span>, pSpecialCode2, <span class="number">3</span>);</span><br><span class="line">    <span class="keyword">if</span> (<span class="literal">NULL</span> == result) <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    LONG offset = *(PLONG)((PUCHAR)result + <span class="number">3</span>);</span><br><span class="line">    <span class="keyword">return</span> (PVOID)((PUCHAR)result + <span class="number">7</span> + offset);  <span class="comment">// mov rax,[rip+disp] 指令长 7 字节</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>（关键逻辑如上，完整工程代码参照 LyShark实现 补齐。）</p><h2 id="4-遍历-PspCidTable"><a href="#4-遍历-PspCidTable" class="headerlink" title="4. 遍历 PspCidTable"></a>4. 遍历 PspCidTable</h2><p>拿到 PspCidTable 地址后，按 <code>TableCode</code> 的级数逐级遍历。整体流程：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">PspCidTable</span><br><span class="line">  ↓ 解析 TableCode（级数 + 基址）</span><br><span class="line">遍历有效 Entry</span><br><span class="line">  ↓ 解码 Entry 得到对象体地址</span><br><span class="line">ObGetObjectType 判断是否为进程对象</span><br><span class="line">  ↓</span><br><span class="line">handle 值即 PID，收进结果数组</span><br><span class="line">  ↓</span><br><span class="line">生成第二份进程列表 B</span><br></pre></td></tr></table></figure><h3 id="4-1-解析-TableCode"><a href="#4-1-解析-TableCode" class="headerlink" title="4.1 解析 TableCode"></a>4.1 解析 TableCode</h3><p>注意 <code>PspCidTable</code> 变量里存的是 <code>HANDLE_TABLE*</code>，取 TableCode 要<strong>两级解引用</strong>：先读变量得表地址，再偏移 <code>+8</code> 读 TableCode 字段。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">ULONG64 ul_tableCode = *(PULONG64)(((ULONG64)*(PULONG64)TableBase) + <span class="number">8</span>);</span><br><span class="line">INT i_low2 = ul_tableCode &amp; <span class="number">3</span>;   <span class="comment">// 表级数</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (i_low2 == <span class="number">0</span>)      parse_table_1(ul_tableCode &amp; ~<span class="number">3</span>, <span class="number">0</span>, <span class="number">0</span>, Callback, Context); <span class="comment">// 单级</span></span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (i_low2 == <span class="number">1</span>) parse_table_2(ul_tableCode &amp; ~<span class="number">3</span>, <span class="number">0</span>, Callback, Context);    <span class="comment">// 二级</span></span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (i_low2 == <span class="number">2</span>) parse_table_3(ul_tableCode &amp; ~<span class="number">3</span>, Callback, Context);       <span class="comment">// 三级</span></span><br></pre></td></tr></table></figure><h3 id="4-2-逐级遍历"><a href="#4-2-逐级遍历" class="headerlink" title="4.2 逐级遍历"></a>4.2 逐级遍历</h3><p>三级表：在当前观察到的 x64 版本中，每层通常是 512 项的指针数组（每项 8 字节），逐级取指针，最底层是 Entry 数组（每项 16 字节）。这里的层级、数量和字段布局不是稳定的公开接口，必须按目标系统版本验证。<code>MmIsAddressValid</code> 只能作为有限的预检，不能替代对象生命周期管理或并发同步。</p><h3 id="4-3-Entry-解码（当前版本假设）"><a href="#4-3-Entry-解码（当前版本假设）" class="headerlink" title="4.3 Entry 解码（当前版本假设）"></a>4.3 Entry 解码（当前版本假设）</h3><p>在当前参考实现假设下，PspCidTable 使用“锁定句柄”格式，Entry 的低 16 位是引用计数或属性位，对象体地址需要这样解码（右移 16 位并按 16 字节对齐）：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ULONG64 ul_decode = (LONG64)ul_recode &gt;&gt; <span class="number">0x10</span>;</span><br><span class="line">ul_decode &amp;= <span class="number">0xfffffffffffffff0</span>;</span><br></pre></td></tr></table></figure><h3 id="4-4-Client-ID-与-PID-的对应关系"><a href="#4-4-Client-ID-与-PID-的对应关系" class="headerlink" title="4.4 Client ID 与 PID 的对应关系"></a>4.4 Client ID 与 PID 的对应关系</h3><p>在 CID 表语义下，条目的 Client ID 数值可以对应 PID 或 TID；只有通过对象类型判断为进程对象后，才能将其作为 PID 收集。当前参考实现假设每个 Entry 承载 4 个 PID 槽位，handle 编码为：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">handle = i * 4 + 1024 * index1 + 512 * index2 * 1024</span><br></pre></td></tr></table></figure><p><strong>PID 必然是 4 的倍数</strong>——这也解释了之前暴力扫描为什么能以 4 为步长对齐。进程对象判断用对象管理器 API，配合异常保护：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> BOOLEAN <span class="title function_">ProcessEnumCallback</span><span class="params">(HANDLEINFO* pHandleInfo, PVOID Context)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!pHandleInfo-&gt;Object || !MmIsAddressValid(pHandleInfo-&gt;Object))</span><br><span class="line">        <span class="keyword">return</span> TRUE;  <span class="comment">// 无效 Entry 跳过</span></span><br><span class="line">    _try &#123;</span><br><span class="line">        <span class="keyword">if</span> (ObGetObjectType(pHandleInfo-&gt;Object) == *PsProcessType) &#123;</span><br><span class="line">            <span class="comment">// handle 直接作为 PID 存入数组</span></span><br><span class="line">            EnumContext-&gt;ProcessArray[EnumContext-&gt;ProcessCount++] = pHandleInfo-&gt;Handle;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    _except(EXCEPTION_EXECUTE_HANDLER) &#123;</span><br><span class="line">        <span class="comment">// 对象可能已被释放，异常时跳过而非蓝屏</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> TRUE;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="5-与现有方案的关系和验证意义"><a href="#5-与现有方案的关系和验证意义" class="headerlink" title="5. 与现有方案的关系和验证意义"></a>5. 与现有方案的关系和验证意义</h2><p>PID 暴力枚举的表面形式是遍历 PID 数值，但每次查询仍然依赖 <code>PsLookupProcessByProcessId</code> 访问内核维护的 CID 映射。因此，它与直接遍历 <code>PspCidTable</code> 并不是两个独立的数据源。</p><p>两种方案的主要区别在于访问路径：</p><ul><li><strong>间接 CID 查询</strong>：遍历候选 PID，通过公开 API 查询对应的进程对象；</li><li><strong>直接 CID 表遍历</strong>：定位并解析私有的 <code>PspCidTable</code>，再根据对象类型筛选进程对象。</li></ul><h2 id="6-实验设计"><a href="#6-实验设计" class="headerlink" title="6. 实验设计"></a>6. 实验设计</h2><table><thead><tr><th>状态</th><th>间接 CID 查询</th><th>直接 CID 表遍历</th><th>预期结论</th></tr></thead><tbody><tr><td>正常进程</td><td>✓</td><td>✓</td><td>正常</td></tr><tr><td>ActiveProcessLinks 隐藏</td><td>✗</td><td>✓</td><td>检出</td></tr><tr><td>PspCidTable 隐藏</td><td>✓</td><td>✗</td><td>检出</td></tr><tr><td>两边同时隐藏</td><td>✗</td><td>✗</td><td>多源方案仍有盲区</td></tr></tbody></table><p>从最后一行可以得出结论：<strong>检测机制不是”万能探测器”，而是攻击面与数据源之间的博弈</strong>——每多一个可信数据源，攻击者就得多篡改一个地方，但盲区始终存在。</p><h2 id="7-遇到的问题"><a href="#7-遇到的问题" class="headerlink" title="7. 遇到的问题"></a>7. 遇到的问题</h2><ol><li><strong>两级解引用</strong>：一开始只解了一次引用，把 <code>PspCidTable</code> 变量地址当成了表地址，遍历全乱；后来意识到它是 <code>HANDLE_TABLE*</code>。</li><li><strong>遍历次数困惑</strong>：参考实现里一级表循环写的是 <code>for (i &lt; 512) // 256?</code>，作者自己也标了疑惑——表大小 4K &#x2F; 每项 16B 应该是 256 项，实际循环却写 512 次，这部分涉及每个 Entry 承载 4 个 handle 的映射，还需要再验证。</li><li><strong>并发修改</strong>：遍历过程中 <code>TableCode</code> 和各级表可能被句柄增删或扩容修改。未经确认不应直接依赖内部锁；当前方案只能接受轻微不一致，后续需要评估版本相关的快照、重试或结果去重策略。</li></ol><h2 id="8-展望"><a href="#8-展望" class="headerlink" title="8. 展望"></a>8. 展望</h2><ul><li>在 Win10、Win11 等目标版本上分别确认 <code>HANDLE_TABLE</code> 布局、<code>TableCode</code> 层级、Entry 数量和对象指针编码；</li><li>为直接遍历逻辑增加版本识别、特征匹配失败处理、遍历边界检查和结果去重，避免把私有结构当作稳定接口；</li><li>通过正常进程创建和退出、PID 较大进程、并发表扩容等场景验证遍历稳定性；</li></ul><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li>Windows Internals, 7th Edition（PspCidTable &#x2F; HANDLE_TABLE 相关章节）</li><li>ReactOS 源码（HANDLE_TABLE &#x2F; PspCidTable 的开放实现参考）</li><li>LyShark 公开示例：PspCidTable 定位与三级表遍历参考实现（<a href="https://www.lyshark.com/post/93beefb4.html%EF%BC%89">https://www.lyshark.com/post/93beefb4.html）</a></li><li>本系列前篇：SnowSword–进程隐藏检测实现（PID 暴力扫描方案）</li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/15/%E7%AC%AC%E4%B8%83%E7%AF%87%E6%97%A5%E6%8A%A5_PspCidTable%E8%BF%9B%E7%A8%8B%E6%9E%9A%E4%B8%BE%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</id>
    <link href="https://snow-blog.pages.dev/2026/08/15/%E7%AC%AC%E4%B8%83%E7%AF%87%E6%97%A5%E6%8A%A5_PspCidTable%E8%BF%9B%E7%A8%8B%E6%9E%9A%E4%B8%BE%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/"/>
    <published>2026-08-15T03:10:00.000Z</published>
    <summary>学习如何在 Windows 内核中定位并遍历 PspCidTable（进程/线程全局句柄表），作为 SnowSword 第二种进程枚举视角。通过 PsLookupProcessByProcessId 的 call 跳板逆向出 PspCidTable 地址，再按 TableCode 级数逐级遍历三级表，用 ObGetObjectType 过滤进程对象，handle 值即 PID。</summary>
    <title>PspCidTable 进程枚举学习记录 —— 从 API 逆向到内核对象表遍历</title>
    <updated>2026-09-21T04:20:05.619Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Malware Analysis" scheme="https://snow-blog.pages.dev/categories/Malware-Analysis/"/>
    <category term="Windows" scheme="https://snow-blog.pages.dev/tags/Windows/"/>
    <category term="Reserve" scheme="https://snow-blog.pages.dev/tags/Reserve/"/>
    <category term="恶意样本" scheme="https://snow-blog.pages.dev/tags/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC/"/>
    <category term="HIPS" scheme="https://snow-blog.pages.dev/tags/HIPS/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <content>
      <![CDATA[<p>在上一篇文章中，我尝试分析了一个 Windows 恶意样本。分析过程中发现，攻击者并不会依赖某一个“危险 API”完成攻击，而是将多个正常功能组合起来形成完整攻击链，例如进程创建、远程内存分配、代码写入以及线程执行等行为共同构成进程注入。</p><p>这让我重新思考之前 SnowWall 中 HIPS 行为检测的设计：安全检测的重点并不是简单判断某个 API 是否危险，而是分析多个事件之间的关联关系。</p><h3 id="1-攻击链"><a href="#1-攻击链" class="headerlink" title="1. 攻击链"></a>1. 攻击链</h3><p><img src="/images/%E6%A0%B7%E6%9C%AC%E6%80%9D%E8%80%83/1.png" alt="进程注入流程图"></p><h3 id="2-单-API-检测不可靠的原因分析"><a href="#2-单-API-检测不可靠的原因分析" class="headerlink" title="2. 单 API 检测不可靠的原因分析"></a>2. 单 API 检测不可靠的原因分析</h3><p>错误设计: 仅拦截 CreateRemoteThread<br>问题: </p><ul><li>误报高：调试器、合法软件更新机制都会调用</li><li>绕过简单：改用 NtQueueApcThread &#x2F; NtSetContextThread 等替代执行方式</li><li>漏报高：攻击者可用 Process Hollowing（无需远程线程）</li></ul><p>改进设计: 从”单点拦截”转向”行为链评分”</p><h3 id="3-SnowWall-设计思考"><a href="#3-SnowWall-设计思考" class="headerlink" title="3. SnowWall 设计思考"></a>3. SnowWall 设计思考</h3><p>通过这次分析，结合SnowWall的原始设计方向,即它刚开始只拦截CreateRemoteThread.<br>经过改进后SnowWall使用了”事件组合”来更准确地判定恶意行为,例如将ProcessCreate(CREATE_SUSPENDED)+MemoryAlloc+MemoryWrite这个事件组合判定为进程挂起注入行为</p><h4 id="3-1-SnowWall-当前行为规则模型-简化"><a href="#3-1-SnowWall-当前行为规则模型-简化" class="headerlink" title="3.1 SnowWall 当前行为规则模型(简化)"></a>3.1 SnowWall 当前行为规则模型(简化)</h4><p>SnowWall 采用三层检测架构：<br>┌─────────────────────────────────────────────────────────────┐<br>│  第一层：硬拦截（极高置信度，直接阻断）                        │<br>│  • 跨进程写入 PE 头（MZ&#x2F;Shellcode）                           │<br>│  • Unmap→Alloc→Write→Protect 完整链                         │<br>│  • 挂起进程恢复线程（Hollowing 最后一步）                      │<br>├─────────────────────────────────────────────────────────────┤<br>│  第二层：时序可疑度评分（核心）                                │<br>│  • 按 PID+Cookie 隔离进程档案，防 PID 复用                    │<br>│  • 指数衰减：30s 半衰期，污点进程 60s                          │<br>│  • 8 标签历史环 → 检测组合序列 → 动态加分                      │<br>│  • 污点标记：触发高危行为后永久降阈值（80→64）                 │<br>├─────────────────────────────────────────────────────────────┤<br>│  第三层：调试上下文豁免                                       │<br>│  • 信任分级：微软签名 &gt; Program Files &gt; 普通                   │<br>│  • 红线：PE 头写入 &#x2F; RWX 分配 &#x2F; 主模块 Unmap 绝不豁免          │<br>└─────────────────────────────────────────────────────────────┘<br>标签泛化将具体 API 转化为抽象行为（如 NtAllocateVirtualMemory(Cross, RWX) → CrossExecAlloc），上层规则与具体 API 解耦，对抗替换绕过。<br>组合检测示例：</p><table><thead><tr><th>攻击类型</th><th>标签序列</th><th>加分</th><th>阈值</th></tr></thead><tbody><tr><td>远程线程注入</td><td>CrossAlloc → CrossWrite → CrossThreadCreate</td><td>+30</td><td>80</td></tr><tr><td>Process Hollowing</td><td>Unmap → Alloc → Write → Protect_Exec</td><td>+35</td><td>80</td></tr><tr><td>保护属性拆分</td><td>CrossAlloc → CrossWrite → CrossProtect_Exec</td><td>+25</td><td>80</td></tr></tbody></table><h3 id="4-展望"><a href="#4-展望" class="headerlink" title="4. 展望"></a>4. 展望</h3><p>当前方案仍然依赖用户态行为采集，而攻击者可以通过 Native API、直接系统调用等方式绕过部分用户态监控。因此下一步计划研究 Windows 内核层事件采集方式，并探索如何结合驱动实现更可靠的行为监控。</p>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/09/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%20-%20%E7%BB%AD/</id>
    <link href="https://snow-blog.pages.dev/2026/08/09/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%20-%20%E7%BB%AD/"/>
    <published>2026-08-09T06:00:00.000Z</published>
    <summary>承接上一篇样本分析，讨论单点拦截某个 API 为什么不可靠，并整理 SnowWall 的三层检测架构（硬拦截、时序可疑度评分、调试上下文豁免），以及标签泛化如何让上层规则与具体 API 解耦。</summary>
    <title>从恶意样本分析到HIPS行为检测：一次Windows安全检测思路的实践</title>
    <updated>2026-09-21T04:20:05.613Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="Malware Analysis" scheme="https://snow-blog.pages.dev/categories/Malware-Analysis/"/>
    <category term="Windows" scheme="https://snow-blog.pages.dev/tags/Windows/"/>
    <category term="Reserve" scheme="https://snow-blog.pages.dev/tags/Reserve/"/>
    <category term="恶意样本" scheme="https://snow-blog.pages.dev/tags/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC/"/>
    <category term="HIPS" scheme="https://snow-blog.pages.dev/tags/HIPS/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <content>
      <![CDATA[<p>今天尝试对一个 Windows 恶意样本进行静态逆向分析，主要学习 PE 文件结构、API 调用分析以及恶意程序常见行为模式。</p><h3 id="1-分析过程"><a href="#1-分析过程" class="headerlink" title="1. 分析过程"></a>1. 分析过程</h3><p>今天尝试第一次完整分析一个 Windows 恶意样本。最开始通过 PE 信息确认样本结构，随后使用 IDA 跟踪关键函数。在分析过程中发现，样本并没有直接调用敏感 API，而是通过动态解析隐藏真实调用，这也是我第一次比较直观地接触到恶意程序对抗静态分析的方法。随后结合 IDA 对关键函数进行跟踪，发现程序并没有直接调用敏感 API，而是通过动态解析方式获取函数地址，增加静态分析难度。<br><strong>报告依据</strong></p><ul><li>样本未直接调用敏感 API，而是在 <code>.data</code> 段存放异或加密的函数指针表（密钥 <code>qword_408000 = 0x6BF72FD7E5809F41</code>），运行时通过 <code>qword_408000 ^ qword_408xxx</code> 还原真实地址，如 <code>0x40100D</code>（WinMain_DecryptCall_Dispatcher）解密出 <code>0x401634</code> 后间接调用。</li><li>示例：<code>sub_401B45</code> 中的 <code>LoadLibraryW(&quot;ADVAPI32.dll&quot;)</code>、<code>OpenSCManagerW</code> 等调用全部经此机制动态解析，规避静态分析。</li></ul><p><img src="/images/%E6%A0%B7%E6%9C%AC%E5%88%86%E6%9E%90/01_dynamic_api_resolve.jpg" alt="截图：0x40100D 处异或解密调用模式"></p><h3 id="2-关键发现"><a href="#2-关键发现" class="headerlink" title="2. 关键发现"></a>2. 关键发现</h3><h4 id="2-1-进程注入分析"><a href="#2-1-进程注入分析" class="headerlink" title="2.1 进程注入分析"></a>2.1 进程注入分析</h4><p>分析过程中发现样本通过挂起创建进程、远程内存分配以及写入 payload 的方式实现代码注入，涉及 CreateProcessW、VirtualAllocEx、WriteProcessMemory 等 API。<br><strong>报告依据</strong></p><ul><li>核心注入函数 <code>sub_4046D1</code>（0x4046D1）实现进程镂空：先 <code>CreateProcessW(自身路径, ..., CREATE_SUSPENDED=0x8000000, ...)</code> 挂起创建进程，再 <code>VirtualAllocEx(hProc, 0, 72, 0x3000, PAGE_EXECUTE_READWRITE=4)</code> 远程分配可执行内存，最后 <code>WriteProcessMemory(hProc, addr, payload, 72, 0)</code> 写入 payload。</li><li>备用注入路径：<code>sub_403851</code> 使用 <code>NtAllocateVirtualMemory</code> &#x2F; <code>NtWriteVirtualMemory</code>（ntdll 原生 API）；<code>sub_403A37</code> 使用 <code>PostQueuedCompletionStatus</code> &#x2F; <code>ZwSetIoCompletion</code> 执行。</li></ul><p><img src="/images/%E6%A0%B7%E6%9C%AC%E5%88%86%E6%9E%90/02_process_injection.jpg" alt="截图：sub_4046D1 反编译中的异或动态调用"></p><p>这让我认识到，HIPS 不应该只关注单个 API 调用，而应该关注 API 调用组合形成的行为链，例如“创建挂起进程 + 远程写入 + 执行权限内存”。</p><h4 id="2-2-反静态调试分析"><a href="#2-2-反静态调试分析" class="headerlink" title="2.2 反静态调试分析"></a>2.2 反静态调试分析</h4><p>样本存在 API 动态解析、字符串 XOR 加密以及跳板函数等混淆方式，使传统静态分析工具难以直接获得真实行为。<br><strong>报告依据</strong></p><ul><li>API 动态解析：函数指针表存于 <code>.data</code> 段，运行时经 <code>qword_408000 ^ qword_408xxx</code> 异或还原（密钥 <code>0x6BF72FD7E5809F41</code>）。</li><li>字符串 XOR 加密：宽字符 <code>char = 低字节 ^ 高字节</code>（如 <code>0x408008</code> → <code>ADVAPI32.dll</code>、<code>0x4083BC</code> → <code>bdservicehost.exe</code>）；ANSI 单字节 XOR 尾字节（如 <code>0x408082</code> → <code>OpenSCManagerW</code>）。</li><li>跳板函数混淆：大量 <code>jmp short</code> 跳板（如 <code>0x401B38 → sub_401B45</code>、<code>0x401FD3 → sub_401FE0</code>、<code>0x40284C → sub_402859</code>），真实函数地址隐藏于异或表之后。</li></ul><p><img src="/images/%E6%A0%B7%E6%9C%AC%E5%88%86%E6%9E%90/03_string_decrypt.jpg" alt="截图：运行脚本后 IDA Python 控制台的解密输出"><br><img src="/images/%E6%A0%B7%E6%9C%AC%E5%88%86%E6%9E%90/04_thunk_jump.jpg" alt="截图：跳板函数示例"></p><p>这让我进一步理解为什么安全工具需要结合静态分析和运行时行为监控。</p><h4 id="2-3-对-HIPS-设计的启发"><a href="#2-3-对-HIPS-设计的启发" class="headerlink" title="2.3 对 HIPS 设计的启发"></a>2.3 对 HIPS 设计的启发</h4><p>此次分析也反向验证了之前设计 HIPS 行为规则的一些思路。例如恶意程序常通过多个正常 API 组合完成攻击，因此检测重点应该从单 API 判断转向行为链分析。<br>CREATE_SUSPENDED + VirtualAllocEx + WriteProcessMemory + CreateRemoteThread</p><h3 id="3-今日总结"><a href="#3-今日总结" class="headerlink" title="3. 今日总结"></a>3. 今日总结</h3><p>通过此次分析，我开始从攻击者视角理解 Windows 内部机制。之前开发 ARK&#x2F;HIPS 时更多关注“如何监控系统”，而逆向分析让我认识到“攻击者如何利用系统机制”同样重要。后续计划继续学习恶意代码分析，并将相关行为抽象为安全检测规则。</p>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/08/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/</id>
    <link href="https://snow-blog.pages.dev/2026/08/08/%E6%81%B6%E6%84%8F%E6%A0%B7%E6%9C%AC%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/"/>
    <published>2026-08-08T12:51:00.000Z</published>
    <summary>第一次完整分析 Windows 恶意样本的记录：从 .data 段异或加密的函数指针表还原动态 API 解析机制，到 CreateProcessW 挂起创建 + VirtualAllocEx + WriteProcessMemory 的进程镂空注入，并据此得出 HIPS 应从单 API 判断转向行为链分析的结论。</summary>
    <title>恶意样本静态逆向实践：XOR 函数指针表与进程镂空注入</title>
    <updated>2026-09-21T04:20:05.614Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="SnowSword Development" scheme="https://snow-blog.pages.dev/categories/SnowSword-Development/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="Anti-Rootkit" scheme="https://snow-blog.pages.dev/tags/Anti-Rootkit/"/>
    <category term="Process" scheme="https://snow-blog.pages.dev/tags/Process/"/>
    <category term="Windows Kernel" scheme="https://snow-blog.pages.dev/tags/Windows-Kernel/"/>
    <category term="SnowSword" scheme="https://snow-blog.pages.dev/tags/SnowSword/"/>
    <content>
      <![CDATA[<h5 id="项目地址：https-gitee-com-le-ziyi-SnowSword"><a href="#项目地址：https-gitee-com-le-ziyi-SnowSword" class="headerlink" title="项目地址：https://gitee.com/le-ziyi/SnowSword"></a>项目地址：<a href="https://gitee.com/le-ziyi/SnowSword">https://gitee.com/le-ziyi/SnowSword</a></h5><h3 id="1-为什么需要检测隐藏进程"><a href="#1-为什么需要检测隐藏进程" class="headerlink" title="1. 为什么需要检测隐藏进程"></a>1. 为什么需要检测隐藏进程</h3><p>在开发 SnowSword 之前，我曾使用过一些 ARK 类工具分析 Windows 系统。但在实际测试过程中发现，不同工具对于进程枚举结果可能存在差异。因此，我希望实现一种多视角交叉验证的方法，而不是依赖单一 API。</p><p>众所周知，部分恶意程序会通过 Hook R3 API（Process32Next、NtQuerySystemInformation）、Hook R0 API（ZwQuerySystemInformation）或篡改 R0 内核结构（EPROCESS 摘链、Hook ZwQuerySystemInformation）来隐藏进程，避免被用户通过系统自带的任务管理器等普通安全工具发现其活动。但正所谓”天网恢恢，疏而不漏”，我们可以使用一种交叉对比进程枚举结果的方法找出恶意程序的隐藏进程。</p><h4 id="1-1-为什么需要多视角枚举？"><a href="#1-1-为什么需要多视角枚举？" class="headerlink" title="1.1 为什么需要多视角枚举？"></a>1.1 为什么需要多视角枚举？</h4><p>Windows中不存在一个绝对可信的进程观察视角。</p><p>用户态工具、对象管理器、内核进程管理机制分别维护着不同的信息来源。<br>如果攻击者只修改其中一种视角，例如Hook某个用户态接口，<br>其他来源仍可能暴露异常。</p><p>因此，SnowSword的设计思路不是寻找一个万能的进程枚举接口，<br>而是从多个角度获取进程信息，并通过结果差异发现异常。</p><h3 id="2-不同视角下的进程枚举机制"><a href="#2-不同视角下的进程枚举机制" class="headerlink" title="2. 不同视角下的进程枚举机制"></a>2. 不同视角下的进程枚举机制</h3><h4 id="2-1-用户态-API-枚举"><a href="#2-1-用户态-API-枚举" class="headerlink" title="2.1 用户态 API 枚举"></a>2.1 用户态 API 枚举</h4><ul><li><p><strong>Process32First + Process32Next</strong>：Windows 用户态最常见的进程枚举接口。但是它依赖用户态 API 调用链，如果攻击者 IAT Hook &#x2F; Inline Hook 它或者更底层的数据来源，返回结果可能已经不可信。因此 SnowSword 中并不将其作为最终判断依据，而是作为一个”普通用户看到的世界”。</p></li><li><p><strong>NtQuerySystemInformation(SystemProcessInformation, …)</strong>：Native API（由于不存在于导入库中，需要自行通过 <code>GetModuleHandle</code> + <code>GetProcAddress</code> 动态调用），可以获取更多进程信息，但由于它已十分出名，所以通常也会被列入 hook 名单。</p></li></ul><h4 id="2-2-内核态-API-枚举"><a href="#2-2-内核态-API-枚举" class="headerlink" title="2.2 内核态 API 枚举"></a>2.2 内核态 API 枚举</h4><ul><li><strong>ZwQuerySystemInformation(SystemProcessInformation, …)</strong>：内核最常见的进程枚举手段，获取的进程信息同 R3 的 <code>NtQuerySystemInformation(SystemProcessInformation, ...)</code>，但相较 R3 更可信。</li></ul><h3 id="3-SnowSword-实现方案"><a href="#3-SnowSword-实现方案" class="headerlink" title="3. SnowSword 实现方案"></a>3. SnowSword 实现方案</h3><h4 id="3-1-用户态获取进程列表"><a href="#3-1-用户态获取进程列表" class="headerlink" title="3.1 用户态获取进程列表"></a>3.1 用户态获取进程列表</h4><ol><li><p>调用 <code>Process32First</code> + <code>Process32Next</code> 获取进程列表 a，作为进程列表基线用于交叉检测隐藏进程。</p></li><li><p>除了传统进程枚举 API 外，SnowSword 还尝试从系统句柄角度观察进程。Windows 中的进程对象通常会对应一个 Process 类型句柄，因此可以通过 <code>SystemHandleInformation</code> 获取全局句柄表，再筛选其中的 Process 和 Thread 类型。通过句柄反向获得 PID，形成进程列表 b。最后去重。</p></li></ol><h4 id="3-2-驱动获取进程列表"><a href="#3-2-驱动获取进程列表" class="headerlink" title="3.2 驱动获取进程列表"></a>3.2 驱动获取进程列表</h4><p>Windows 内核分配 PID 时采用 4 字节对齐的增量分配策略——PID 的高 30 位用于索引进程对象数组，低 2 位固定为 0。因此合法 PID 必然是 4 的倍数，以 4 为步长暴力枚举可以跳过 75% 的无效值。</p><p>具体实现：以 4 为步长暴力枚举 <code>0 ~ 0x20000</code>（131072）的 PID，尝试调用 <code>PsLookupProcessByProcessId</code> 获取进程对象指针，成功且 <code>PsGetProcessExitStatus</code> 返回值等于 <code>STATUS_PENDING</code>（为了排除部分僵尸进程）则将 PID 插入进程列表 c。</p><p>该方法本质上利用了系统 PID 管理机制进行间接验证，与直接遍历 <code>PspCidTable</code> 相比实现简单，但效率和兼容性存在限制。</p><p>最初考虑过直接遍历 EPROCESS 链表，但是该方案存在两个问题：</p><ol><li>不同 Windows 版本结构变化较大。</li><li>如果攻击者采用 DKOM 隐藏，同样可能影响该链表。</li></ol><p>因此 SnowSword 选择通过 PID 反查方式，利用系统自身 PID 管理机制获取进程对象。虽然扫描范围有限，但不依赖固定结构偏移。</p><h4 id="3-3-用户态与内核态结果比较"><a href="#3-3-用户态与内核态结果比较" class="headerlink" title="3.3 用户态与内核态结果比较"></a>3.3 用户态与内核态结果比较</h4><p>比较进程列表 a、b、c，在 UI 中标记出现在列表 b&#x2F;c 但未出现在列表 a 的隐藏进程。</p><h4 id="3-4-隐藏进程检测实验"><a href="#3-4-隐藏进程检测实验" class="headerlink" title="3.4 隐藏进程检测实验"></a>3.4 隐藏进程检测实验</h4><p><img src="/images/SnowSword-%E9%9A%90%E8%97%8F%E8%BF%9B%E7%A8%8B%E6%A3%80%E6%B5%8B/Screenshot1.png" alt="隐藏进程检测实验截图"></p><blockquote><p>截图说明：</p><ul><li><strong>SnowSword 进程列表</strong>（上方）：可以看到 <code>TestAPI.exe</code>（PID 9812，黄色高亮），以及多个 <code>(Exited)</code> 进程（已退出但内核对象尚未销毁的僵尸进程）。</li><li><strong>任务管理器</strong>（下方）：<strong>无法看到</strong> <code>TestAPI.exe</code>，因为已被 InlineHook 摘除。</li><li><strong>TestAPI 窗口</strong>（右下）：提供”Hook”按钮，用于触发 DLL 注入。</li></ul></blockquote><p>我编写了一个测试程序 <code>TestAPI.exe</code>（PID 9812），并开发了一个 <code>InlineHook.dll</code>。通过 TestAPI，将 <code>InlineHook.dll</code> 分别注入到**任务管理器（Taskmgr.exe）**和 <strong>SnowSword</strong> 两个进程中。</p><p><code>InlineHook.dll</code> 的核心逻辑是 Hook <code>ntdll.dll</code> 中的 <code>NtQuerySystemInformation</code> 函数，当查询 <code>SystemProcessInformation</code>（进程列表）时，将 <code>TestAPI.exe</code> 从返回的链表数据中”摘除”。核心逻辑如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 注意：本代码假设被隐藏进程不会出现在链表的第一个节点</span></span><br><span class="line"><span class="comment">// （SystemProcessInformation 的头节点通常是 System Idle Process）</span></span><br><span class="line"><span class="comment">// 生产环境需要处理头节点被隐藏的情况</span></span><br><span class="line"></span><br><span class="line">NTSTATUS NTAPI <span class="title function_">Fake_NtQuerySystemInformation</span><span class="params">(</span></span><br><span class="line"><span class="params">    SYSTEM_INFORMATION_CLASS SystemInformationClass,</span></span><br><span class="line"><span class="params">    PVOID SystemInformation,</span></span><br><span class="line"><span class="params">    ULONG SystemInformationLength,</span></span><br><span class="line"><span class="params">    PULONG ReturnLength</span></span><br><span class="line"><span class="params">)</span></span><br><span class="line">&#123;</span><br><span class="line">    NTSTATUS Status = Old_NtQuerySystemInformation(</span><br><span class="line">        SystemInformationClass,</span><br><span class="line">        SystemInformation,</span><br><span class="line">        SystemInformationLength,</span><br><span class="line">        ReturnLength</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!NT_SUCCESS(Status) || SystemInformationClass != SystemProcessInformation)</span><br><span class="line">        <span class="keyword">return</span> Status;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 双指针法：pNextOffset 指向前一个节点的 NextEntryOffset 字段（或头指针）</span></span><br><span class="line">    PULONG pNextOffset = (PULONG)&amp;((PSYSTEM_PROCESS_INFORMATION)SystemInformation)-&gt;NextEntryOffset;</span><br><span class="line">    PSYSTEM_PROCESS_INFORMATION Current = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (Current)</span><br><span class="line">    &#123;</span><br><span class="line">        BOOL Hide = (Current-&gt;ImageName.Buffer &amp;&amp;</span><br><span class="line">            _wcsicmp(Current-&gt;ImageName.Buffer, <span class="string">L&quot;TestAPI.exe&quot;</span>) == <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (Hide)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">if</span> (Current-&gt;NextEntryOffset == <span class="number">0</span>)</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="comment">// 尾节点：将前一个节点的偏移设为 0</span></span><br><span class="line">                *pNextOffset = <span class="number">0</span>;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">            &#123;</span><br><span class="line">                <span class="comment">// 中间节点：跳过当前节点</span></span><br><span class="line">                *pNextOffset += Current-&gt;NextEntryOffset;</span><br><span class="line">                Current = (PSYSTEM_PROCESS_INFORMATION)((PUCHAR)Current + Current-&gt;NextEntryOffset);</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 推进</span></span><br><span class="line">        <span class="keyword">if</span> (Current-&gt;NextEntryOffset == <span class="number">0</span>)</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        pNextOffset = &amp;Current-&gt;NextEntryOffset;</span><br><span class="line">        Current = (PSYSTEM_PROCESS_INFORMATION)((PUCHAR)Current + Current-&gt;NextEntryOffset);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Status;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>实验结果：</strong></p><table><thead><tr><th>工具</th><th>是否看到 TestAPI.exe</th><th>原因</th></tr></thead><tbody><tr><td>被注入的任务管理器</td><td>❌ 看不到</td><td><code>NtQuerySystemInformation</code> 被 Hook，链表被篡改</td></tr><tr><td>被注入的 SnowSword</td><td>✅ 能看到</td><td>使用句柄枚举 + 内核驱动反查，不依赖被 Hook 的 R3 API</td></tr></tbody></table><p>这就验证了<strong>单一 R3 API Hook 无法对抗多视角交叉检测</strong>的观点。</p><p>在驱动层枚举时，我们通过 <code>PsGetProcessExitStatus</code> 过滤掉 <code>STATUS_PENDING</code> 以外的进程，避免将已退出进程误判为隐藏进程。而用户态的 <code>Process32Next</code> 通常不会返回这些已退出进程，这也解释了为什么 SnowSword 的进程列表比任务管理器”更长”——它包含了更多内核视角的信息。</p><h3 id="4-开发过程中遇到的问题"><a href="#4-开发过程中遇到的问题" class="headerlink" title="4. 开发过程中遇到的问题"></a>4. 开发过程中遇到的问题</h3><h4 id="4-1-比较结果"><a href="#4-1-比较结果" class="headerlink" title="4.1 比较结果"></a>4.1 比较结果</h4><p>一开始我想当然以为直接取列表 a 和列表 b&#x2F;c 的差集即可，但 UI 上会出现重复显示的隐藏进程，排查后才发现是因为一个进程可能：被多个进程打开句柄，或是拥有多个线程句柄。所以 Handle-PID 不是简单的一对一关系，需要使用 HashSet 保存 PID。</p><h4 id="4-2-僵尸进程判断问题"><a href="#4-2-僵尸进程判断问题" class="headerlink" title="4.2 僵尸进程判断问题"></a>4.2 僵尸进程判断问题</h4><p>测试过程中发现，部分已经退出的进程对象仍然可能短时间存在。因此不能简单认为 <code>PsLookupProcessByProcessId</code> 成功即可，需要结合 <code>PsGetProcessExitStatus</code> 进一步判断。</p><h3 id="5-后续优化方向"><a href="#5-后续优化方向" class="headerlink" title="5. 后续优化方向"></a>5. 后续优化方向</h3><p>目前 SnowSword 的隐藏进程检测仍存在限制：</p><ol><li>PID 扫描范围固定</li><li>依赖 <code>PsLookupProcessByProcessId</code></li><li>无法发现所有内核级隐藏方式</li></ol><p>后续计划：</p><ul><li>解析 <code>PspCidTable</code> 的三级表结构获取进程列表</li><li>增加线程交叉验证</li></ul><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32next">Microsoft Learn - Process32Next function</a></li><li><a href="https://bbs.kanxue.com/thread-292214.htm">看雪论坛 - Windows 进程枚举 NtQuerySystemInformation(SystemProcessInformation)</a></li><li>Windows Internals, 7th Edition</li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/04/SnowSword-%E8%BF%9B%E7%A8%8B%E9%9A%90%E8%97%8F%E6%A3%80%E6%B5%8B-%E4%BF%AE%E6%AD%A3%E7%89%88/</id>
    <link href="https://snow-blog.pages.dev/2026/08/04/SnowSword-%E8%BF%9B%E7%A8%8B%E9%9A%90%E8%97%8F%E6%A3%80%E6%B5%8B-%E4%BF%AE%E6%AD%A3%E7%89%88/"/>
    <published>2026-08-04T13:05:00.000Z</published>
    <summary>介绍SnowSword中隐藏进程检测模块的设计思路，从用户态进程枚举API到内核EPROCESS对象分析，通过多种视角交叉验证发现异常进程。</summary>
    <title>SnowSword 进程隐藏检测实现：从用户态枚举到内核对象分析</title>
    <updated>2026-11-05T13:18:22.189Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="CTF" scheme="https://snow-blog.pages.dev/categories/CTF/"/>
    <category term="Reverse Engineering" scheme="https://snow-blog.pages.dev/categories/CTF/Reverse-Engineering/"/>
    <category term="Windows" scheme="https://snow-blog.pages.dev/tags/Windows/"/>
    <category term="网络安全协会暑期培训" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E5%8D%8F%E4%BC%9A%E6%9A%91%E6%9C%9F%E5%9F%B9%E8%AE%AD/"/>
    <category term="CTF" scheme="https://snow-blog.pages.dev/tags/CTF/"/>
    <category term="Reverse" scheme="https://snow-blog.pages.dev/tags/Reverse/"/>
    <category term="PE" scheme="https://snow-blog.pages.dev/tags/PE/"/>
    <category term="密码学" scheme="https://snow-blog.pages.dev/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/"/>
    <content>
      <![CDATA[<h2 id="题目信息"><a href="#题目信息" class="headerlink" title="题目信息"></a>题目信息</h2><table><thead><tr><th>项目</th><th>内容</th></tr></thead><tbody><tr><td>赛事</td><td>MoeCTF 2021</td></tr><tr><td>题目</td><td>PEPEPE</td></tr><tr><td>分值</td><td>443 分</td></tr><tr><td>类型</td><td>Reverse &#x2F; PE 文件还原</td></tr><tr><td>考点</td><td>已知明文攻击、XOR 流密码、PE 文件结构</td></tr><tr><td>平台</td><td>NSSCTF (<a href="https://www.nssctf.cn/problem/3391">https://www.nssctf.cn/problem/3391</a>)</td></tr></tbody></table><h2 id="题目描述"><a href="#题目描述" class="headerlink" title="题目描述"></a>题目描述</h2><blockquote><p>题目提供了一个经过加密的 PE 文件和加密程序源码，需要分析加密算法，并恢复原始可执行文件。</p></blockquote><p>加密后的文件头部已无 <code>MZ</code> 标志，用 <code>file</code> 命令识别为纯 <code>data</code>。</p><h2 id="加密原理分析"><a href="#加密原理分析" class="headerlink" title="加密原理分析"></a>加密原理分析</h2><p>查看加密器 <code>main.c</code> 的核心逻辑：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">scanf</span>(<span class="string">&quot;%s&quot;</span>, input);                      <span class="comment">// 输入 key</span></span><br><span class="line">...</span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; file_len - <span class="number">1</span>; i++) &#123;</span><br><span class="line">    bank[i] = getc(f);</span><br><span class="line">    bank[i] = ~(bank[i] ^ input[i % input_len]);   <span class="comment">// 逐字节 XOR 后取反</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>加密公式：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">enc[i] = ~(org[i] ^ key[i % key_len])</span><br></pre></td></tr></table></figure><p>其中 <code>key</code> 是加密时输入的字符串，按 <code>i % key_len</code> 循环使用。这是一个<strong>循环 XOR 流密码</strong>（多了一步取反），强度完全取决于 key 的保密性。</p><h2 id="解题思路：已知明文攻击"><a href="#解题思路：已知明文攻击" class="headerlink" title="解题思路：已知明文攻击"></a>解题思路：已知明文攻击</h2><h3 id="为什么能攻击？"><a href="#为什么能攻击？" class="headerlink" title="为什么能攻击？"></a>为什么能攻击？</h3><p>PE 文件有<strong>固定不变的结构</strong>——文件头。任何 PE 文件都以 <code>MZ</code>（<code>0x4D 0x5A</code>）开头，后面跟着结构固定的 DOS Header（大量字节是固定的 <code>0x00</code>）。这就是”已知明文”。</p><p><img src="/images/%E6%97%A5%E6%8A%A5_PEPEPE_%E5%B7%B2%E7%9F%A5%E6%98%8E%E6%96%87%E6%94%BB%E5%87%BB/pe_structure_diagram.png" alt="PE文件结构示意图"></p><p>前 64 字节的 DOS Header 中，大量字节是固定值或零值，非常适合作为已知明文。</p><h3 id="Key-恢复"><a href="#Key-恢复" class="headerlink" title="Key 恢复"></a>Key 恢复</h3><p>把加密公式变形：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">key[i] = ~enc[i] ^ org[i]</span><br></pre></td></tr></table></figure><p>用加密文件的前若干字节 + 已知的 PE 头部明文，恢复 key 的循环片段，检测周期得完整 key（其中 <code>plain_head</code> 取自原始文件 <code>file_org</code> 的前 64 字节，即标准 DOS Header）：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">recover_key</span>(<span class="params">data: <span class="built_in">bytes</span>, plain_head: <span class="built_in">bytes</span></span>) -&gt; <span class="built_in">bytes</span>:</span><br><span class="line">    ks = <span class="built_in">bytes</span>([(~data[i]) &amp; <span class="number">0xFF</span> ^ plain_head[i] <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(plain_head))])</span><br><span class="line">    <span class="keyword">for</span> period <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, <span class="built_in">len</span>(ks) // <span class="number">2</span> + <span class="number">1</span>):</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">all</span>(ks[i] == ks[i % period] <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(ks))):</span><br><span class="line">            <span class="keyword">return</span> ks[:period]</span><br><span class="line">    <span class="keyword">return</span> ks</span><br></pre></td></tr></table></figure><p>运行结果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[+] recovered key = b&#x27;reverierwilllikeyou!&#x27; (len=20)</span><br></pre></td></tr></table></figure><p><strong>key &#x3D; <code>reverierwilllikeyou!</code></strong>（20 字节），呼应了 <code>file_org.c</code> 中的 “I think <strong>ReverierXu</strong> will like you!”。</p><h2 id="完整还原"><a href="#完整还原" class="headerlink" title="完整还原"></a>完整还原</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">decrypt</span>(<span class="params">data: <span class="built_in">bytes</span>, key: <span class="built_in">bytes</span></span>) -&gt; <span class="built_in">bytes</span>:</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">bytes</span>([(~b) &amp; <span class="number">0xFF</span> ^ key[i % <span class="built_in">len</span>(key)] <span class="keyword">for</span> i, b <span class="keyword">in</span> <span class="built_in">enumerate</span>(data)])</span><br></pre></td></tr></table></figure><p>验证结果：</p><ul><li><code>file_restored</code> 被识别为 <code>PE32 executable</code></li><li>与原始文件对比，前 45559&#x2F;45560 字节完全一致</li></ul><blockquote><p>最后一个字节差异：加密循环只处理 <code>file_len - 1</code> 个字节，末字节为未初始化内存（垃圾值），不影响 PE 完整性。</p></blockquote><h2 id="运行验证"><a href="#运行验证" class="headerlink" title="运行验证"></a>运行验证</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">cp</span> file_restored file_restored.exe &amp;&amp; <span class="built_in">echo</span> <span class="string">&quot;&quot;</span> | ./file_restored.exe</span><br><span class="line">[root@Track.Sh]# Good job, I think ReverierXu will like you!</span><br><span class="line">[root@Track.Sh]# moectf&#123;P3_Structur3_1s_r3ally_fUnnY!&#125;</span><br></pre></td></tr></table></figure><h2 id="Flag"><a href="#Flag" class="headerlink" title="Flag"></a>Flag</h2><blockquote><p>注意：还原后的程序输出的是 <code>moectf{}</code> 格式，而 NSSCTF 平台要求使用 <code>NSSCTF{}</code> 格式提交（题目描述中已注明）。</p></blockquote><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">NSSCTF&#123;P3_Structur3_1s_r3ally_fUnnY!&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol><li>识别加密算法为<strong>循环 XOR（+取反）流密码</strong></li><li>利用 <strong>PE 文件头固定结构</strong> 进行<strong>已知明文攻击</strong>恢复 key</li><li>检测 key 循环周期得到完整 key</li><li>解密整个文件，还原出可运行的 PE，运行得到 flag</li><li>固定格式的数据结构会降低简单流密码的安全性，因此在设计加密方案时不能仅依赖 XOR 与固定密钥。</li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format">PE Format - Microsoft Learn</a></li><li><a href="https://www.nssctf.cn/problem/3391">NSSCTF - PEPEPE</a></li></ul>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/08/01/%E6%97%A5%E6%8A%A5_PEPEPE_%E5%B7%B2%E7%9F%A5%E6%98%8E%E6%96%87%E6%94%BB%E5%87%BB/</id>
    <link href="https://snow-blog.pages.dev/2026/08/01/%E6%97%A5%E6%8A%A5_PEPEPE_%E5%B7%B2%E7%9F%A5%E6%98%8E%E6%96%87%E6%94%BB%E5%87%BB/"/>
    <published>2026-08-01T12:00:00.000Z</published>
    <summary>通过 MoeCTF 2021 的 PEPEPE 题目，学习如何利用 PE 文件头（DOS Header）的固定结构进行已知明文攻击，恢复循环 XOR 流密码的密钥，最终还原被加密的 PE 文件。涉及 PE 文件结构、流密码分析、已知明文攻击与周期检测等知识点。</summary>
    <title>[MoeCTF 2021] PEPEPE —— 利用 PE 文件头恢复 XOR 密钥</title>
    <updated>2026-09-21T04:20:05.614Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="学习笔记" scheme="https://snow-blog.pages.dev/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
    <category term="网络安全" scheme="https://snow-blog.pages.dev/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    <category term="Windows" scheme="https://snow-blog.pages.dev/tags/Windows/"/>
    <category term="内核编程" scheme="https://snow-blog.pages.dev/tags/%E5%86%85%E6%A0%B8%E7%BC%96%E7%A8%8B/"/>
    <category term="学习记录" scheme="https://snow-blog.pages.dev/tags/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/"/>
    <content>
      <![CDATA[<h2 id="起点"><a href="#起点" class="headerlink" title="起点"></a>起点</h2><p>我最初接触计算机时，更多关注的是软件开发和系统底层原理。</p><p>相比于单纯调用高级接口完成应用开发，我更喜欢了解程序运行背后的机制，例如：</p><ul><li>操作系统如何管理进程和线程</li><li>程序如何加载和执行</li><li>用户态与内核态之间如何交互</li><li>硬件资源如何被系统抽象和管理</li></ul><p>随着学习深入，我逐渐发现，系统底层知识与网络安全之间存在非常紧密的联系。</p><p>安全问题很多时候并不是单独存在于某个漏洞或工具中，而是建立在对操作系统、程序运行机制以及计算机体系结构的理解之上。</p><p>因此，我开始尝试向网络安全方向扩展。</p><hr><h2 id="当前学习方向"><a href="#当前学习方向" class="headerlink" title="当前学习方向"></a>当前学习方向</h2><p>目前我的学习重点主要集中在 Windows 平台安全相关方向。</p><h3 id="用户态安全研究"><a href="#用户态安全研究" class="headerlink" title="用户态安全研究"></a>用户态安全研究</h3><p>用户态是大多数程序运行的环境，也是理解安全机制的重要入口。</p><p>目前主要学习：</p><ul><li>Windows API 调用机制</li><li>进程与线程管理</li><li>PE 文件结构</li><li>调试与程序分析</li><li>系统信息采集</li></ul><p>这些内容帮助理解一个程序从启动到运行过程中发生了什么。</p><hr><h3 id="Windows-内核与驱动开发"><a href="#Windows-内核与驱动开发" class="headerlink" title="Windows 内核与驱动开发"></a>Windows 内核与驱动开发</h3><p>Windows 内核提供了更加底层的系统控制能力，也是系统安全研究的重要方向。</p><p>目前正在学习：</p><ul><li>Windows 驱动开发模型</li><li>WDM 基础结构</li><li>IRP 请求处理机制</li><li>内核对象管理</li><li>用户态与内核态通信</li></ul><p>通过驱动开发，可以进一步理解操作系统内部工作方式。</p><hr><h3 id="安全工具开发"><a href="#安全工具开发" class="headerlink" title="安全工具开发"></a>安全工具开发</h3><p>在学习过程中，我也尝试开发一些辅助分析工具。</p><p>目前正在开发的项目是 SnowSword，一个面向 Windows 平台的系统分析工具。</p><p>项目主要用于：</p><ul><li>学习 Windows 系统内部结构</li><li>实践系统信息采集</li><li>验证安全分析相关技术</li></ul><p>通过实际开发工具，可以将理论知识转化为更加具体的实践经验。</p><p>后续会单独记录项目开发过程中的设计思路和遇到的问题。</p><hr><h2 id="后续学习计划"><a href="#后续学习计划" class="headerlink" title="后续学习计划"></a>后续学习计划</h2><p>未来希望继续完善自己的知识体系：</p><h3 id="Windows方向"><a href="#Windows方向" class="headerlink" title="Windows方向"></a>Windows方向</h3><ul><li>深入学习 Windows Internals</li><li>研究系统安全机制</li><li>学习恶意代码分析方法</li><li>提升逆向分析能力</li></ul><h3 id="网络安全基础"><a href="#网络安全基础" class="headerlink" title="网络安全基础"></a>网络安全基础</h3><ul><li>网络协议</li><li>常见漏洞原理</li><li>Web安全基础</li><li>密码学基础</li></ul><h3 id="实践方向"><a href="#实践方向" class="headerlink" title="实践方向"></a>实践方向</h3><ul><li>参与 CTF 或安全实验</li><li>阅读优秀安全项目源码</li><li>通过实际项目提升工程能力</li></ul><hr><h2 id="当前不足"><a href="#当前不足" class="headerlink" title="当前不足"></a>当前不足</h2><p>目前我的学习方向更加偏向 Windows 系统和底层开发，在网络方向还有很多需要补充的地方。</p><p>例如：</p><ul><li>网络协议知识还需要系统学习</li><li>Linux 安全生态接触较少</li><li>安全分析经验仍需要通过更多实践积累</li></ul><p>因此，后续会逐步完善自己的知识结构，而不是局限于某一个方向。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>网络安全是一门综合性很强的领域。</p><p>工具、框架和环境会不断变化，但对计算机基础原理的理解始终具有价值。</p><p>希望通过博客记录自己的学习过程，将零散的知识整理下来，也方便未来回顾自己的成长路线。</p><p>这也是开始记录网络安全学习的第一步。</p>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/07/30/from-system-development-to-cyber-security/</id>
    <link href="https://snow-blog.pages.dev/2026/07/30/from-system-development-to-cyber-security/"/>
    <published>2026-07-29T16:00:00.000Z</published>
    <summary>记录从 Windows 系统开发逐步探索网络安全方向的学习路线，包括用户态、内核开发、安全工具开发以及未来学习规划。</summary>
    <title>从 Windows 底层开发到网络安全：我的学习路线</title>
    <updated>2026-07-30T10:32:09.005Z</updated>
  </entry>
  <entry>
    <author>
      <name>Snow</name>
    </author>
    <category term="技术教程" scheme="https://snow-blog.pages.dev/categories/%E6%8A%80%E6%9C%AF%E6%95%99%E7%A8%8B/"/>
    <category term="Hexo" scheme="https://snow-blog.pages.dev/tags/Hexo/"/>
    <category term="Butterfly" scheme="https://snow-blog.pages.dev/tags/Butterfly/"/>
    <category term="博客搭建" scheme="https://snow-blog.pages.dev/tags/%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA/"/>
    <category term="GitHub Pages" scheme="https://snow-blog.pages.dev/tags/GitHub-Pages/"/>
    <content>
      <![CDATA[<p>前言<br>作为一个正在学习网络安全的技术人，我一直想拥有一个属于自己的技术博客，用来记录学习笔记和踩坑经验。经过一段时间的折腾，终于把博客搭起来了。这篇文章完整记录了从环境准备到部署上线的全过程，希望能帮助到同样想搭建博客的你。<br>一、环境准备与Hexo初始化<br>1.1 安装Node.js<br>Hexo基于Node.js运行，建议安装18.x或更高版本。安装完成后在终端验证版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">node -v</span><br><span class="line">npm -v</span><br></pre></td></tr></table></figure><p>1.2 安装Git<br>从Git官网下载安装，用于版本控制、主题拉取以及博客后续部署。<br>1.3 安装Hexo CLI<br>全局安装Hexo脚手架工具：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g hexo-cli</span><br></pre></td></tr></table></figure><p>1.4 初始化博客项目<br>在普通文件夹下创建博客目录，规避权限报错问题：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> projects &amp;&amp; <span class="built_in">cd</span> projects</span><br><span class="line">hexo init my-blog</span><br><span class="line"><span class="built_in">cd</span> my-blog</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><p>1.5 本地预览博客<br>启动本地服务，预览初始博客页面：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo server</span><br></pre></td></tr></table></figure><p>访问 <code>http://localhost:4000</code>，看到Hexo默认博客页面即为初始化成功。<br>⚠️ 踩坑记录：hexo init 连接失败<br>执行初始化时遇到报错：<code>Failed to connect to github.com port 443 via 127.0.0.1</code><br>报错原因：Git全局代理配置与本地代理软件端口不匹配，导致无法正常拉取GitHub资源。<br>解决方案：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方案A：取消全局代理（优先推荐）</span></span><br><span class="line">git config --global --<span class="built_in">unset</span> http.proxy</span><br><span class="line">git config --global --<span class="built_in">unset</span> https.proxy</span><br><span class="line"></span><br><span class="line"><span class="comment"># 方案B：需保留代理时，配置正确端口</span></span><br><span class="line">git config --global http.proxy http://127.0.0.1:7890</span><br><span class="line">git config --global https.proxy http://127.0.0.1:7890</span><br></pre></td></tr></table></figure><p>经验总结：浏览器可正常访问GitHub，不代表Git网络配置正常，二者独立生效，需单独配置。<br>二、安装Butterfly主题<br>Butterfly是Hexo热门现代化主题，内置暗黑模式、TOC目录、代码高亮、图片灯箱等丰富功能，无需大量自定义插件，开箱即用。<br>2.1 克隆主题源码<br>在博客根目录执行命令，拉取稳定版主题：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># GitHub官方源（稳定版）</span></span><br><span class="line">git <span class="built_in">clone</span> -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly</span><br></pre></td></tr></table></figure><p>国内GitHub访问卡顿&#x2F;超时，可使用Gitee镜像源：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Gitee镜像源</span></span><br><span class="line">git <span class="built_in">clone</span> -b master https://gitee.com/immyw/hexo-theme-butterfly.git themes/butterfly</span><br></pre></td></tr></table></figure><p>2.2 安装主题依赖<br>Butterfly依赖pug、stylus渲染器，必须安装否则页面渲染报错：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-renderer-pug hexo-renderer-stylus --save</span><br></pre></td></tr></table></figure><p>2.3 启用主题<br>打开博客根目录 <code>_config.yml</code>，修改主题配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">theme:</span> <span class="string">butterfly</span></span><br></pre></td></tr></table></figure><p>2.4 验证主题生效<br>清除缓存并重启本地服务：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo clean &amp;&amp; hexo server</span><br></pre></td></tr></table></figure><p>页面从默认简约样式变为现代化卡片布局，即主题安装启用成功。<br>三、主题基础个性化配置<br>3.1 分离主题配置文件（关键）<br>禁止直接修改<code>themes/butterfly/_config.yml</code>，主题升级会清空自定义配置。<br>在根目录生成独立主题配置文件，永久保留自定义配置：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> themes/butterfly/_config.yml _config.butterfly.yml</span><br></pre></td></tr></table></figure><p>后续所有主题美化、功能开关均修改 <code>_config.butterfly.yml</code> 文件。<br>3.2 站点基础信息配置<br>修改根目录 <code>_config.yml</code>，配置网站核心信息：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">title:</span> <span class="string">SnowSword</span></span><br><span class="line"><span class="attr">subtitle:</span> <span class="string">&#x27;记录网络安全学习之路&#x27;</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">&#x27;网络安全学习笔记与技术分享&#x27;</span></span><br><span class="line"><span class="attr">author:</span> <span class="string">Snow</span></span><br><span class="line"><span class="attr">language:</span> <span class="string">zh-CN</span></span><br><span class="line"><span class="attr">timezone:</span> <span class="string">&#x27;Asia/Shanghai&#x27;</span></span><br><span class="line"><span class="attr">url:</span> <span class="string">https://user-lzy.github.io</span></span><br></pre></td></tr></table></figure><p>3.3 导航栏菜单配置<br>修改 <code>_config.butterfly.yml</code>，自定义顶部导航及图标：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">menu:</span></span><br><span class="line">  <span class="string">首页:</span> <span class="string">/</span> <span class="string">||</span> <span class="string">fa-solid</span> <span class="string">fa-house</span></span><br><span class="line">  <span class="string">归档:</span> <span class="string">/archives/</span> <span class="string">||</span> <span class="string">fa-solid</span> <span class="string">fa-archive</span></span><br><span class="line">  <span class="string">分类:</span> <span class="string">/categories/</span> <span class="string">||</span> <span class="string">fa-solid</span> <span class="string">fa-folder</span></span><br><span class="line">  <span class="string">标签:</span> <span class="string">/tags/</span> <span class="string">||</span> <span class="string">fa-solid</span> <span class="string">fa-tags</span></span><br><span class="line">  <span class="string">关于:</span> <span class="string">/about/</span> <span class="string">||</span> <span class="string">fa-solid</span> <span class="string">fa-user</span></span><br></pre></td></tr></table></figure><p>3.4 首页文章摘要规则<br>配置首页文章展示逻辑，优化阅读体验：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">index_post_content:</span></span><br><span class="line">  <span class="attr">method:</span> <span class="number">2</span>          <span class="comment"># 优先读取文章description，无则自动截取正文</span></span><br><span class="line">  <span class="attr">length:</span> <span class="number">500</span>        <span class="comment"># 摘要最大字数</span></span><br></pre></td></tr></table></figure><p>3.5 创建必备独立页面<br>终端执行命令，批量生成分类、标签、关于页面：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">hexo new page categories</span><br><span class="line">hexo new page tags</span><br><span class="line">hexo new page about</span><br></pre></td></tr></table></figure><p>依次修改对应页面<code>index.md</code> 配置：<br>source&#x2F;categories&#x2F;index.md</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">title: 分类</span><br><span class="line">type: &quot;categories&quot;</span><br><span class="line"><span class="section">comments: false</span></span><br><span class="line"><span class="section">---</span></span><br></pre></td></tr></table></figure><p>source&#x2F;tags&#x2F;index.md</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">title: 标签</span><br><span class="line">type: &quot;tags&quot;</span><br><span class="line"><span class="section">comments: false</span></span><br><span class="line"><span class="section">---</span></span><br></pre></td></tr></table></figure><p>source&#x2F;about&#x2F;index.md</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">title: 关于我</span><br><span class="line">type: &quot;about&quot;</span><br><span class="line"><span class="section">comments: false</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">欢迎来到我的技术博客！这里主要记录我在网络安全学习中的笔记与踩坑经验。</span><br></pre></td></tr></table></figure><p>3.6 头像配置及避坑指南<br>将头像图片放入 <code>source/images/</code> 目录，修改 <code>_config.butterfly.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 侧边栏头像配置</span></span><br><span class="line"><span class="attr">avatar:</span></span><br><span class="line">  <span class="attr">img:</span> <span class="string">/images/14.jpg</span></span><br><span class="line">  <span class="attr">effect:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 关于页面信息配置</span></span><br><span class="line"><span class="attr">about:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">avatar:</span> <span class="string">/images/14.jpg</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">&quot;Snow&quot;</span></span><br><span class="line">  <span class="attr">description:</span> <span class="string">&quot;程序员一枚，正在学习网络安全&quot;</span></span><br><span class="line">  <span class="attr">top_img:</span> <span class="string">/images/14.jpg</span></span><br></pre></td></tr></table></figure><p>避坑提示：头像显示默认占位图，大概率是图片路径错误或未执行 <code>hexo clean</code> 清除缓存。<br>四、GitHub Pages 部署上线</p><blockquote><p><strong>2026-11-05 补注</strong>：下面这一节记录的是当时的搭建过程，保留原文不做修改。该 GitHub 仓库现已不可访问（返回 404），本站已迁移到 Cloudflare Pages；如果照着这一节配置，<code>hexo deploy</code> 会失败。</p></blockquote><p>4.1 创建专属GitHub仓库<br>登录GitHub，新建公开仓库，仓库名必须严格遵循格式：<code>用户名.github.io</code>（与自己GitHub用户名完全一致），否则无法正常访问。<br>4.2 安装部署插件<br>安装Hexo Git部署工具，实现一键部署：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-deployer-git --save</span><br></pre></td></tr></table></figure><p>4.3 配置部署参数<br>修改根目录 <code>_config.yml</code> 部署配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">deploy:</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">&#x27;git&#x27;</span></span><br><span class="line">  <span class="attr">repo:</span> <span class="string">https://github.com/user-lzy/user-lzy.github.io.git</span></span><br><span class="line">  <span class="attr">branch:</span> <span class="string">main</span></span><br></pre></td></tr></table></figure><p>4.4 一键部署上线<br>依次执行清理、生成、部署命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">hexo clean    <span class="comment"># 清除旧缓存文件</span></span><br><span class="line">hexo generate <span class="comment"># 生成静态网站文件</span></span><br><span class="line">hexo deploy   <span class="comment"># 部署到GitHub Pages</span></span><br></pre></td></tr></table></figure><p>部署成功后，访问 <code>https://user-lzy.github.io</code> 即可成功访问个人博客。<br>五、博客后续优化方向<br>基础搭建完成后，可按需进行功能优化，提升博客体验：<br>优化方向功能说明<br>站内搜索安装 <code>hexo-generator-search</code> 实现本地全文搜索<br>评论系统接入Giscus、Twikoo等轻量化评论插件<br>站点统计开启内置不蒜子统计，展示访客、浏览量数据<br>暗黑模式Butterfly原生支持，可直接在配置文件开启切换功能<br>代码高亮美化切换主题内置代码高亮配色，适配编程阅读场景<br>写在最后<br>搭建个人技术博客门槛并不高，核心在于耐心配置、解决报错。Hexo+Butterfly+GitHub Pages的组合，零成本、高颜值、易维护，非常适合技术新人记录学习历程。<br>后续我会持续更新网络安全学习笔记，完善博客功能。</p>]]>
    </content>
    <id>https://snow-blog.pages.dev/2026/07/27/Hexo-Butterfly%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E5%85%A8%E8%AE%B0%E5%BD%95/</id>
    <link href="https://snow-blog.pages.dev/2026/07/27/Hexo-Butterfly%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E5%85%A8%E8%AE%B0%E5%BD%95/"/>
    <published>2026-07-27T10:00:00.000Z</published>
    <summary>从零开始搭建Hexo博客并配置Butterfly主题，包含环境配置、主题安装、常见踩坑及部署上线的完整记录。</summary>
    <title>Hexo + Butterfly 个人技术博客搭建全记录</title>
    <updated>2026-11-05T13:20:47.133Z</updated>
  </entry>
</feed>
