1. 为什么我要在本地复现这套恶意代码实验
在安全这个行当里摸爬滚打久了,我有个很深的体会:读一百篇恶意代码分析报告,不如亲手在虚拟机里跑一遍样本。分析报告是别人的思考结果,它告诉你"这个样本做了什么",但很难告诉你"为什么要这么做""哪一步是关键拐点""哪些行为会被安全软件拦截"。尤其是从脚本病毒到DLL注入这条技术演进路径,如果不亲手搭建环境、编译样本、观察行为,你对恶意代码的认知就始终停留在纸面上。
我这次实验的起点很朴素:想搞明白几类经典恶意代码的底层运作逻辑,包括VBS脚本病毒如何借助系统机制实现自启动和传播,传统PE文件感染为什么要操作节区表和入口点,以及DLL注入为什么能成为现代攻防对抗中的常青树。标题里的"从脚本病毒到DLL注入"其实是一条非常清晰的技术演进线——脚本类恶意代码利用的是解释器环境和系统机制,PE感染利用的是可执行文件的格式特征,DLL注入利用的是进程地址空间的共享机制。三层递进,一层比一层接近操作系统底层。
这篇文章是我整个实验过程的完整复盘。从环境搭建、样本编写、行为验证到检测对抗,每一步我都记录了原始操作和踩坑过程。适合三类人阅读:一是安全专业的学生,想找一份能落地复现的实验手册;二是刚入门恶意代码分析的从业者,想理解"样本是怎么跑起来的";三是做蓝队防御的工程师,想从攻击者视角反推检测规则应该怎么设计。整个过程只用本地虚拟机完成,全程断网,所有样本只在受控环境中运行,实验结束后直接恢复快照,安全风险完全可控。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实验环境与安全边界设计
2.1 为什么必须用双虚拟机而不是单机
恶意代码实验的第一原则是绝对隔离。我见过不少初学者图省事,直接在宿主机上装个杀毒软件就开始分析样本,这等于把自己家大门敞开让人参观。正确做法是双虚拟机结构:一台作为攻击机,一台作为受害机,两台之间通过NAT或Host-Only网络通信,宿主机不参与任何实验流量。
我的具体配置如下:
| 角色 | 操作系统 | IP规划 | 核心软件 |
|---|---|---|---|
| 攻击机 | Kali Linux 2023 | 192.168.56.101 | Metasploit、msfvenom、手工编译工具链 |
| 受害机 | Windows 7 SP1 x64 | 192.168.56.102 | 关闭UAC、关闭Windows Defender实时保护、安装Process Monitor、Process Explorer、Sysmon |
| 宿主机 | macOS / Windows均可 | 不参与实验网络 | VMware Workstation / VirtualBox |
Windows 7是恶意代码实验的黄金靶场。原因很实在:它保留了大量传统弱点,兼容性极好,进程模型和注册表机制足够经典;更重要的是,很多APT样本的攻击链本身就针对Win7设计,用它做实验能最大程度还原真实场景。新版的Windows 10/11对DLL注入、脚本执行做了大量加固,实验过程中容易触碰各种缓解机制,干扰对恶意代码本身的理解。
2.2 网络隔离与快照管理的细节
网络配置上我选了Host-Only模式。这个模式的特点是虚拟机之间可以互相通信,但虚拟机无法访问外部网络,宿主机默认也不与它们互通。相比NAT模式,Host-Only的隔离性更严格,基本杜绝了实验样本向真实网络扩散的可能。
另一个必须做的事是在每次实验前打快照。这看起来是基本功,但实战中太多人忽略。我的习惯是:每完成一个阶段的样本编写和验证,就保存一个干净快照;每次运行新样本之前,再手动把受害机回滚到上一个快照。这样即使某个样本把系统搞得面目全非,也能在几秒钟内恢复现场。实验结束后,直接删除整个虚拟机目录或关闭快照连接,样本就彻底封存了。
注意:实验过程中我全程断网,Kali和Win7之间的文件传输用共享文件夹完成。共享文件夹在传递样本后立即断开连接,避免样本反向感染攻击机。这个细节看似简单,但绝对是血的教训——我见过有人在共享文件夹里放着待分析的样本,结果受害机上的病毒顺着共享通道把攻击机也感染了。
2.3 实验样本的伦理边界
这里必须说清楚一件事:理解恶意代码的机制不等于制作恶意软件。我的实验样本全部是教学性质的概念验证代码,不具备真实传播能力和实际破坏效果,而且只在两台虚拟机的封闭环境内运行。整个实验的最终目的是掌握"如何识别和分析"恶意代码,而不是"如何制作和投放"恶意代码。如果你打算复现这套实验,请务必遵守同样的边界。
DLL注入、脚本病毒这些技术本身是双刃剑。安全软件要用它们做沙箱检测,EDR要用它们做行为监控,红队要用它们做渗透测试。技术本身没有善恶,使用目的决定性质。我的所有样本在实验结束后都被销毁,虚拟机恢复到初始快照,不保留任何可执行文件。
3. 脚本病毒实验:从VBS宏到自启动传播链
3.1 为什么从脚本病毒开始而不是直接上PE
先说结论:脚本病毒是理解恶意代码运行机制的性价比之王。它不需要编译,直接用系统自带的解释器运行,代码意图一目了然,且大量滥用Windows的系统机制(开机启动、文件关联、进程执行),这些机制同样被高级恶意代码使用。理解了脚本病毒,再去看PE感染和DLL注入,很多概念都是相通的。
拿VBS脚本病毒举例。VBS全称VBScript,是Windows系统自带的脚本语言,由wscript.exe或cscript.exe解释执行。一个最简单的VBS病毒通常包含两个核心部分:自我复制逻辑和传播/驻留逻辑。
自我复制的常见做法是读取自身文件内容,然后写入目标位置。VBS里用Scripting.FileSystemObject对象操作文件系统,核心代码大概是这样:
vbscript复制' 获取当前脚本的完整路径
strSelfPath = WScript.ScriptFullName
' 创建文件系统对象
Set fso = CreateObject("Scripting.FileSystemObject")
' 将自身复制到启动目录
Set objShell = CreateObject("WScript.Shell")
strStartup = objShell.SpecialFolders("Startup")
fso.CopyFile strSelfPath, strStartup & "\svchost.vbs", True
这段代码看起来简单,但它触及了VBS恶意代码的两个关键机制:一是通过WScript.ScriptFullName获取自身路径完成自复制,二是通过SpecialFolders("Startup")定位启动目录实现开机自启。启动目录是在用户登录时自动执行其中的程序,这是最经典的驻留手段之一。
3.2 完整的传播链设计:U盘感染加文件感染
为了让实验更接近真实场景,我给脚本病毒加上了传播能力,完整链路是:受害者插入U盘 → 病毒将自身副本写入U盘根目录 → 修改U盘文件夹选项 → 当U盘在其他电脑上打开时自动运行。
U盘传播的脆弱点是自动播放功能。虽然新版Windows默认关闭了AutoRun,但Win7时代很多用户会开启"自动播放所有媒体和设备"。病毒利用这一点,在U盘根目录写入两个文件:一个是autorun.inf,一个是病毒本体copy.vbs。autorun.inf的内容就几行:
ini复制[AutoRun]
shell\open\command = wscript.exe copy.vbs
action=打开文件夹
这里有个细节值得琢磨:[AutoRun]节下的shell\open\command定义了当用户双击U盘图标时执行的命令。正常情况是打开资源管理器,恶意情况下就会执行wscript.exe copy.vbs。之所以不直接写copy.vbs而要写wscript.exe copy.vbs,是因为VBS脚本必须由解释器执行,直接双击效果一样,但显式声明wscript.exe能让脚本以无窗口方式运行,隐蔽性更好。
文件感染部分我用了更传统的方式——VBS脚本注入到HTML文件里。思路是:扫描当前目录下所有.htm和.html文件,在文件头部插入一段VBS脚本,当用户用IE打开这些HTML文件时,脚本随页面一起执行,实现二次传播。HTML文件中嵌入VBS脚本是IE时代的经典手法,虽然现代浏览器默认禁用ActiveX脚本,但在内网老旧系统上依然有生存空间。
3.3 受害机上的行为观察记录
样本在受害机上运行后,我用Process Monitor做了行为追踪,记录到如下关键行为链:
wscript.exe进程启动,读取了C:\Users\test\Desktop\payload.vbs- 创建
C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\svchost.vbs,启动目录写入成功 - 在U盘根目录(实验中用D盘模拟)创建
autorun.inf和copy.vbs - 枚举D盘根目录下的
.html文件,逐一在文件头部添加脚本代码 - 修改注册表
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run,新增一项WindowsUpdate指向脚本路径,实现第二层驻留
启动目录和注册表Run键是脚本病毒最常使用的两条驻留路径。启动目录的优点是实现简单、对所有用户可见,缺点是容易被发现;注册表Run键的隐蔽性稍好,但对权限有要求,普通用户账户写入HKCU的Run键没有问题,但要写HKLM的Run键就需要管理员权限。
实验过程中我特意关闭了Win7的UAC和Windows Defender,这是为了还原"无防御环境下的恶意代码行为基线"。有了基线数据后续做检测实验才能对比——如果一上来就是防御全开,样本根本跑不起来,也就看不到完整的行为链了。
3.4 脚本病毒的检测思路随手记
做完行为观察后,我顺手验证了Sysmon对脚本病毒的检测覆盖情况。Sysmon是微软官方的一款系统监控工具,通过配置XML规则记录进程创建、网络连接、文件创建等事件。
针对这个VBS样本,我配置的Sysmon规则重点监控两个事件ID:事件ID 1(进程创建)和事件ID 11(文件创建)。当wscript.exe进程创建时,Sysmon会记录进程命令行,这样就能看到wscript.exe copy.vbs这样的可疑命令行;当文件在启动目录被创建时,事件ID 11会记录文件路径和创建进程的PID,把两个事件关联起来,就能形成一条完整的攻击链线索。
实际操作中,Sysmon的XML配置会让不少新手抓狂,因为规则写得不严谨就会漏报或者噪声爆炸。我踩过的坑是:只写match="exclude"的排除规则,忘了加include规则,导致Sysmon把所有进程事件都记录下来了,日志量暴涨,几小时就写满磁盘。正确做法是先配置include白名单,只监控wscript.exe、cscript.exe、powershell.exe这几个高危解释器,再排除系统正常进程,这样日志量可控,告警也更准确。
4. PE文件感染实验:手工植入代码到节区
4.1 从脚本代码到二进制代码的思维转变
脚本病毒实验做完,我明显感觉到一个天花板:VBS代码再精巧,也是解释执行,行为模式容易被安全软件的模式库识别。真正的恶意代码进阶是转向编译型二进制,也就是PE文件感染技术。这背后的技术核心是修改PE文件结构,把恶意代码塞进宿主程序,让宿主程序在运行时先执行恶意代码,再跳回正常逻辑,用户表面上看到的程序行为没有变化。
为什么需要这项技术?因为现代杀毒软件对未知的可执行文件会做行为分析。如果恶意代码以独立文件形式存在,运行时会触发行为监控;但把它注入到一个用户信任的合法程序(比如notepad.exe)里,行为监控会认为"notepad.exe做了某些操作",降低告警优先级。这就是为什么PE感染技术至今仍是恶意代码分析中的必修课。
PE文件的结构可以粗略理解为三部分:DOS头 + PE头 + 节区(Section)。DOS头里最关键是e_lfanew字段,它记录了PE头的偏移地址;PE头里包含文件类型、编译时间戳、节区数量等信息;节区则是真正存放代码和数据的地方,常见的节区有.text(代码)、.data(数据)、.rsrc(资源)。
PE感染的经典思路是新增一个节区,把恶意代码放到新节区里,然后修改PE头中的入口点(AddressOfEntryPoint),让它先跳转到恶意代码,恶意代码执行完后再跳回原始入口点。整个过程等于给宿主程序加了一段"开场白"。
4.2 感染流程的分步拆解
这部分我用的实验对象是Windows自带的notepad.exe(计算器、画图等小工具也行),宿主文件在受害机上远程复制到实验目录,保证原始文件损坏了也不会影响系统。感染过程分五步:
第一步,解析PE头结构。 我写了一个小工具读取notepad.exe的DOS头和PE头。关键信息是:DOS头中e_lfanew字段值(指向PE头的偏移)、PE头中NumberOfSections字段(当前节区数量)、每个节区的SizeOfRawData(原始数据大小)和PointerToRawData(文件中的偏移地址)。
-
第二步,确定新增节区的位置和大小。 感染需要额外的空间存放恶意代码。我选择在文件末尾追加一个新节区,节区名可以伪装成
.vmp0或.upx0,模仿加壳软件的节区命名,降低审计者的怀疑。新节区的VirtualAddress从上一个节区按内存对齐后计算,SizeOfRawData设置为恶意代码大小(按文件对齐),Characteristics字段设置为0xE0000020,代表可读、可写、可执行。 -
第三步,写入恶意代码。 恶意代码本身是用C语言写的一段独立函数,功能是弹出一个消息框提示"感染成功",然后返回0。编译的时候不链接标准库,直接用Windows API的裸汇编实现,这样生成的机器码很小,大约200字节左右。实际恶意代码不可能这么简单,通常会从其他进程读取shellcode或恶意DLL,但教学场景下以最小可用为准。
-
第四步,修改PE头中的入口点。 原始的
AddressOfEntryPoint指向notepad.exe的正常入口,我把它改成指向新节区的虚拟地址。计算方法是:新入口点VA = 新节区VirtualAddress + 恶意代码在节区内的偏移。这一步是整个感染的关键拐点,改错一个字节程序就无法启动。 -
第五步,将所有修改写回文件。 用二进制方式打开notepad.exe,按照偏移量逐段修改,保存为notepad_infected.exe。这里有个注意事项:修改前必须备份原始文件,并且写回时要用
CreateFile的OPEN_EXISTING模式而非CREATE_ALWAYS,否则文件被覆盖后不可恢复。
4.3 感染后程序的运行时表现
我双击运行了感染后的notepad_infected.exe,观察到的现象很有意思:先弹出一个"感染成功"的消息框,点击确定后notepad.exe才正常打开空白文档。这说明两个关键行为都实现了:恶意代码在入口点被率先执行,执行完后成功跳回了原始入口点,宿主程序功能不受影响。
用Process Explorer观察进程行为时,我发现notepad_infected.exe加载的DLL列表和原始notepad.exe几乎没有区别。这正是PE感染隐蔽性的来源——从用户视角看,程序能正常打开,和平时用的一样;从杀毒软件视角看,进程签名(数字签名、文件哈希)虽然改变了,但加载模块和行为模式没有异常。
不过PE感染也有一个明显的弱点:文件哈希会变化,且文件大小会增加。 在蓝队视角下,只要维护关键可执行文件的哈希基线,任何未知哈希的notepad.exe出现都会触发告警。这也是现在很多EDR产品做"文件名+路径+哈希"三重校验的原因。
4.4 从感染者视角看防御方的检测盲区
做这轮实验时我反复问自己一个问题:如果防御方只做了哈希校验,我的感染样本能被发现吗?答案是能,因为文件大小从原始的153KB膨胀到155KB,哈希彻底变了。但如果是高级攻击者,会选择"往空白区写入"——不新增节区,而是利用已有节区中的零填充区(通常存在于.text节区和.data节区之间的对齐区域)嵌入恶意代码,文件大小不变,哈希虽然也变,但文件头部section数量不变,静态特征更隐蔽。这种"空腔感染"技术在真实攻击中非常常见,是PE研究的进阶方向。
蓝队的应对思路也对应着升级:不能只看哈希和大小,还要做代码完整性校验。比如用Get-AuthenticodeSignature检查数字签名是否有效,或者用dumpbin /headers检查节区数量是否异常,再或者直接执行一次"入口点偏移是否指向节区头部"的检查。这些检测手段在EDR里都属于静态扫描的基础能力,但在纯手工分析场景中依然有效。
5. DLL注入实验:进程地址空间的"偷渡"
5.1 为什么恶意代码要选择DLL注入而不是直接运行EXE
DLL注入在恶意代码演进史中的地位很特殊。它不修改任何宿主文件,而是把恶意代码以DLL形式"借宿"在合法进程的地址空间中执行。这样做的好处非常明显:
一是借壳运行,进程凭据和信任级别都是宿主进程的。如果把恶意代码注入到svchost.exe(服务宿主进程)里,Windows会认为这是系统服务在运行,安全软件不太容易直接拦截。
二是逃避独立进程检测。安全软件通常会监控"新出现的可执行文件运行"行为,但DLL注入不需要创建新进程,恶意代码在一个已经被系统信任的进程里执行,检测难度陡增。
三是方便做持久化和提权。注入系统进程后,恶意代码可以访问宿主进程的权限范围,经常被用来做权限维持或横向渗透。
我在实验里复现了三种经典注入方式:CreateRemoteThread注入、SetWindowsHookEx注入 和 AppInit_DLLs注入。其中CreateRemoteThread是最基础、也最能说明注入原理的一种。
5.2 CreateRemoteThread注入的完整代码实现
CreateRemoteThread注入的思路分四步:
- 用
OpenProcess打开目标进程,获取进程句柄。 - 在目标进程的地址空间中用
VirtualAllocEx分配一块内存,用来存放"DLL路径字符串"。 - 用
WriteProcessMemory把DLL的绝对路径写入这块内存。 - 用
CreateRemoteThread在目标进程中创建一个线程,线程的入口点指向LoadLibraryA函数,参数是指向上述内存地址的指针。
关键代码片段如下(C语言):
c复制BOOL InjectDll(DWORD dwProcessId, LPCSTR szDllPath) {
HANDLE hProcess = NULL;
LPVOID pRemoteMem = NULL;
FARPROC pLoadLibrary = NULL;
HANDLE hThread = NULL;
// 打开目标进程
hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, dwProcessId);
if (hProcess == NULL) return FALSE;
// 在目标进程内分配空间
pRemoteMem = VirtualAllocEx(hProcess, NULL, strlen(szDllPath) + 1,
MEM_COMMIT, PAGE_READWRITE);
if (pRemoteMem == NULL) return FALSE;
// 写入DLL路径
WriteProcessMemory(hProcess, pRemoteMem, szDllPath, strlen(szDllPath) + 1, NULL);
// 获取LoadLibraryA在kernel32.dll中的地址
pLoadLibrary = GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
// 创建远程线程执行LoadLibrary
hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLibrary, pRemoteMem, 0, NULL);
if (hThread == NULL) return FALSE;
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
这里有个关键原理必须理解透彻:为什么GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA")拿到的函数地址在目标进程中同样有效?
因为Windows的进程模型里,kernel32.dll和ntdll.dll这种系统核心DLL在每个进程中的加载基址都是相同的。操作系统在创建进程时,会优先将这些核心DLL映射到每个进程的固定地址。所以我在当前进程里查到的LoadLibraryA地址,和目标进程里的地址完全一致,可以直接作为远程线程的入口点。这也是CreateRemoteThread注入能成立的前提。
5.3 注入之后的验证:凭什么确认注入成功
注入完成后,怎么确认DLL真的被目标进程加载了?我在实验里用了三种验证方法:
第一种是Process Explorer直接看模块列表。 打开Process Explorer,找到目标进程(我用的是notepad.exe),查看Modules选项卡,如果出现inject_demo.dll,说明注入成功。这是最直观的方式,也最容易被安全人员发现。真实攻击中的DLL通常会借用系统DLL的名字或合法软件DLL的名字做伪装,比如version.dll或winmm.dll,不细看根本不会注意到。
第二种是调用LoadLibrary后观察行为。 我写的实验DLL在DllMain里只做了一件事:创建一个文本文件C:\injected.txt,写入一行"注入成功"和时间戳。当notepad.exe进程中出现这个文件时,就证明DLL被成功加载并执行了初始化代码。
第三种是用API Monitor捕捉LoadLibrary调用。 API Monitor是一款Windows API调用监控工具,能实时显示进程调用的API序列。在注入的瞬间,API Monitor会记录到notepad.exe调用了LoadLibraryA("C:\inject_demo.dll"),这个调用记录是注入行为最权威的证据。
5.4 另外两种注入方式的对比与差异
我另外复现了SetWindowsHookEx注入和AppInit_DLLs注入,拿来做对比实验。
SetWindowsHookEx注入的原理是利用Windows消息钩子。进程在接收键盘、鼠标等消息时,会加载与该消息关联的钩子DLL。攻击者调用SetWindowsHookEx(WH_KEYBOARD, ...)注册一个全局键盘钩子,当系统任何进程触发键盘消息时,钩子DLL就会被加载到该进程中。这种方式的优点是无需目标进程的句柄,全局生效;缺点是依赖消息触发,调度延迟高,且杀软对全局钩子的监控很成熟。
AppInit_DLLs注入的原理更底层:修改注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs,指定一个DLL路径。当任何进程加载user32.dll时,系统会按注册表配置自动加载AppInit_DLLs指定的DLL。这是三种方式中最"批量"的,一次设置对所有GUI进程生效,但也最容易被安全软件检测,因为注册表键值太敏感了。
对比结果如下表:
| 注入方式 | 触发机制 | 隐蔽性 | 实现难度 | 检测难度 |
|---|---|---|---|---|
| CreateRemoteThread | 手动创建远程线程 | 中 | 中 | 低 |
| SetWindowsHookEx | 全局消息钩子 | 中高 | 低 | 中 |
| AppInit_DLLs | 注册表配置自动加载 | 低 | 极低 | 低 |
三种方式对照着做一遍,你会对"进程间代码注入"的理解从"听说过"变成"真的会了",也能在分析真实样本时更快地判断攻击者用的是什么注入手法。
5.5 防御方如何发现DLL注入
做完注入实验后,我把视角切回蓝队,验证了几种检测手段的实际效果。
最有效的是查看进程加载的模块列表。 正常进程加载的DLL路径、签名、公司名都是可解释的。比如notepad.exe加载的comctl32.dll是微软官方签名的系统组件,但突然出现一个C:\Users\test\inject_demo.dll,路径在用户目录,签名不存在,这几乎可以百分百确定是注入。我在Process Explorer里一眼就看到了这个异常DLL,说明只要有人愿意看一眼模块列表,CreateRemoteThread注入的隐蔽性就大打折扣。
其次是监控API调用序列。 Sysmon虽然没有直接监控VirtualAllocEx/WriteProcessMemory这类注入原始API,但可以通过事件ID 8(CreateRemoteThread调用)直接捕获远程线程创建行为。我在实验机器上开启Sysmon的事件ID 8监控,在注入发生时立即捕获到了告警,事件记录包括源进程(注入器)、目标进程(notepad.exe)以及远程线程的起始地址。
最后是行为基线。 真正难发现的是那种"注入到系统进程后不暴露明显行为"的攻击。比如注入到svchost.exe里,DLL加载后只是静默等待,不做任何文件操作。这种情况下,只有基于行为的异常检测才能有效——比如svchost.exe的CPU占用率异常飙升、可疑的网络连接、不正常的注册表访问等。这类检测通常由EDR完成,单靠人工分析很难覆盖。
6. 把三种实验串联起来:攻击链视角的复盘
6.1 从脚本到PE到DLL的演进逻辑
单独做完三个实验后,我把它们串起来重新审视了一眼。为什么恶意代码技术的演进路径是"脚本病毒→PE感染→DLL注入"?这背后其实是攻击者需求的进化。
最早期攻击者的核心需求是"能跑起来"。脚本病毒零成本、零编译,只要系统有解释器就能运行,所以VBS、JS、HTA成了首选。但脚本代码的行为特征太明显,杀软一套特征库就能识别,于是攻击者开始学习把代码编译成PE。
第二阶段的核心需求是"隐蔽地跑"。PE感染通过修改宿主程序,把攻击代码伪装成正常软件的一部分。文件哈希变了没关系,关键是行为模式更接近正常程序,能躲过一波静态检测。但PE感染需要掌握PE格式的底层知识,技术门槛高,且文件会被安全软件重点检查。
第三阶段的核心需求是"借壳跑"。DLL注入把恶意代码注入到系统可信进程中,不修改任何宿主文件,不创建新进程,整个过程在内存中完成。这对防御方的检测能力提出了极高的要求——静态查杀和文件监控全部失效,只能依赖运行时行为分析。这就是为什么现代EDR都在强调"行为检测引擎"和"内存扫描"。
6.2 三种技术在同一攻击链中的组合使用
真实攻击从来不会只使用单一技术。我复盘了一个近期分析过的样本,它的攻击链是这样的:
第一阶段:投递。 攻击者发送一封带恶意宏的Office文档,宏用VBS脚本编写。文档一打开,宏启动PowerShell下载下一阶段载荷。这里用的是脚本病毒技术。
第二阶段:落地。 PowerShell下载的是一个文件捆绑器,它会释放一个恶意DLL和一个合法的便携软件(比如绿色版压缩工具),然后用PE感染的方式把DLL"拼接"到合法软件的节区里。用户运行时,压缩工具正常打开,但其中已经被植入了恶意代码。这里用的是PE感染技术。
第三阶段:注入。 压缩工具运行后,恶意代码执行DLL注入,把自己注入到系统进程explorer.exe中。explorer.exe是Windows外壳进程,加载DLL不会引起怀疑。注入成功后,恶意代码获得用户级权限的稳定驻留,后续再考虑提权或横向移动。这里用的是DLL注入技术。
把这三段拼接起来,你会发现每一段都对应着我前面的实验。单独看每段技术都不算高深,但组合起来就形成了一条难以追踪的完整攻击链。这也是我在复盘中最深的体会:防御方不能只盯着某个单一技术,要从攻击链角度做纵深防御。
6.3 基于这个攻击链防御方该怎么布防
对照这条攻击链,我在实验环境中验证了几条防御策略:
针对脚本阶段的防御: 限制Office宏执行、对PowerShell启用Constrained Language Mode、对wscript.exe/cscript.exe做AppLocker白名单控制。这些措施能把脚本类攻击挡在第一层。实验环境里我分别测试了这些策略,Windows PowerShell的CLM模式能直接阻断大多数VBS掉起PowerShell的调用链,效果立竿见影。
针对PE感染阶段的防御: 维护关键软件哈希基线、启用WDAC(Windows Defender Application Control)强制签名校验、对高危目录(用户下载目录、应用数据目录)设置实时扫描。PE感染样本在静态扫描下特征明显,检测率很高。
针对DLL注入阶段的防御: 开启Sysmon事件ID 8监控、部署EDR做运行时行为分析、对敏感进程(lsass.exe、explorer.exe)设置额外保护。这一层的防御成本最高,但也是攻击链中最关键的拦截点——一旦DLL注入成功,后面的信息窃取动作就很难拦截了。
三条防线互为补充,任何单一防线被绕过,后续防线还能兜底。这也是我对"纵深防御"的一次真实落地验证。
7. 实验踩坑记录与复现建议
7.1 实验环境相关的坑
坑一:共享文件夹把病毒传给了攻击机。 我前面提到过一次,这里详细说。第一次做VBS病毒实验时,我图方便一直在共享文件夹里改代码,样本直接在共享目录里运行了。结果病毒在执行自我复制时把自己写到了共享文件夹的多个位置,然后宿主机同步到攻击机目录。虽然VBS在Linux下不会执行,但样本文件已经发生了"逃逸"。从那以后我严格规定:共享文件夹只能用于"单向传递一次",传入受害机后立刻断开。
坑二:快照忘打导致环境脏了。 有一次做DLL注入实验前忘了打快照,结果注入的DLL和样本导致Win7系统重启后出现异常弹窗。虽然不影响整体实验,但后续做行为监控时噪声数据很多,干扰了判断。现在我的习惯是每次实验必打快照,实验之后必回滚。
坑三:杀毒软件自动更新导致样本被清。 Win7虚拟机我虽然关闭了Defender实时保护,但没有断网时它会自动更新并启用防护,直接把我放在桌面的样本删了。后来我设置了系统更新为"绝不自动下载",并在Host-Only网络下彻底断开了外网连接。
7.2 代码编写相关的坑
坑一:CreateRemoteThread的线程函数类型。 第一次写DLL注入代码时,我把LoadLibraryA的地址强制转换成了LPTHREAD_START_ROUTINE类型。这本身没问题,但有个隐蔽的坑:在64位系统上,线程函数必须接受一个LPVOID参数并返回DWORD,LoadLibraryA完全符合这个签名。但在32位系统上,某些编译器对函数指针的转换会报错,需要显式做指针转换。我跨了一个编译环境测试时踩了这个坑,后来统一改成(LPTHREAD_START_ROUTINE)GetProcAddress(...)就正常了。
坑二:PE头解析的字节对齐。 做PE感染实验时,我读SizeOfRawData时用错了字节序,导致新节区偏移计算错误,直接生成的文件根本无法运行。PE头中的字段都是小端序存储,解析时如果用大端方式读取,所有数值都会反转。排查了很久才发现是读文件的字节序处理问题。建议初学者直接用成熟工具(如pefile库)解析PE头,不要自己从零写字节流解析。
坑三:VBS脚本的编码问题。 VBS脚本文件用记事本另存为时,如果保存为UTF-8编码,字符串中的中文注释会变成乱码,甚至可能导致脚本语法错误。VBS默认的ANSI编码和UTF-8不兼容。我测试时用wscript.exe运行一个UTF-8编码的VBS脚本,直接弹语法错误。正确做法是用ANSI编码保存脚本,或干脆全程用英文写注释,减少编码焦虑。
7.3 给想复现的读者的建议清单
如果你准备按这套实验复现,我按优先级整理了一份清单供参考:
- 用VMware Workstation,VirtualBox的Host-Only网络偶尔会出现DNS解析问题,实验时会多花时间排除
- Win7 SP1虚拟机务必打齐补丁,否则可能无法安装Sysmon(依赖.NET Framework 4.6.1)
- 所有样本放D盘专用目录,C盘系统目录别碰,系统文件保护机制会干扰实验
- 每次实验前后检查
netstat -ano,确认虚拟机上没有意外建立的连接 - 实验结束后用快照回滚,而不是手动删文件,很多注册表残留是删不干净的
复盘到这里,核心的实验过程和原理都讲完了。最后说一点我个人的体会:恶意代码分析这门手艺,最值钱的部分不是会读汇编、会看OD,而是"能站在攻击者角度想问题"的思维方式。当你亲手把一段代码"注入"进别人的进程,你才会真正理解为什么EDR要把进程模块列表放在那么显眼的位置;当你亲手把一个PE文件"感染"掉,你才会真正理解哈希校验和完整性检测的意义。防御的本质是理解攻击,这份理解必须靠动手实验去换,光看不练是不行的。
