网络连接与流量痕迹

"这台机器当时在和谁通信"这个问题,内存给出的答案比任何日志都完整。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词:netscan、netstat、内核池、stale data、TCPIP 结构、SNI、dumpfiles、交叉验证 难度:进阶 前置知识:TCP/IP 协议栈、Windows 网络内核结构、Volatility 3 基本使用

一、概述

"这台机器当时在和谁通信"这个问题,内存给出的答案比任何日志都完整。

原因是它记录的是协议栈的实际状态,而不是"被记录下来的行为"。代理、防火墙、IDS 日志都要配置,可能丢包,可能只记一部分;内存里的 TCB 结构是当时连接的真实快照。

但这里有一条必须先知道的路:内核池里的网络结构会残留已释放的数据。

内核的分页池分配器不会主动清零已释放的内存。一个连接关闭后 TCB 被释放回池,字段里仍保留着旧的 IP、端口和 PID。后果很具体:netscan 显示一条 ESTABLISHED 到境外 IP 的连接,看起来铁证如山——但它可能几小时前就关闭了。单凭 netscan 输出认定"该机当时与某 IP 建立了连接",在方法论上站不住。

这一步落在关联环节,上游是内存镜像与已完成的进程定位,下游是外传类结论的定性。它提供的是"方向",不是"结论"——要认定数据外传,流量侧必须独立支撑。

与采集环节的耦合点在于时间:内存里的连接是采集区间内的状态,与 PCAP 的时间线对齐方式决定了能不能做因果论证。

可用的对象与判据如下:

对象 具体形态 关键字段/判据
内核网络结构 TCB、UDP_ENDPOINT、TCP_ENDPOINT 状态字段、索引项、PID
枚举方式 netscan(扫池,含残留)vs netstat(遍历结构,仅当前活动) 两者结果是否一致
连接归属 本地端口 → PID → 进程映像路径 归属链条是否闭合
明文残留 内存中的域名、URL、HTTP 头、请求体 是否有上下文可判定方向
抓包侧 DigiForensics.pcap、NetFlow 导出、防火墙日志 与内存侧的时间对齐

第二行是核心:netscan 与 netstat 的差异不是工具选择问题,而是方法论问题。两者的一致性本身就是一项判据。

据此能回答的是这些:采集区间内存在哪些连接、它们归属哪个进程、连的是哪个 IP 与端口、内存里是否残留了明文通信内容(域名、URL、请求体)、某个 IP 是否与已知的可疑基础设施吻合。

回答不了的是这些。它不能单侧认定"发生了数据外传"——连接存在与内容外传是两件事,后者需要流量侧或落盘侧独立支撑。它也给出不了精确到秒的行为顺序,内存证据的时序精度受采集耗时限制。

往下读之前,先确认你已经掌握:TCP 状态机与端口的基本概念、内存镜像的基本可用性、以及至少一种把 PID 与进程对应起来的方法。镜像还没有,先看内存采集方法;要跟抓包做时序对齐,先看PCAP 网络流量取证里的结论强度分级口径。连接指向远控软件时,去远程工具凭据解密看同一批凭据在磁盘侧的固定方法。

二、核心原理

2.1 netscan 与 netstat 的原理差异

两者输出看起来相似,原理却完全不同。

netstat —— 遍历内核维护的活动链表

内核全局变量:TcpCompartmentSet / UdpCompartmentSet
        ↓
遍历活动连接链表(每个 TCP 端点一个 TCB)
        ↓
只包含【当前处于活动状态】的连接

这与 tasklist、netstat -ano 看到的是同一份数据——因为它们都走内核的活动链表。** netscan —— 扫描内核池,搜索结构特征**

遍历物理内存的每一个页
        ↓
搜索符合 TCB / UDP_ENDPOINT 特征模式的字节
        ↓
对每个候选做有效性校验
        ↓
输出【所有能找到的】结构,包括已释放后残留的
维度 netscan netstat
方法 扫描内核池 遍历活动链表
已关闭的连接 能发现(可能) 看不到
已释放的残留 会误报 不会
证据强度 线索,需验证 较强(内核视角的当前态)

这让它成为发现"短暂外联"的独门工具。

代价就是 stale data 误报。

2.2 stale data(陈旧数据)的成因

T1 连接建立,分配 TCB,写入 remote IP/port/PID/状态
T2 连接关闭,内核释放 TCB
   ↑ 释放 ≠ 清零!分页池只把这段内存标记为"可复用"
T3 该内存块被其他对象部分复用(覆盖了部分字段)
T4 采集内存
T5 netscan 扫描 → 若 TCB 特征字段仍"看起来合法",就被报出

为什么残留字段"看起来合法":

  • IP 是 4 字节固定值,被复用后可能恰好又是合法 IP
  • 端口是 2 字节,几乎任何值都合法
  • 状态字段(TcpState)在固定偏移,释放时没被改,所以仍是 ESTABLISHED
  • PID 字段可能被新对象覆盖成别的进程 PID——最危险,会错误归因到无辜进程

