Skip to main content

白加黑利用

“白加黑”常被简单描述为“合法 EXE 加恶意 DLL”,但这个说法还不够准确。它真正滥用的是软件生态中的加载信任:一个合法、通常带有效签名的程序,会按照既定搜索规则或插件机制加载旁边的组件;攻击者控制了被加载的组件,于是恶意代码自然运行在合法程序内部。

先说结论:

白 = 原始、未被篡改的合法加载程序
黑 = 攻击者可控的 DLL、插件、配置或后续载荷
加 = 合法程序原本就具备的加载关系

白文件的有效签名表示:在当前校验方式与信任策略下,签名所覆盖的内容通过了完整性验证,签名者通过了相应信任检查。它不等于整文件每个字节都未变化,也不证明同目录中的其他文件安全,更不证明当前文件名、图标、落地路径和产品身份真实。

基本机制

Windows 程序在运行时需要解析依赖。除了系统已知 DLL,程序还可能从应用目录、系统目录、环境路径、清单、注册信息或自定义插件目录中寻找组件。

如果一个合法程序:

  • 依赖某个没有使用绝对路径定位的 DLL。
  • 会优先在自身目录寻找同名组件。
  • 缺少对被加载组件的签名、哈希或发布者约束。
  • 暴露了可被替换的插件、编解码器或扩展接口。

那么攻击者只需让它在受控目录里启动,并让一个接口兼容的恶意组件出现在预期位置,合法程序就可能主动加载该组件。这个过程不要求修改白文件,也不要求攻击者拥有白文件发布者的私钥。

受控目录
├── 看似正常的软件名.exe ← 原始合法文件,签名有效
├── 程序会寻找的组件.dll ← 未签名、伪造签名或来源异常
└── 配置 / 加密数据 ← 可选的后续载荷

用户启动 EXE

EXE 按正常逻辑寻找依赖或插件

同目录恶意 DLL 被加载

恶意代码在合法签名进程内执行

这更准确地对应 DLL 侧加载、搜索顺序劫持或插件机制滥用,而不是“把恶意代码塞进一个仍保持有效签名的 EXE”。

黑 DLL 如何执行恶意行为

DLL 不只是“被动的链接库”

DLL 的确主要用于复用代码和导出函数,但“库”描述的是它的组织和加载方式,不代表其中不能包含主动执行逻辑。DLL 被进程加载时,Windows 加载器会完成映像映射、依赖解析、重定位和初始化,然后调用 DLL 的入口函数。

因此,DLL 可以同时具备两种角色:

正常角色:提供导出函数,供 EXE 调用
恶意角色:在加载初始化阶段执行代码,或被 EXE 调用后执行恶意逻辑

DLL 通常没有像 EXE 那样的 main 入口,但常见执行入口包括:

  • DllMain:DLL 被加载、线程创建或进程退出时由系统调用;
  • TLS Callback:在进入 DllMain 前后执行初始化代码;
  • 导出函数:白程序按照预期名称调用的函数;
  • C/C++ 运行库初始化函数:间接触发全局对象、构造函数或初始化逻辑;
  • DLL 内部创建的线程:加载后异步执行后续任务。

所以,“DLL 本身是链接库”与“DLL 可以执行恶意行为”并不矛盾:它可以先以库的形式被加载,再利用加载入口或导出函数获得执行机会。

通过 DllMain 执行

最直接的方式是把恶意逻辑放在 DllMain 的进程附加分支中:

白 EXE 启动
→ 加载黑 DLL
→ Windows 调用 DllMain(DLL_PROCESS_ATTACH)
→ 黑 DLL 执行初始化和恶意逻辑

此时白程序甚至不需要显式调用某个恶意导出函数,只要依赖解析或显式加载成功,DLL 就已经获得代码执行机会。可能执行的行为包括:

  • 解密内存中的下一阶段载荷;
  • 创建工作线程,避免阻塞加载流程;
  • 读取配置、注册表和本地文件;
  • 建立 C2 连接;
  • 加载其他 DLL 或内存模块;
  • 执行持久化、注入或信息收集。

不过,DllMain 运行在加载器锁相关的初始化上下文中,不适合执行所有复杂操作。更成熟的实现通常只在 DllMain 中完成简单初始化,然后创建线程或转入导出函数继续执行。

通过导出函数执行

有些白程序会调用 DLL 中固定名称的导出函数,例如初始化、编解码器、插件入口或版本查询函数。黑 DLL 可以伪造这些导出函数,在满足白程序调用约定的同时执行恶意逻辑:

白 EXE 加载黑 DLL
→ GetProcAddress / 导入表解析导出函数
→ 调用预期导出函数
→ 黑 DLL 执行恶意逻辑
→ 返回看似正常的结果

