128 lines
5.3 KiB
PowerShell
128 lines
5.3 KiB
PowerShell
|
|
# 扫描 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<string>();
|
|||
|
|
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
|