必须记住:stale data 影响的不只是"连接是否存在",还包括"是哪个进程"。这是误伤无辜进程的高发点。

2.3 TCP 状态的语义区分

状态 含义 取证意义
ESTABLISHED 连接已建立且活动 最需关注,但注意 stale 误报
SYN_SENT / SYN_RECV 握手进行中 外连尝试
LISTENING 监听端口 后门驻留的直接信号
TIME_WAIT 主动关闭方等待 证明此前的连接确实发生过
CLOSE_WAIT 对端已关闭,本地未释放 应用层异常,程序未正确关闭

TIME_WAIT 与 CLOSED 的区别很关键:TIME_WAIT 残留本身就是有效证据——它表明连接确实建立过且是本机主动关闭的。孤立的 ESTABLISHED 残留不能证明任何事。这是判读 netscan 的实用过滤器。

2.4 端口与 IP 的解读

端口 用途 说明
4444 Metasploit 默认 payload 端口 经典后门端口
1337 常见于各类工具默认 需结合进程判断
8080 / 8443 代理 / 备用 HTTPS 本身正常,但常被滥用
9001 / 9050 Tor SOCKS 默认端口 匿名网络
53(非标准源端口) DNS 外传通道 隐蔽信道
5555 / 6666 / 7777 常见反连端口 批量扫描特征

但"异常端口"不等于恶意。企业环境的代理、监控 agent、数据库同步都用常见端口。端口是线索,不是结论。

IP 判读要点:

  • RFC 1918 私有地址(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16):不说明任何问题
  • 回环(127.0.0.1)、链路本地(169.254.0.0/16):本机内部通信 / DHCP 失败地址,通常无意义
  • 公网 IP:需查归属。但要警惕"CDN IP ≠ 真实服务端"——域名的解析结果往往是 CDN 节点
  • IPv6:注意是否有 IPv4 映射地址(::ffff:a.b.c.d 形式)

2.5 内存中的明文通信内容

网络结构里只有 IP 和端口,真正的通信内容在别处:HTTP 请求/响应在套接字缓冲区与进程堆,聊天/邮件正文在客户端进程的消息结构,浏览器表单与页面缓存在浏览器进程内存,文件传输片段在套接字缓冲区。

提取手段都是 dumpfiles 配合定向 strings。

⚠️ 重要现实:现代 HTTPS 流量在内存里也是加密的,直接搜不到明文。但三个位置仍可能有明文:

  1. TLS 握手的 SNI(Server Name Indication)字段——客户端为加速/共享而明文发送的服务名,这是 HTTPS 下最可靠的可见线索
  2. 应用层在加密前写入缓冲区的数据
  3. 证书链中的域名信息

SNI 的取证价值极高:它能证明"这台机器曾尝试连接某个域名",即使流量加密。strings 输出中域名通常是明文 ASCII 或 UTF-8。

2.6 内存与抓包的互补

能力 内存 抓包
已关闭的连接 可能可见(池残留) ❌ 已关闭永远看不到
加密流量内容 看不到明文 看不到明文
域名(SNI) 可从内存字符串提取 可从握手包提取
事后分析 ✅ 可反复分析同一份镜像 ❌ 需重新采集
需要预置 需事先装好抓包工具 必须事先部署

核心互补关系:抓包必须事先部署,事后再想抓已经来不及;内存是事后唯一的机会,但内容层面受限于当时应用状态。两者结合:netscan 告诉你"有连接到这个 IP"→ 查威胁情报 → 拿 IP 换到具体域名 → 在内存里 grep 该域名找证据链。

内存的时间戳精度不够做因果排序,Sysmon 事件 3/7 的连接记录带毫秒级时间且落在日志里,能与 netscan 结果逐条对齐,聚合与导出方式见Sysmon 与日志聚合。


三、操作步骤

3.1 第 1 步:枚举连接(两个插件都要跑)

netstat:内核活动链表(当前真实状态);netscan:内核池扫描(含已关闭连接与残留)

python3 vol.py -f DigiForensics-mem.raw -r csv windows.netstat > netstat.csv python3 vol.py -f DigiForensics-mem.raw -r csv windows.netscan > netscan.csv head -1 netstat.csv ; head -1 netscan.csv # 先确认列名

3.2 第 2 步:交叉验证(本篇核心步骤,不可跳过)

⚠️ 先确认列位。-r csv 的输出第一列是 TreeDepth,其后才是插件的实体列。netscan 与 netstat 的列定义完全一致(源码核实,两者 TreeGrid 逐字段相同),共 10 个实体列 + 1 个 TreeDepth:

$1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11
TreeDepth Offset Proto LocalAddr LocalPort ForeignAddr ForeignPort State PID Owner Created

所以"四元组 + 状态"的键是 $4|$5|$6|$7|$8。$9 是 PID,不是状态——把 $9 当状态用是本步骤最常见的错误,它会让"仅 netscan 有"的集合里混进同一连接的不同进程归属,比对结论直接失效。漏掉 TreeDepth 同样致命:会让整体错位一列。