为了避免白程序因缺少函数或返回值异常而退出,黑 DLL 可能会:

  • 保留白程序需要的函数名和参数形式;
  • 返回符合预期的状态码或对象;
  • 将真正恶意逻辑放在函数调用前后;
  • 将原始 DLL 重命名后转发部分导出函数;
  • 只在特定命令行、文件存在或时间条件满足时执行载荷。

因此,分析黑 DLL 时不能只查看 DllMain,还要检查导出表、导出函数的交叉引用和白程序实际调用了哪些接口。

通过 TLS Callback 或运行库初始化执行

如果恶意逻辑放在 TLS Callback 或运行库初始化阶段,它可能在分析人员预期的导出函数之前执行:

DLL 映射
→ TLS Callback
→ 运行库初始化
→ DllMain
→ 导出函数

这类入口可以用于解密载荷、反调试、环境检查或创建后续线程。检测时应检查 PE 的 TLS 目录、入口地址和初始化代码,不能只围绕导出函数做静态分析。

黑 DLL 为什么可以“借白执行”

黑 DLL 被加载后,恶意代码运行在白 EXE 的进程空间中,因此可以继承或利用白程序的运行上下文:

  • 进程列表中显示的是白 EXE 的名称;
  • 进程树和初始启动行为可能看起来正常;
  • DLL 可能继承白程序的网络、文件和用户会话环境;
  • 某些安全产品可能只对启动 EXE 做签名判断;
  • 黑 DLL 可以继续加载内存载荷,减少最终模块落盘。

但需要注意:白签名不会传递给黑 DLL。 黑 DLL 仍然应单独检查签名、路径、哈希、导出表和加载来源。白 EXE 只是提供了一个合法的加载宿主,不会把黑 DLL 变成可信组件。

黑 DLL 的典型执行链

白 EXE 启动
→ 按依赖或插件规则定位 DLL
→ 加载黑 DLL
→ DllMain / TLS Callback 执行初始化
→ 导出函数被白 EXE 调用
→ 创建线程或解密下一阶段载荷
→ C2 通信、持久化、注入或数据收集

如果白 EXE 只提供加载能力,黑 DLL 也可能在入口函数中快速创建线程,然后立即返回一个正常值,使白程序继续运行;此时恶意逻辑已经脱离白程序的正常业务流程在后台执行。

检测重点

分析“白加黑”时,建议把以下事件关联起来:

签名正常的白 EXE
→ 从用户可写目录加载异常 DLL
→ DLL 存在 DllMain / TLS / 可疑导出逻辑
→ 创建线程、解密载荷或发起网络连接

重点检查:

  • DLL 是否位于白 EXE 同目录、临时目录、下载目录或其他用户可写目录;
  • DLL 是否未签名、签名者异常或文件刚刚落地;
  • DLL 的原始文件名、导出函数和版本信息是否与白程序职责匹配;
  • 加载后是否出现 CreateThread、内存分配、解密、C2、持久化或进程注入;
  • 白 EXE 是否正常加载过该 DLL,还是通过异常搜索路径被劫持;
  • DLL 是否加载了第二阶段模块,或在内存中出现没有正常磁盘对应的可执行区域。

因此,高价值信号不是“一个签名 EXE 加载了 DLL”,而是:

合法白 EXE
+ 异常路径中的黑 DLL
+ 加载入口或导出函数执行
+ 与原始业务无关的网络、持久化、注入或数据访问行为

签名边界

Authenticode 会对 PE 主体计算专用哈希,并把摘要、签名者信息和证书链放在签名结构中。校验时不只是看文件里“有没有证书”,还要重新计算摘要并验证签名。

因此:

  • 修改已签名文件的代码段、资源、导入表或其他受哈希覆盖的内容,正常会使签名失效。
  • 复制证书表、保留公司名字符串或替换图标,不能产生该公司的有效签名。
  • “证书存在”“证书链可信”“文件摘要匹配”是不同概念,产品不能只检查前两项。
  • 某些签名格式历史上存在未覆盖区域或校验策略差异,但不能把它概括成“任意替换代码仍能保活签名”。严格、完整的校验应拒绝主体被改动的文件。
warning

如果一个工具声称某文件“有腾讯证书”,先确认它展示的是嵌入证书的主题信息,还是完整的文件摘要校验结果。只有后者成功,才能称为该文件签名有效。

白签名文件尾部藏载荷

这类手法把“载荷存在哪里”和“谁负责执行它”分开:EXE 或 DLL 可以充当数据载体,而真正读取、解析和执行数据的逻辑可能在另一个组件中。它可以与白加黑组合,但不等于经典 DLL 侧加载。

Overlay 不会自动执行

