最近把《恶意代码分析实战》第14章配套的 Labs 完整过了一遍。这一章的主题是恶意代码的网络特征,实验做下来比前几章都要过瘾。之前我一直把精力放在静态分析上,一遇到动态抓包就有点心虚,总觉得流量这东西抓是抓到了,但不知道该怎么“读懂”。这次借着 Labs-14,我把“会联网的样本”从头到脚扒了个干净,整个过程补齐了我在网络行为分析上的很多短板。
如果你正在学恶意代码分析,或者在安全运营、蓝队应急里负责样本分析,这章实验非常值得认真做。大部分恶意代码最终都要通过网络才能完成命令与控制、数据回传和更新,所以网络侧的分析能力直接决定了你能不能把一个样本的完整行为讲清楚。走完 Labs-14,你掌握的不只是某一个样本的行为,更是一整套分析套路:先从静态线索定位网络行为,再用虚拟环境动态抓包还原网络交互,最后提取 IOC 并落地成检测规则。下面我把这次实验的过程、思路和踩过的坑完整写出来。
1. 先把实验环境搭成一个“小黑房”
1.1 虚拟机和快照策略
做网络行为分析,最忌把样本放到真实生产网络里跑。我的做法是在虚拟机里搭一套完全隔离的“小黑房”。实验机用的是 Windows 7 32 位虚拟机,分配了 1 核 2G 内存,装完系统后不打任何真实业务补丁,把所有共享文件夹、拖拽、剪贴板共享全部关掉,然后打一个干净快照。
为什么选 Windows 7 32 位?因为第 14 章的实验样本大多是 32 位 PE 文件,这类样本在 32 位系统上兼容性最好,执行环境越接近样本的“预期”,越容易触发完整行为。当然如果你要分析 64 位样本,就得准备 64 位虚拟机,但原理是一样的。
快照这个动作特别重要。每跑完一个样本,不管结果是好是坏,一定要恢复到干净快照再跑下一个。否则前一个样本留下的注册表自启动项、服务、释放文件都会污染后一个样本的运行环境,最后分析出来的结果全是脏数据。我现在养成了铁律:动态分析前必须从干净快照启动,分析完直接回滚,绝不在同一个系统状态下连续跑两个样本。
1.2 把流量收口:ApateDNS 和 INetSim 的配合
接着是网络侧的准备。分析主控机我用 Kali Linux,开着 Wireshark 抓包。Windows 虚拟机配置静态 IP,比如 192.168.1.100,网关指向 Kali 的 IP 192.168.1.1。这样所有流量都会经过 Kali,Wireshark 抓的就是两台机器之间的完整通信。
但光抓包还不够。恶意样本通常会尝试连接某个域名或 IP,如果域名解析到了真实公网,请求就会发到互联网上,这既不可控也不安全。而且公网上的 DNS 状态、服务器响应千变万化,会严重干扰分析结论。所以我一般在 Windows 虚拟机里装 ApateDNS,把所有 DNS 请求固定解析到 Kali 的 IP 上。Kali 里再启动 INetSim,它会模拟 HTTP、HTTPS、SMTP、FTP、DNS 等常见服务,让样本误以为自己真的连上了服务器,从而继续往下执行。
这套组合的好处是:所有请求和响应都停在内网里,你可以随时返回伪造的响应内容,观察样本在不同服务端反馈下的行为变化。比如你用 INetSim 返回一个空页面,样本可能继续等;返回一个带特定字段的页面,样本可能进入下载下一步模块的流程。这种可控性是真实互联网给不了的。
1.3 辅助工具别凑合
除了网络工具,进程和注册表行为也得盯着。我常用的辅助工具包括 Process Monitor,用来记录文件、注册表、网络、进程创建事件;Regshot,用来对比样本运行前后的注册表变化;以及 PE 查看工具,比如 DIE 和 CFF Explorer,用来做静态定位。
这一套环境搭好之后,我建议先做一个自检:在 Windows 里执行 nslookup,把 DNS 指向 Kali 的 IP,解析一个测试域名,确认 ApateDNS 能正常响应;再用 curl 访问 192.168.1.1 的 80 端口,确认 INetSim 的 HTTP 服务有返回。这个自检最多两分钟,但能省掉后面大量“为什么样本不出流量”的排查时间。
2. 静态分析:拿到样本先别双击,看它想联网的痕迹
2.1 查壳和基础信息
拿到样本后我不急着双击,先把它丢进 DIE 里看一遍。Dec 或 Detect It Easy 能识别编译器、加壳器、区段信息。第 14 章的样本大部分没有加壳,但有几道题会故意用 UPX 壳练手。如果入口点落在一个可执行区段,或者区段名是 UPX0、UPX1,说明加壳了,得先脱壳再继续。
为什么要先脱壳?因为壳会把真正的导入表和字符串藏起来,你不脱壳看到的导入函数全是壳自身的,对分析没有意义。脱壳可以用工具,也可以手脱,但咱们做分析实验,重点是行为而不是脱壳技巧,所以只要能把内存里还原的 PE dump 出来就够了。
基础信息里还有一个容易被忽略的点:编译时间戳。虽然这个字段可以被伪造,但有时候能给你提供线索。比如一个编译时间写着 2003 年的样本,却用了现代 API 和加密库,这本身就说明作者故意改了时间戳,背后可能有反分析意识。
2.2 导入表不会说谎:WinINet 与 WS2_32 的预兆
接下来是静态分析的重头戏:打开导入表,优先找网络相关的函数。这里我会分两类看:
一类是 WinINet / WinHTTP 系列函数,比如 InternetOpen、InternetConnect、HttpOpenRequest、HttpSendRequest。出现这类函数,说明样本大概率走 HTTP 协议做通信,常见于下载器、C2 木马、信息窃取木马。它会把一部分行为藏进 URL 和请求头里。
另一类是 WinSock / WS2_32 系列函数,比如 socket、connect、send、recv、select。如果样本导入表只有这些网络函数,说明它不走 HTTP,而是使用自定义的 TCP 或 UDP 协议,这种通信比 HTTP 更隐蔽,分析难度也更高。
以 Lab14-1 为例,我静态看到它导入了 InternetOpenA、InternetConnectA、HttpOpenRequestA、HttpSendRequestA,字符串里还有 “POST” 和 “/login.php”。到这时候基本能断定:它一定会发起 HTTP POST 请求,并且会有内容回传。至于回传什么,就要靠动态抓包来验证。
再看 Lab14-2,导入表完全不同,主要是一堆 socket、send、recv,字符串里也没有明显的 URL。说明这个样本走的是原始 TCP 或 UDP,甚至可能是 DNS 隧道。这样我在动态阶段就会重点观察非 80/443 的端口,以及 DNS 请求的异常形态。
2.3 字符串里藏着的“明文线索”
字符串提取不用太复杂,用 strings 扫一遍,再过滤出 http、https、ftp、.dll、.php 这些关键字就够用了。除了 URL,我还会找 User-Agent、Content-Type、User-Agent 后面的版本号,以及一些看起来像加密密钥的固定字符串。
这里有一个重要提醒:不要因为静态字符串里没看到 URL 就认为样本不联网。有的样本会把 URL 分成多段存储在数据段里,运行时再拼接;也有的样本把域名做了 base64 编码或异或处理,静态看起来就是乱码。所以字符串分析只能作为线索,不能作为结论。
我习惯把静态看到的线索列成一个表:
- 可能的协议方向:HTTP 还是原始 TCP
- 可能的目标域名或 IP
- 可能用到的 URI 路径
- 可疑的 User-Agent
- 是否加壳、是否有反调试
有了这个表,动态阶段就有了明确的观察目标。比如看到一个可疑域名,我在 Wireshark 里就会专门过滤 dns.qry.name == 那个域名;看到一个 POST 路径,就盯着 HTTP 请求里是否有同样的路径。这个习惯能大大提升效率。
3. 动态分析:让流量自己说真话
3.1 每次运行前的固定检查
动态分析看起来简单,其实最容易翻车。我现在每次运行样本前都会按固定清单走一遍:
- 把 Windows 恢复到干净快照;
- 确认静态 IP 能 ping 通 Kali;
- 在 Windows 里启动 ApateDNS,DNS 指向 Kali 的 IP;
- 在 Kali 里启动 INetSim;
- 在 Kali 里启动 Wireshark,过滤条件设为 ip.addr == 192.168.1.100;
- 在 Windows 里启动 Process Monitor,过滤进程名为样本名;
- 双击样本,开始计时观察。
样本运行后不要急着关掉,很多恶意代码会等一段时间再发起通信。我见过有的样本延迟几分钟才开始连 C2,也有的样本会在特定系统时间触发行为。所以至少要观察 2 到 3 分钟,如果一直没有流量,再考虑是不是有反虚拟环境或延迟逻辑。
3.2 HTTP 样本的表与里
Lab14-1 运行后,Wireshark 很快出现了三组包:
- DNS 查询:域名解析到 192.168.1.1,这是 ApateDNS 伪造的响应;
- TCP 三次握手到 192.168.1.1 的 80 端口;
- HTTP 请求:POST /login.php HTTP/1.1,Host 是那个可疑域名,User-Agent 是一个伪造的浏览器标识。
跟着 HTTP 流看请求体,能明显看到一段十六进制数据。我把这段数据复制出来,用 CyberChef 的 From Hex 解了一下,发现里面包含了计算机名、用户名和部分注册表信息。也就是说,这个样本首先会收集主机信息,然后通过 HTTP POST 外传。
到这里外传行为已经确认,但网络分析不能只停在“它发了什么”。我回头对照 Process Monitor,看到样本创建了一个互斥体,写入了注册表 Run 键,还在 Temp 目录释放了一个文件。这说明网络行为只是它整个生命周期的一环,持久化和释放文件也同样重要。把网络和行为链结合起来,才能对一个样本给出完整判断。
3.3 DNS 隧道怎么识别
Lab14-2 的过程让我印象最深。静态阶段没看到 URL,动态跑起来后,Wireshark 里几乎看不到 80/443 流量,反而是一连串 DNS 查询。用过滤器 dns.flags.response == 0 只看请求,能看到类似这样的域名:aa2f1.example-c2-lab.com、b3d9c.example-c2-lab.com、cf7aa.example-c2-lab.com。
这种域名结构非常可疑:同一个根域名、二级域名各不相同、每段子域名字符集很散、长度基本一致。正常程序的 DNS 请求不会这么规律,域名更不会带这种随机字符。我把这些 qname 拼接起来,去掉根域名后剩下的字符用 base32 尝试解码,结果解出了一段类似指令的内容。这就是典型的 DNS 隧道:数据被打进 DNS 查询的子域里传输。
识别 DNS 隧道不能只看“全是有 DNS 请求”。我总结的区分要点有三个:
- 请求的域名是否集中在同一个根域名下;
- 子域名长度是否固定、字符是否随机;
- 请求频率和内容是否明显超出正常程序解析域名的需要。
如果三条都满足,基本可以判定是隐蔽信道。
3.4 加密流量要在内存里找答案
Lab14-3 则展示了另一种情况:静态只有 socket、send、recv,动态连接的是一个不常见的高端口 1337。Follow TCP Stream 之后,载荷开头几个字节像固定标志,后面跟着长度字段,再往后全是看不出含义的字节。
遇到这种加密或编码流量,我通常的做法是回到调试器,在 recv 函数返回值上下断点,然后查看接收缓冲区的内容。这一步很关键:网络包里看到的是密文,内存里经过函数处理后的缓冲区可能就是明文。对比两者的差异,就能判断它到底是简单异或、base64 变种,还是用了 AES 这类强加密。
Lab14-3 后来被我测出来是对每个字节异或 0x2A,密钥是固定写死在程序里的。这种“玩具级”加密在真实恶意代码里也不少见,尤其是很多样本作者为了省事直接用了可逆编码。分析的时候不要一看到流量不像 HTTP 就往高级加密上想,先试 Magic 和最简单的基础编码,往往有意外收获。
3.5 时间线跟到底:Procmon 和 Wireshark 对表
新手容易犯一个错误:只抓包不抓进程。其实流量和进程行为必须放一起看才能还原出完整攻击链。Process Monitor 的时间戳和 Wireshark 的包时间戳是可以对齐的。我会把样本进程的网络事件拉出来,看它连接远程端口的时间点,再和 Wireshark 里对应流的时间点比对,这样能确认流量是哪个进程、哪个操作触发的。
更复杂的情况是样本做了进程注入,网络流量并不是从样本自己的进程发出的。这时候 Process Monitor 可能显示正常的系统进程里有外联连接,比如 svchost.exe 突然多了到远程端口的连接。遇到这种情况,要立刻 dump 对应进程的内存,结合注入痕迹分析,不然你会误判流量来源。
4. 把分析结果变成检测规则
4.1 提取 IOC 的一手模板
动态分析结束后,不能只留下“啊我看到了 C2 域名”这种模糊印象。我会把所有可观测特征整理成一张 IOC 表,包括网络侧和主机侧。
网络侧我会记这些:
- 域名和 IP
- 端口和协议
- HTTP URI、请求方法、User-Agent
- DNS 查询模式,比如子域长度和字符集
- TCP 载荷的固定标志位或前几个字节
主机侧我会记这些:
- 互斥体名称
- 注册表 Run 键或服务名
- 释放的文件路径
- 创建的进程名称
- 进程注入的目标进程
这张表既是报告的素材,也是接下来写检测规则的输入。
4.2 写一条 Snort 规则
网络侧的检测规则,我一般用 Snort 或 Suricata 语法。比如 Lab14-1 的 HTTP 外传行为,可以快速写成下面这样:
alert tcp $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS ( msg:"Potential C2 HTTP POST - login.php"; content:"POST /login.php"; content:"Host: example-c2-lab.com"; content:"User-Agent: Mozilla/5.0"; sid:140001; rev:1; )写规则的时候,我特别强调“选稳定的特征”。域名和 URI 路径如果固定,可以作为主特征;User-Agent 可能会变化,就不要单独用来命中,而是组合使用。如果样本使用 DNS 隧道,Snort 规则可以对 DNS 查询做长度和字符计数,但这种规则容易误报,建议先用 Zeek 或脚本离线分析,确定隧道频次之后再落成签名。
4.3 写一条 YARA 规则
主机侧的检测主要靠 YARA 识别样本文件和释放物。YARA 可以扫字符串、导入表特征、PE 结构等。一个简单的例子:
rule Lab14_1_HTTPC2 { meta: author = "yourname" description = "Detection for Lab14-1 HTTP C2 sample" strings: $s1 = "POST /login.php" ascii $s2 = "example-c2-lab.com" ascii $s3 = "InternetOpenA" ascii condition: uint16(0) == 0x5A4D and all of them }注意 YARA 规则的第一个条件,先用 uint16(0) == 0x5A4D 判断文件是 MZ 头,避免把非 PE 文件也拉进来。字符串最好选那些静态分析阶段稳定存在的,不要选频繁变动的 URL 参数。这样的 YARA 规则才能用于主机扫描和样本库检索。
规则写完之后,最重要的一步是用干净的软件和正常流量做“负反馈验证”。我见过太多规则能抓样本但也能把王者荣耀的流量误判成恶意,这种规则生产环境根本不敢用。验证的时候,手头没有真实环境,我就用虚拟机的正常系统更新流量做测试,至少保证规则不会在日常系统里乱报。
5. 常见问题与排查记录
5.1 样本不产生任何网络流量
这个坑出现频率最高。样本不联网的原因可能有几种:
- 样本检测到虚拟化环境,直接退出或进入“休眠”状态;
- ApateDNS 没生效,域名解析失败,样本的网络栈没走到发起请求那一步;
- INetSim 没有正确模拟样本期待的服务端响应,样本卡在等待状态;
- 样本内建了很长的延迟,需要多等一会儿。
我的排查顺序是:先看 Procmon 里进程是否还活着,再看 ApateDNS 的日志里有没收到解析请求,再看 INetSim 的日志里有没有连接记录,最后才怀疑反虚拟化。最有效的自检办法是先用正常程序验证环境,比如 nslookup 设置 DNS 为 Kali 的 IP,curl 测一下 HTTP 服务,确认链路畅通后再跑样本。
5.2 样本检测到虚拟机就休眠怎么办
有些样本会检查注册表里的虚拟机标识、硬件设备名,甚至进程列表里的 vmware tools 进程。遇到这种情况,先别急着硬碰硬,可以先做一次常规规避:关闭虚拟机里的拖拽、剪贴板共享、硬件加速,把虚拟机名称改成非默认名称,再把 tools 的服务停掉。很多样本的反虚拟机逻辑只做表面检查,这些手段能蒙过去。
如果样本用更底层的方式识别虚拟机,比如比较 CPU 指令集特征,那就要用调试器跟进去 patch 了。但这一章实验的重点是网络行为分析,不建议在反调试上花太多时间。先把样本能不能跑出正常行为这个问题解决,再回来研究它的反分析手段。
5.3 Wireshark 有包但看不明白是什么
当 Wireshark 里有 TCP 连接,但 Follow HTTP Stream 没有解析出 HTTP 内容时,十有八九是自定义协议或加密流量。这时候先看原始数据流,如果载荷首字节不是 GET、POST 这类 ASCII 明文,就按自定义协议处理。
处理自定义协议我有一套固定流程:保存原始 pcap,把载荷导出成二进制文件,再用 CyberChef 的 Magic 模块跑一遍,看能不能识别出 base64、base32、异或等编码。如果 Magic 也识别不出来,就回调试器在 recv 函数下断点,直接看内存缓冲区的真实数据。这一招在对付“表面加密”的样本时非常有效。
5.4 DNS 请求太多,怎么区分正常和异常
正常程序也会有大量 DNS 请求,尤其是浏览器和系统更新进程。区分隧道流量,我主要看三个统计指标:
- 请求域名数量分布:是否高度集中在同一个根域名;
- 子域名长度和字符熵:正常域名可读性高,隧道子域名通常是无意义字符;
- 请求频率:隧道为了传数据,频率通常会明显高于正常解析。
用 Wireshark 的 Statistics -> DNS 面板可以很快看到域名列表。如果某个域名的请求数量占比异常高,再结合子域名特征,基本就能锁定。
5.5 环境越跑越脏,结果全是错的
最后说一个我踩过很多次的坑:贪图省事不还原快照,结果前一个样本留下的自启动项和注册表键把后一个样本的触发条件给满足了,分析结论毫无意义。现在我每次跑样本都从干净快照启动,样本跑完不管有没有抓到流量都立刻还原。这个过程看似麻烦,实际节省的时间远超浪费的时间。分析工作最怕“结论不可复现”,而干净快照是复现一切的基础。
最后再分享一个小技巧。第 14 章的实验做完后,我把每个样本的网络行为整理成了一张固定模板表,包含协议、目标地址、端口、载荷特征、关联进程。以后遇到新样本,直接按这个模板填,能很快建立初步印象。动态分析不是看一遍流量就完事,要多跑几次,每次换不同的服务端响应,观察样本有没有多分支逻辑。我在做 Lab14-2 时,就通过改变 INetSim 的 DNS 响应内容,发现样本会根据响应长度决定是否进入下一阶段。这种隐藏在“变体行为”里的细节,只抓一次包是永远发现不了的。希望这份实验记录能帮你少走点弯路。