键 = 四元组 + 状态 = $4|$5|$6|$7|$8($9 是 PID,别搞错)

awk -F',' 'NR>1 {gsub(/"/,""); print $4"|"$5"|"$6"|"$7"|"$8}' netstat.csv
| sort -u > /tmp/netstat_keys.txt awk -F',' 'NR>1 {gsub(/"/,""); print $4"|"$5"|"$6"|"$7"|"$8}' netscan.csv
| sort -u > /tmp/netscan_keys.txt comm -13 /tmp/netstat_keys.txt /tmp/netscan_keys.txt | tee /tmp/only-netscan.txt # 仅 netscan 有 comm -23 /tmp/netstat_keys.txt /tmp/netscan_keys.txt # 仅 netstat 有

C) PID 归属不一致(stale data 典型特征):把 PID 并入键做二次比对

awk -F',' 'NR>1 {gsub(/"/,""); print $4"|"$5"|"$6"|"$7"|"$8"|"$9}' netscan.csv
| sort -u > /tmp/netscan_withpid.txt awk -F',' 'NR>1 {gsub(/"/,""); print $4"|"$5"|"$6"|"$7"|"$8"|"$9}' netstat.csv
| sort -u > /tmp/netstat_withpid.txt comm -13 /tmp/netstat_withpid.txt /tmp/netscan_withpid.txt

判读规则表(务必写进报告):

情形 解释 结论写法
两边都有 采集时刻确实活动的连接 可作为结论,强度最高
仅 netscan 有,ESTABLISHED 可能已关闭,也可能纯属残留 ⚠️ 不能下结论,须另有证据;标注"待验证"
仅 netscan 有,TIME_WAIT 连接确实建立过,本机主动关闭 可作为"曾有连接"的证据,强度中等
仅 netscan 有,LISTENING 端口监听,可能是残留 ⚠️ 不能下结论,需与 netstat 和磁盘侧交叉
仅 netstat 有 netscan 漏检 以 netstat 为准

为什么"TIME_WAIT 更可信":TIME_WAIT 是 TCP 正常关闭流程产生的活跃状态——残留的 TCB 状态字段虽然没被清,但要成为 TIME_WAIT 必须曾经真的进入过这个状态。相比之下 ESTABLISHED 是连接存续最长的状态,也是最常被残留保存的值,最容易被"冒充"。

3.3 第 3 步:筛选高价值连接

只看公网出口连接(过滤私有地址和回环)

python3 vol.py -f DigiForensics-mem.raw -r pretty windows.netstat
| grep -vE '10.|172.(1[6-9]|2[0-9]|3[01]).|192.168.|127.0.0.|0.0.0.0'

监听端口(后门驻留信号) / 高危端口

python3 vol.py -f DigiForensics-mem.raw -r pretty windows.netstat | grep -i 'LISTENING' python3 vol.py -f DigiForensics-mem.raw -r pretty windows.netstat
| grep -iE ':(4444|1337|9001|9050|5555|6666|7777) '

netscan 的公网连接(按 3.2 交叉验证后才能引用)

python3 vol.py -f DigiForensics-mem.raw -r pretty windows.netscan
| grep -vE '10.|172.(1[6-9]|2[0-9]|3[01]).|192.168.|127.0.0.|0.0.0.0'
| tee /work/netscan-public.txt

3.4 第 4 步:定位连接归属进程

从 netstat 输出拿 PID,再查该进程

python3 vol.py -f DigiForensics-mem.raw -r pretty windows.pslist | grep -E '\s<PID>$|<可疑PID>'

可疑进程的完整命令行(判断它是否"本该"联网)

python3 vol.py -f DigiForensics-mem.raw -r pretty windows.cmdline --pid <PID>

进程加载了什么(判断是真实浏览器还是伪装)

python3 vol.py -f DigiForensics-mem.raw -r pretty windows.dlllist --pid <PID>

该进程的注入检查(并发是后门常见行为)

python3 vol.py -f DigiForensics-mem.raw -r csv windows.malware.malfind --pid <PID>

归属判断的关键:不是每个网络进程都该有网络连接。 notepad.exe 有外连、Word 进程连到公网 IP、系统服务连到非常规端口——这些都需要解释。

3.5 第 5 步:从内存提取域名与 URL

⚠️ 必须分块扫描。strings -a -n 6 DigiForensics-mem.raw 会把整个镜像读进管道,16 GB 起步的镜像足以 OOM。定义一个可复用的分块函数(bs=1M 在 GNU/BSD 的 dd 上都可用,skip 的单位是块不是字节):

IMG=DigiForensics-mem.raw

用法: scan_img <正则> <输出文件> [最小串长]

scan_img () { local re="$1" out="$2" min="${3:-6}" size step=64 i=0 size=$(wc -c < "$IMG") : > "$out" while [ $((istep)) -lt "$size" ]; do dd if="$IMG" bs=1M skip=$((istep)) count="$step" status=none 2>/dev/null \