PE 文件最后一个节的磁盘数据之后,可能还有证书表、安装包数据或其他附加内容。分析工具对 Overlay 的划分可能不同;“存在尾部数据”本身不是恶意证据,证书表也不能直接当成恶意载荷。

磁盘上的 EXE / DLL(示意,不代表固定布局)
├── PE 头与各节数据
├── 证书表(若采用嵌入式签名)
└── 其他附加数据(可能有,也可能没有)

数据要参与执行,还需要:
某个组件读取数据 → 解析 / 解密 / 解释 → 执行逻辑或触发漏洞

Windows 不会仅因为数据位于 EXE 尾部,就把它当作入口代码执行。若原程序从不处理这段数据,攻击者单纯追加内容并不能让它自动运行。必须找到读取者、数据用途及控制流转移的证据,才能称为“尾部载荷执行”。

更具体地说,PE Loader 按 PE 头、节表等结构加载映像,并不是把从文件开头到 EOF 的全部字节当成代码执行。节定义范围以外的数据不会仅因附在文件后面就成为可执行节;证书表也不是普通的代码映射区。程序后来主动打开或映射整个文件,是另一回事。

为什么文件变化后签名仍可能有效

Authenticode 使用的 PE 摘要不是整文件 SHA256:校验和字段、证书表目录项及证书表等有特定排除规则。因此,特定未覆盖区域中可能容纳额外内容;但不能推导为“任意向文件末尾追加数据,签名都不会失效”。普通附加数据可能参与摘要计算,结果还取决于结构是否合法及实际验证路径。参见微软的 PE 格式与 Authenticode 摘要规则

微软记录过在 WIN_CERTIFICATE 的 PKCS#7 填充区等位置嵌入内容的风险,并明确指出:这些内容本身不会直接执行,但应用可能错误信任并处理它们。相关历史问题包括 CVE-2013-3900;EnableCertPaddingCheck 针对特定证书填充实施更严格检查,并不是通用的“尾部载荷检测器”。还应区分嵌入式签名与目录签名,不能假定工具显示的 Valid 一定来自嵌入签名验证。参见 Understanding executable file signing微软安全公告 2915720

与白加黑组合的形态

  • 白 EXE + 黑 DLL + 尾部数据:黑 DLL 获得执行机会后,读取某文件尾部的下一阶段载荷。侧加载负责执行入口,尾部数据负责存储。
  • 白 EXE / 白 DLL 的原生功能被滥用:组件本来就能处理嵌入数据、配置或脚本,攻击者控制其输入;是否成立必须由具体处理逻辑证明。
  • 合法组件存在解析漏洞:攻击者可控数据触发漏洞,进而改变控制流。这是漏洞利用,不能仅凭签名和异常行为就确认。
  • 签名文件仅作为载体:另一个已经具备执行能力的组件读取其中数据,签名文件本身可能根本没有运行。

尾部数据怎样转化为执行:四条路径

路径一:利用已有的自读、解包功能。 安装器、自解压程序、更新器或打包程序可能本来就包含附加包读取逻辑:获取自身路径、打开自身文件、定位数据、读取并解压。这是合法功能;是否能被恶意利用,要进一步确认输入可控性和数据校验边界,不能认为所有安装器都可利用。

签名程序 / 配套加载组件
→ 获取自身或目标文件路径
→ 打开文件并定位附加包
→ 读取、解密或解压数据
├── 释放 DLL 到磁盘,再加载它
├── 在内存中装载 DLL(如手工映射 / 反射加载)
├── 将解码内容转为可执行内存中的代码
└── 释放程序并启动子进程

取证时可关注 GetModuleFileNameCreateFileSetFilePointer / SetFilePointerExReadFile 等调用及文件映射行为。具体包的定位可能依赖偏移、长度或尾部标记,没有统一格式。上述 API 序列只是调查线索,不代表所有样本都会使用这些接口,也不代表调用这些接口就是恶意。

路径二:利用配置、插件或解释执行机制。 尾部内容不一定是机器码,也可能是脚本、自定义数据库、插件包、压缩数据,或者描述下一阶段的位置、入口和解密参数的配置。可能出现“读取附加包 → 解析配置 → 选择 DLL / 导出函数 / 命令行 → 调用已有加载接口”的链路。即使最终操作在原版 DLL 内完成,驱动它的输入也可能来自攻击者。

路径三:作为漏洞输入。 数据进入反序列化器、压缩包解析器、图片或字体解析器、脚本引擎或自定义字节码解释器,触发其中的具体漏洞并改变控制流。必须同时证明程序确实处理了数据、相应漏洞存在且被触发;这些解析器的存在本身不是漏洞证据。这里尾部数据是攻击输入,不是 Windows 自动执行的代码。

