# 扫描 PLC 网络:找 S7 设备(端口 102),并做 COTP 握手确认是不是西门子 PLC # # 用法(在【不受 DSH 沙箱限制】的普通 PowerShell 里运行): # powershell -ExecutionPolicy Bypass -File tools\scan-plc.ps1 # powershell -ExecutionPolicy Bypass -File tools\scan-plc.ps1 -Subnet 192.168.0.0/16 # powershell -ExecutionPolicy Bypass -File tools\scan-plc.ps1 -Subnet 192.168.189.0/24 -TimeoutMs 400 # # 输出:IP / MAC / 是否 S7(COTP 握手 D0)/ 响应耗时 param( [string]$Subnet, # 形如 192.168.189.0/24;省略则自动取有线网卡所在网段 [int]$TimeoutMs = 600, [int]$Threads = 128 ) $ErrorActionPreference = 'Stop' # ---------- 1. 自动取网段 ---------- if (-not $Subnet) { $cand = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' -and $_.InterfaceAlias -notlike '*VMware*' } if (-not $cand) { Write-Host '找不到可用网卡,请手动指定 -Subnet,例如 -Subnet 192.168.0.0/24' -ForegroundColor Red exit 1 } $pick = $cand | Select-Object -First 1 $ip = $pick.IPAddress; $plen = $pick.PrefixLength $Subnet = "$ip/$plen" Write-Host ("使用网卡 {0} 的地址 {1}" -f $pick.InterfaceAlias, $Subnet) -ForegroundColor Cyan } $parts = $Subnet.Split('/') $baseIp = $parts[0] $prefix = [int]$parts[1] Write-Host ("扫描 {0} 的 TCP 102(S7),超时 {1}ms,并发 {2} ..." -f $Subnet, $TimeoutMs, $Threads) -ForegroundColor Cyan # ---------- 2. 并发扫描 + COTP 握手(C# 实现,PS 5.1 也能跑) ---------- $src = @' using System; using System.Net.Sockets; using System.Collections.Concurrent; using System.Threading.Tasks; public class PlcScan { static readonly byte[] COTP_CR = new byte[] { 0x03,0x00,0x00,0x16, 0x11,0xE0,0x00,0x00, 0x00,0x01,0x00, 0xC1,0x02,0x01,0x00, 0xC2,0x02,0x01,0x02, 0xC0,0x01,0x0A }; public static string[] Scan(string cidrIp, int prefix, int port, int timeoutMs, int threads) { var p = cidrIp.Split('.'); uint ip = ((uint)int.Parse(p[0]) << 24) | ((uint)int.Parse(p[1]) << 16) | ((uint)int.Parse(p[2]) << 8) | (uint)int.Parse(p[3]); uint mask = prefix == 0 ? 0u : (0xFFFFFFFFu << (32 - prefix)); uint net = ip & mask, bcast = net | ~mask; var bag = new ConcurrentBag(); Parallel.For((long)net + 1, (long)bcast, new ParallelOptions { MaxDegreeOfParallelism = threads }, i => { string s = string.Format("{0}.{1}.{2}.{3}", (i >> 24) & 255, (i >> 16) & 255, (i >> 8) & 255, i & 255); try { using (var c = new TcpClient()) { var ar = c.BeginConnect(s, port, null, null); if (ar.AsyncWaitHandle.WaitOne(timeoutMs) && c.Connected) bag.Add(s); } } catch { } }); var arr = bag.ToArray(); Array.Sort(arr); return arr; } public static bool IsS7(string ip, int timeoutMs) { try { using (var c = new TcpClient()) { var ar = c.BeginConnect(ip, 102, null, null); if (!ar.AsyncWaitHandle.WaitOne(timeoutMs) || !c.Connected) return false; c.EndConnect(ar); c.ReceiveTimeout = timeoutMs; var st = c.GetStream(); st.Write(COTP_CR, 0, COTP_CR.Length); st.Flush(); var buf = new byte[64]; var r = st.BeginRead(buf, 0, buf.Length, null, null); if (!r.AsyncWaitHandle.WaitOne(timeoutMs)) return false; int n = st.EndRead(r); // COTP Connection Confirm: 0x03 0x00 ... 0x11 0xD0 return n >= 6 && buf[0] == 0x03 && buf[5] == 0xD0; } } catch { return false; } } } '@ Add-Type -TypeDefinition $src -Language CSharp $sw = [Diagnostics.Stopwatch]::StartNew() $hosts = [PlcScan]::Scan($baseIp, $prefix, 102, $TimeoutMs, $Threads) $sw.Stop() Write-Host ("端口 102 开放的主机:{0} 台(扫描耗时 {1:N1}s)" -f $hosts.Count, $sw.Elapsed.TotalSeconds) -ForegroundColor Green if ($hosts.Count -eq 0) { Write-Host '没发现开着 102 的主机。检查:① 网线是否插在同一交换机;② 本机 IP 是否与 PLC 同网段;③ PLC 是否启用;④ 防火墙。' -ForegroundColor Yellow exit 0 } # ---------- 3. 取 MAC 并确认 S7 ---------- $arp = @{} (arp -a) | ForEach-Object { if ($_ -match '^\s*(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-]{17})\s+') { $arp[$matches[1]] = $matches[2].ToUpper() } } $rows = foreach ($h in $hosts) { $t0 = [Diagnostics.Stopwatch]::StartNew() $isS7 = [PlcScan]::IsS7($h, [Math]::Max($TimeoutMs, 1000)) $t0.Stop() [pscustomobject]@{ IP = $h MAC = if ($arp.ContainsKey($h)) { $arp[$h] } else { '(未知)' } S7握手 = if ($isS7) { '是' } else { '否' } 耗时ms = [int]$t0.Elapsed.TotalMilliseconds } } $rows | Sort-Object IP | Format-Table -AutoSize $out = Join-Path $PSScriptRoot 'scan-plc-result.csv' $rows | Export-Csv -Path $out -NoTypeInformation -Encoding UTF8 Write-Host ("结果已保存:{0}" -f $out) -ForegroundColor Cyan Write-Host '把这张表贴给开发,即可继续判断设备 4 / 设备 5 是哪个 IP。' -ForegroundColor Cyan