路径四:与侧加载串联。 可能先侧加载黑 DLL,再由它读取 EXE 或 DLL 尾部的数据;也可能先由具备执行能力的程序释放尾部 DLL,再通过正常加载接口或不安全的搜索路径加载。单纯使用 LoadLibrary 不自动等于搜索顺序劫持,仍需确认加载目标与路径控制关系。关键问题始终是:在载荷被读取之前,哪个组件已经取得执行机会? 不能用“尾部有 DLL”循环解释“谁先执行了读取它的代码”。

可以把这类组合概括为 Signed carrier + unsigned appended payload(有效签名载体 + 未受该签名保护的附加载荷)。这个模型只描述满足相应签名覆盖条件的样本,不适用于所有 Overlay,也不意味着载体一定负责执行。

与代码段篡改、Module Stomping 的区别

形态改动位置或角色签名与检测重点
尾部附加数据文件数据区域,需其他逻辑读取核对实际哈希覆盖范围、结构差异及读取执行链
磁盘代码段 / 节内容修改磁盘上的 .text、受保护的头字段、导入表或入口等修改受签名摘要覆盖的内容通常使原签名失效
Code Cave 利用使用已有空隙存放内容,可能发生在磁盘或内存“空隙”不等于未签名区;须确定实际修改位置和执行入口
Module Stomping / 内存映像覆盖已映射模块的内存代码被替换或覆盖磁盘签名可仍有效,重点看内存完整性与实际执行地址

“PE Stomping”等名称在不同材料中可能指不同操作,不能仅按术语把它们全部等同于 Overlay,或认定它们都绕过了文件签名。

磁盘:signed.dll → 签名有效,整文件可与原版一致
↓ 加载映像
内存:signed.dll → 保护属性改变 / 写入 → patch、hook 或覆盖

执行内容已不同于磁盘

可调查 NtProtectVirtualMemoryVirtualProtectWriteProcessMemory 等操作与代码差异,但这些调用不是必需的固定前序。当前进程对自身内存的写入也可能只是普通内存复制;因此既不能只盯跨进程写入,也不能仅凭保护属性变化判定模块被恶意覆盖。

比“存在 Overlay”更有价值的三类行为链

以下是检测思路,不是所有 EDR 都会直接提供的固定事件:

内存执行方向:
签名进程读取自身映像、尤其接近 EOF 的区域
→ 解码 / 解压数据 → 写入内存 → 内存具备执行权限
→ 线程或控制流进入对应区域

释放再加载方向:
签名进程读取附加数据 → FileWrite 某 DLL → LoadDll 同一 DLL

代理执行方向:
签名进程读取自身 / 载体 → 释放或解析下一阶段
→ CreateProcess,或通过 rundll32 / regsvr32 等组件继续执行

关联时应核对同一进程实例、文件路径与哈希、时间顺序、读写偏移、内存地址,以及输出数据和输入包之间的关系。仅仅在时间上相邻,不足以证明因果关系。

静态上可以用“签名有效 + 相比可信原版出现异常附加区域 / 体积明显增大”筛选候选,再叠加上述行为。不能只用 file_size > end_of_last_sectionfile_size > certificate_end 定性:证书、对齐和合法附加包都会影响边界,不存在适合所有产品的“恶意 Overlay 大小阈值”。安装包、自解压文件、部分游戏或更新组件也可能正常使用附加数据。

检测名称应与证据强度匹配。例如,只有读取证据时可以称“签名 PE 读取尾部附加数据”;确认释放后可称“签名程序释放嵌入载荷”;只有证实执行来源时才称“签名映像执行附加载荷”。不要用笼统的 Malicious PE Overlay 把存储异常直接升级为恶意执行结论。

尾部数据的取证要点

  1. 保留 EXE、DLL 和同目录数据文件,分别记录整文件 SHA256、大小、版本、签名结果与验证环境。
  2. 对比可信来源的同版本、同架构原版;不要把 Authenticode 摘要当作整文件哈希。
  3. 核对各节的 PointerToRawData + SizeOfRawData、证书表文件偏移与长度以及 EOF。证书表目录中的地址是文件偏移,不是普通 RVA。
  4. 将差异定位到具体区域,区分正常证书、合法安装数据、填充和未知内容;高熵只能提示压缩或加密,不能单独定性。
  5. 查找谁读取了该区域、读取的偏移与长度、数据如何被处理,以及是否进入执行路径。仅有“自读文件”也不足以证明加载恶意代码。

EXE 和 DLL 都是白签名,如何解释恶意行为

签名有效与官方原版是两种证据

已知条件能支持的判断不能据此排除的情况
EXE、DLL 均显示签名有效两者在当前策略下通过签名验证已签名恶意组件、未覆盖区域变化、功能滥用、内存篡改
两者整文件 SHA256 均与可信官方同版本一致在基线可信的前提下,取证时磁盘文件可视为原版外部输入、合法功能被滥用、漏洞利用、运行时修改
已确认实际加载的是这两个原版文件不支持“这一个 DLL 是攻击者改写的黑 DLL”其他组件参与,以及合法 DLL 被用于非预期加载链

因此,不能仅凭“双白签”排除侧加载;如果实际加载的 DLL 确为合法原版,可以降低“该 DLL 本身就是黑组件”的优先级,但不能顺带排除整个进程的恶意执行链。磁盘取证还需与事件发生时间对应,避免把事后恢复的文件当作运行时样本。

两个文件完全合法,也可能被利用

原生能力滥用:程序本来就能联网、访问其他进程、修改注册表或执行安装动作,攻击者通过参数、配置、IPC、RPC 或其他接口驱动这些功能。恶意性来自使用目的和上下文,不一定来自新增代码。

漏洞利用:合法程序处理不可信输入时出现内存破坏等问题,攻击者获得控制。输入可能来自网络、已有连接、共享内存或早于调查时间窗的操作,不一定伴随新的文件或 DLL。

运行时篡改:磁盘文件是原版,不代表进程中的代码和数据未被修改。内存比对需要考虑重定位、导入解析、合法热补丁及安全软件 Hook,不能把所有差异都当成攻击。

两个白组件共同构成代理利用链:合法 EXE 提供加载接口,合法 DLL 提供导出函数或业务能力,攻击者通过参数、配置、注册表、COM 对象、脚本或附加数据控制最终行为。这可以理解为 LOLBin / LOL DLL 式功能滥用,不要求其中任何一个磁盘文件被替换成黑文件。

原版 signed.exe
→ LoadLibrary 加载原版 signed.dll
→ 调用导出函数 / 业务接口
→ 处理攻击者可控输入
→ 在原进程内执行不符合正常用途的操作

所以对白签 DLL 至少要分别考虑:尾部被用作数据容器、原生读取或加载能力被滥用、继续加载了其他恶意依赖、自身漏洞被利用,以及加载后内存被修改。签名检查只能回答其中一部分问题。

这些是待验证的假设,不是“合法进程出现异常”就能直接得出的结论。

有利用路径,为什么日志不一定能还原

有实际利用链,不等于有完整的可观测事件链。 文件、模块、进程和网络日志描述的是部分系统行为,不是每一次函数调用、解析步骤或控制流转移。

利用入口或阶段有采集覆盖时可找的证据常见盲区与判断边界
命令行 / 参数进程创建、父进程、特殊路径、URL、脚本参数参数可能截断;程序启动后的输入不一定写在启动命令行里
文件 / 注册表 / 配置谁写入配置、写入时间、程序读取目标、相关键值变化记录写入不代表记录读取;文件可能早已存在
DLL / COM / 插件LoadDll、模块路径、COM 激活、注册信息与插件访问模块事件不一定说明为何加载;进程外 COM 还涉及其他进程
内存漏洞利用异常、崩溃、内存保护变化、调用栈、后续行为堆溢出、UAF(释放后使用)、指令指针劫持未必有单独事件
纯进程内执行当前进程的敏感 API、网络连接、内存或栈异常可以没有子进程、无载荷落盘、无额外正常模块加载

举例来说,一条假设的真实链和日志可能是:

实际机制:攻击者控制文件
→ signed.exe 读取并解析
→ 漏洞触发,指令指针(如 x64 的 RIP)被劫持
→ 代码复用 / shellcode 执行
→ 网络连接、敏感进程访问

日志所见:signed.exe
→ NetworkConnect
→ ReadProcessMemory(lsass.exe)

日志中的两个后续操作不能证明上面的漏洞链,只能说明这是一种尚未排除的解释。EDR 可能更容易看到载荷的外部行为,而不是漏洞最初如何获得读写或控制流能力。

在产品具备相应能力时,可补看 ETW Threat Intelligence 相关遥测、内存扫描、异常与崩溃记录,以及 CFG / CET 相关保护触发信息。但这些不是完整的执行轨迹:数据是否存在取决于系统、硬件、编译与保护配置、传感器支持和采集权限;漏洞利用也不保证触发崩溃或保护告警。微软 CFG 文档说明了其对间接调用目标的约束,它不等于通用的漏洞过程记录器。

同样不能把 Sysmon 与“所有 API 调用日志”混为一谈。例如 Sysmon 的映像加载事件需要启用;CreateRemoteThread 事件面向在其他进程中创建线程,并不代表记录了所有进程内线程创建。应先核对实际事件配置,再解释缺失。参见 Sysmon 事件说明

优先补查哪些运行时行为

当现象是“合法 EXE → 合法 DLL → 异常行为”,可以按以下清单检查已有遥测,而不是预设每一种事件必然存在:

  • 内存准备VirtualAllocVirtualProtectNtProtectVirtualMemory,关注目标地址、权限变化及后续执行关联。
  • 数据写入WriteProcessMemory、进程内复制或解码,区分跨进程注入与当前进程处理自身数据;memcpy 通常不是独立的 EDR 事件。
  • 线程与控制流ThreadCreateCreateThreadNtCreateThreadEx、远程线程及实际起始地址;没有新线程不代表没有新执行逻辑。
  • 输入与加载:读取自身 EXE / DLL、特殊输入文件、配置、注册表、脚本、COM 与异常模块加载。
  • 后续效果:异常网络连接、ReadProcessMemory 等进程访问、凭据访问、敏感注册表修改、服务创建及持久化。

这些名称是 API 或事件类别示例,不是某产品的统一 action_name。在具体平台中应先确认字段含义、源进程和目标进程,以及“调用尝试”与“操作成功”的区别。

只看到进程直接做恶意行为,没有前置异常

首先区分没有发生没有采集到。应确认传感器是否覆盖文件读取、模块加载、内存操作等事件,是否存在过滤、丢失、保留期限制,以及时间窗是否覆盖进程启动和输入到达的时刻。许多日志只记录文件创建或修改,不能据此认定程序没有读文件。

只有在相关行为本应被可靠采集、时间窗完整且数据未丢失时,“没有观察到”才有较强的排除价值。否则不能因为没有文件读取或模块加载日志,就断言尾部载荷或漏洞利用的可能性明显下降。

另外,恶意执行不必具备固定的 VirtualAlloc → WriteProcessMemory → CreateThread 链:进程可以复用已有线程和内存,或者调用已有代码。反过来,这些 API 在合法软件中也很常见,单次调用不是恶意证明。

对“没有任何额外异常”的情况,可集中比较两个方向:

  • 原程序已有能力被驱动:例如进程本就能联网、访问其他进程、创建服务或操作注册表,异常目的通过未被采集的控制面传入;此时没有额外模块或注入痕迹是可以解释的。
  • 获得控制的前置过程缺失:例如内存破坏、间接跳转或堆对象异常没有被记录,之后攻击者复用已有代码。ROP(返回导向编程)和 COOP(面向对象代码复用)属于需要证据支持的代码复用方向,不能因为没看到内存分配就直接下这类结论。

应一并核查原对话中涉及的“缺失项”:异常文件读取、新模块、子进程、脚本、可疑命令行、内存分配或保护修改、跨进程写入、新线程,以及配置或注册表输入。它们是否真的被覆盖,比“界面上没有显示”更重要。

调查时优先回答:

  1. 所谓“恶意行为”是否已经确认,还是仅命中规则?例如访问敏感进程,需要结合访问权限、目标和业务职责判断。
  2. 事件是否正确归属到该进程实例?核对启动时间和进程唯一标识,避免 PID 复用或父子进程混淆。
  3. 是否有参数、文件、注册表、网络或进程间通信输入?同时向前扩展时间窗,并查看其他进程是否影响过它。
  4. 敏感操作的调用地址、调用栈和内存区域是什么?这些通常比进程名和签名更接近实际执行来源。

调用栈与内存证据怎么用

观察可支持的调查方向注意事项
调用地址位于原版模块的正常函数原生功能被滥用,或正常业务触发告警代码复用和控制流劫持也可能经过合法模块
调用来自无磁盘映像对应的可执行私有内存内存载荷、手工映射等方向JIT、运行时生成代码也会产生类似区域
模块代码与可信基线存在无法解释的差异运行时补丁、覆盖或注入先排除加载修正、合法 Hook 和热补丁
栈缺失、截断或无法符号化当前证据不足,需要补充上下文不能直接解释为 shellcode 或规避行为

可关注 call_stackstack_tracecaller_addresscaller_modulethread_start_address 及内存映射,但字段是否存在取决于实际产品。线程起始地址正常不代表后续执行始终正常,栈经过合法模块也不证明没有漏洞利用。

例如,同样观察到 signed.exe → ReadProcessMemory(lsass.exe),下面两类调用来源指向不同的调查重点(为简化示意,不代表完整系统调用栈):

来源 A:signed.exe!某正常业务函数
→ kernel32!ReadProcessMemory
优先核查:该功能为何被触发、访问权限与调用上下文。

来源 B:unknown / 无正常映像对应的可执行私有内存
→ kernel32!ReadProcessMemory
优先核查:该内存由谁写入、内容是什么、为何获得执行。

来源 A 更支持从业务能力滥用方向调查,但不能排除代码复用或栈伪装;来源 B 增强对内存载荷的怀疑,但要排除合法运行时行为。可执行区域也不一定是 RWX,它可能是 RX,或曾经从 RW 变成 RX。“谁在调用、执行地址属于什么内容”通常比“进程的磁盘文件是谁签名”更接近机制证据。

如何安排排查优先级

以下分支可以同时成立,不是互斥分类,也不是仅凭某一特征就能定性的规则:

白签 EXE + 白签 DLL + 已确认的异常行为
├── 只有 Valid,没有可信原版对照
│ → 先验证来源、整文件哈希和实际加载对象,不急于排除黑 DLL
├── 尾部相对原版有未知差异
│ → 定位差异,追踪读取者与数据用途
├── 自读自身 / DLL,特别是读取差异区域
│ → 检查嵌入数据处理链,也排除自校验和正常解包
├── 解码、内存写入、执行权限与执行地址能够关联
│ → 提高内存载荷执行的调查优先级
└── 两个整文件均为可信原版
→ 降低“这两个磁盘文件被改写”的优先级
→ 转查输入、功能调用、其他组件、漏洞与运行时篡改

如果同时确认:EXE 和 DLL 的整文件哈希与可信原版一致、没有尾部差异,且完整覆盖的遥测中没有额外模块、内存分配或保护修改、异常文件 / 注册表 / 网络输入和可疑命令行,那么“这两个磁盘文件被修改后直接执行新增代码”可以放到很低优先级;更值得先查原生能力的触发上下文和观测盲区。整文件哈希一致本身已包含尾部字节一致,不需要把二者当成独立加权证据。

不过,上述条件仍不证明进程从未受控,也不允许用没有某类事件排除所有漏洞。若不能确认覆盖完整,应保留“未知”,而不是把缺失项当成已证实不存在。

证据不足时,更稳妥的结论是:“已观察到合法签名进程的异常行为,但前置执行机制未确认;当前证据不足以区分原生能力滥用、漏洞利用和运行时篡改。” 不应把“未发现黑 DLL”写成“已排除恶意代码执行”,也不应把“尾部有数据”写成“已确认尾部载荷执行”。

“腾讯签名小 QQ”怎样出现

假设现场发现一个文件名和图标都像 QQ、属性页显示腾讯有效签名,但体积远小于当前完整 QQ 客户端。最合理的解释通常不是“攻击者把完整 QQ 压小后绕过了签名验证”,而是下面这类设计:

复用小型合法组件

大型软件套件通常不只有主程序,还包含启动器、更新器、崩溃上报器、卸载器、辅助工具和兼容性组件。这些文件可能只有几百 KB 或几 MB,同样由厂商签名。

攻击者可以原样复制其中一个较小的合法组件,把它重命名成看似主程序的名称,再配上诱导性的目录名、快捷方式和图标。因为文件字节没有变化,它的腾讯签名仍然有效。

原始身份:某套腾讯软件中的小型辅助组件
磁盘外观:QQ.exe / QQ安装程序.exe
签名结果:有效,签名者为腾讯
产品事实:它从来不是完整 QQ 客户端

Windows 的代码签名不绑定“这个文件必须叫什么名字”,也不保证“它必须位于哪个目录”,更不会因为文件名是 QQ.exe 就拿它和官方 QQ 主程序的体积进行比较。

利用原生加载能力

被挑选的白壳通常还具备某种可利用的加载能力,例如会在程序目录寻找依赖 DLL、读取插件、处理特定配置,或者调用同目录辅助模块。攻击者让黑组件满足最低加载条件,白壳便会主动把它带入进程。

“接口兼容”不等于要复刻整个原厂组件。对于分析人员而言,只需理解:黑组件会保留白壳启动所需的少量接口或初始化行为,再把控制流交给隐藏载荷;具体实现细节因组件而异。

外观伪装补全身份

攻击者还会利用快捷方式、父目录名称、版本资源、压缩包名称或诱导文案,把一个真实的小型腾讯组件包装成“QQ 主程序”。这属于身份伪装,不是签名破解。

最终形成的是:

合法签名白壳(原样)
+ 同目录黑 DLL / 插件
+ QQ 名称、图标、快捷方式等外观
= 看似“腾讯签名的小 QQ”

所谓“绕过签名验证”,实际往往是绕过了错误的信任决策:安全产品只验证启动 EXE 的签名,随后直接给整个进程或目录放行,没有继续检查它加载了什么、从哪里加载,以及加载后的行为是否合理。签名验证本身并没有被数学意义上绕过。

变体

DLL 搜索顺序劫持

合法程序通过名称解析依赖,攻击者控制了优先搜索位置。检测重点是白程序所在目录、缺失或异常的依赖、加载路径以及 DLL 签名。

插件与扩展滥用

合法程序明确支持加载第三方插件,但没有充分限制插件来源。它与侧加载的区别在于:插件加载是产品设计的一部分,风险来自扩展信任边界过宽。

配置驱动的后续加载

白壳先读取同目录配置、脚本或数据,再据此加载模块或发起网络请求。此时“黑”不一定直接表现为 DLL,也可能是被解释、解密或下载的内容。

白壳作为代理执行器

有些合法组件本来就能注册组件、运行安装动作或调用系统功能。攻击者传入恶意输入,让白程序代为执行。此类行为更接近系统二进制代理执行,不一定存在 DLL 侧加载。

抽象案例

一个压缩包中只有一个体积很小的 QQ.exe、一个名称普通的 DLL 和一个数据文件。检查发现:

  • QQ.exe 的完整 Authenticode 校验成功,签名者确为腾讯。
  • 文件哈希能对应到某个历史版本软件包中的辅助程序,而不是 QQ 主程序。
  • 文件名与原始名称不同,落地在用户临时目录。
  • 启动后从自身目录加载未签名 DLL。
  • 随后进程出现与该辅助程序职责无关的持久化或公网通信。

这条证据链足以说明“合法组件被重新投放并用于侧加载”。不应写成“腾讯签名被伪造”,也不能仅凭签名者名称归因于腾讯软件供应链。

检测

不把签名当白名单

合理的信任模型至少要同时考虑:

维度需要回答的问题
完整性文件摘要是否与签名匹配,而不只是包含证书
身份原始文件名、产品名、版本、哈希是否符合官方基线
位置签名厂商、产品与当前路径是否匹配
来源文件由谁下载、解压或释放,是否带网络来源标记
依赖它加载了哪些 DLL,组件签名和路径是否可信
行为当前进程的网络、持久化、凭据与注入行为是否符合职责

高价值组合

下面这些组合通常比“有效签名”更有研判价值:

  • 大厂签名的小型组件被重命名为旗舰产品主程序。
  • 合法文件出现在下载、临时、公共用户或随机目录。
  • 签名 EXE 从同目录加载未签名、低信誉或刚落地的 DLL。
  • EXE 的原始文件名、产品描述、当前文件名和父目录相互矛盾。
  • 组件版本明显陈旧,且与同目录其他文件的版本不一致。
  • 合法宿主执行了与原始职责无关的联网、键盘读取、截屏、令牌操作或持久化。
  • 内存中的模块与磁盘基线不一致,或存在没有正常模块记录的可执行私有内存。

取证顺序

  1. 对白壳执行完整签名验证,并记录摘要、签名者、时间戳和校验策略。
  2. 用哈希和版本信息确认它原本属于哪个产品、承担什么角色。
  3. 检查当前文件名、原始文件名、路径、下载来源和首次落地进程。
  4. 建立模块加载清单,重点看应用目录中的 DLL 与插件。
  5. 比较同一进程启动前后的文件、注册表、网络和内存行为。
  6. 对同目录文件整体隔离取证,不能只删除黑 DLL 或只信任白 EXE。

缓解

  • 允许规则优先绑定发布者、产品、原始文件名、版本范围和受控路径,不要只绑定签名者。
  • 对来自互联网或用户可写目录的签名文件保留行为检测,不自动提升到全信任。
  • 监控签名进程加载同目录未签名模块,建立官方安装目录和模块哈希基线。
  • 软件开发侧使用安全的 DLL 搜索策略、绝对路径、受控插件目录及组件签名校验。
  • 清理不再使用的旧组件,并阻止企业环境中不受支持版本任意运行。

ATT&CK 对照

Technique适用含义
T1574.002 DLL Side-Loading合法程序加载攻击者控制的 DLL
T1574.001 DLL Search Order Hijacking利用依赖搜索顺序抢先加载恶意 DLL
T1036 Masquerading文件名、图标、路径或产品身份伪装
T1553.002 Code Signing盗用证书、恶意签名或其他代码签名滥用;不要把所有白加黑都机械归到这里

小结

“有腾讯有效签名、体积却远小于 QQ”的文件,并不神秘:它很可能本来就是腾讯软件套件中的一个小型合法组件,只是被原样复制、改名和重新投放。攻击者没有修改白壳,所以签名仍有效;真正的恶意能力来自白壳随后加载的黑组件。

因此最重要的判断是:

有效签名验证所覆盖内容的完整性,并按策略检查签名者信任
不证明当前名称、路径、用途和运行时依赖可信

防守方必须把签名、文件身份、落地路径、模块加载和运行行为放在一起判断。