| name | performing-plc-firmware-security-analysis |
| description | 本技能涵盖分析可编程逻辑控制器(PLC)固件的安全漏洞,包括硬编码凭据、不安全的更新机制、后门功能、内存损坏缺陷和未记录的调试接口。涉及从常见PLC平台(西门子S7、Allen-Bradley、施耐德Modicon)提取固件、固件镜像静态分析、仿真环境中的动态分析,以及与已知良好基线的对比以检测篡改。
|
| domain | cybersecurity |
| subdomain | ot-ics-security |
| tags | ["ot-security","ics","scada","industrial-control","iec62443","firmware-analysis","plc-security"] |
| version | 1.0.0 |
| author | mahipal |
| license | Apache-2.0 |
执行PLC固件安全分析
适用场景
- 在IEC 62443组件安全评估(IEC 62443-4-2)中评估PLC安全性时
- 在疑似入侵或供应链攻击后验证固件完整性时
- 在关键基础设施部署前评估新PLC平台的安全性时
- 在授权实验室对工业控制系统设备进行漏洞研究时
- 响应疑似PLC逻辑或固件篡改的安全事件时
不适用于未经明确授权和安全控制措施到位的情况下对生产PLC进行操作。固件提取和分析应在实验室设备或离线备份上执行。切勿将PLC固件上传至公开分析服务。授权现场测试程序请参见performing-ics-penetration-testing。
前置条件
- 目标PLC硬件或仿真环境的隔离实验室环境
- 目标平台的PLC编程软件(西门子TIA Portal、Rockwell Studio 5000、施耐德EcoStruxure)
- 固件提取工具(binwalk、firmware-mod-kit、JTAG/SWD调试器)
- 静态分析工具(Ghidra、IDA Pro、支持ARM/MIPS/PowerPC的Binary Ninja)
- 了解PLC架构(实时操作系统、梯形图逻辑执行、I/O扫描)
- 用于完整性对比的已知良好固件参考副本
工作流程
步骤 1:获取PLC固件进行分析
通过授权方式提取或获取PLC固件。可通过从供应商下载、从实验室设备提取或从项目备份中获取。
"""PLC固件获取与完整性验证。
支持从项目文件、网络下载提取固件,
以及与已知良好基线进行二进制镜像对比。
"""
import hashlib
import json
import os
import struct
import sys
import zipfile
from datetime import datetime
from pathlib import Path
class PLCFirmwareAcquisition:
"""处理来自各种来源的PLC固件获取。"""
def __init__(self, output_dir="firmware_analysis"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
self.manifest = {
"acquisition_date": datetime.now().isoformat(),
"firmware_samples": [],
}
def extract_from_siemens_project(self, project_path):
"""从西门子TIA Portal项目中提取固件/程序块。
TIA Portal项目(.ap16/.ap17)是ZIP压缩包,包含
XML编码的PLC程序块和系统配置。
"""
print(f"[*] 正在分析西门子项目: {project_path}")
results = {"platform": "Siemens", "blocks": []}
if zipfile.is_zipfile(project_path):
with zipfile.ZipFile(project_path, "r") as zf:
for info in zf.infolist():
if info.filename info.filename:
block_data = zf.read(info.filename)
block_hash = hashlib.sha256(block_data).hexdigest()
block_path = .output_dir / info.filename.replace(, )
block_path.write_bytes(block_data)
results[].append({
: info.filename,
: info.file_size,
: block_hash,
: (block_path),
})
()
.manifest[].append(results)
results
():
()
results = {: , : []}
(acd_path, ) f:
header = f.read()
header header:
f.seek()
full_data = f.read()
file_hash = hashlib.sha256(full_data).hexdigest()
results[].append({
: os.path.basename(acd_path),
: (full_data),
: file_hash,
: header[:].(),
})
()
.manifest[].append(results)
results
():
data = Path(firmware_path).read_bytes()
{
: (firmware_path),
: (data),
: hashlib.md5(data).hexdigest(),
: hashlib.sha256(data).hexdigest(),
: hashlib.sha512(data).hexdigest(),
}
():
current_hash = .compute_firmware_hash(current_fw)
baseline_hash = .compute_firmware_hash(baseline_fw)
= current_hash[] == baseline_hash[]
result = {
: datetime.now().isoformat(),
: current_hash,
: baseline_hash,
: ,
: ,
}
:
current_data = Path(current_fw).read_bytes()
baseline_data = Path(baseline_fw).read_bytes()
min_len = ((current_data), (baseline_data))
first_diff =
diff_count =
i (min_len):
current_data[i] != baseline_data[i]:
first_diff :
first_diff = i
diff_count +=
result[] = first_diff
result[] = diff_count
result[] = (current_data) - (baseline_data)
result
():
manifest_path = .output_dir /
(manifest_path, ) f:
json.dump(.manifest, f, indent=)
()
__name__ == :
acq = PLCFirmwareAcquisition()
(sys.argv) < :
()
()
()
()
sys.exit()
cmd = sys.argv[]
cmd == (sys.argv) > :
acq.extract_from_siemens_project(sys.argv[])
cmd == (sys.argv) > :
acq.extract_from_rockwell_project(sys.argv[])
cmd == (sys.argv) > :
result = acq.compare_firmware_integrity(sys.argv[], sys.argv[])
(json.dumps(result, indent=))
:
()
sys.exit()
acq.save_manifest()
步骤 2:对固件镜像进行静态分析
使用binwalk进行固件解包,使用Ghidra进行反汇编,以识别固件二进制文件中的安全问题。
binwalk -e firmware.bin
binwalk firmware.bin
strings -n 8 firmware.bin > firmware_strings.txt
grep -iE "(password|passwd|pwd|secret|key|credential|login|admin|root)" firmware_strings.txt
grep -iE "(http|ftp|telnet|ssh|snmp|modbus|192\.168|10\.|172\.)" firmware_strings.txt
grep -iE "(debug|backdoor|test_mode|factory|service_port|hidden)" firmware_strings.txt
grep -iE "(BEGIN RSA|BEGIN CERTIFICATE|AES|DES|private.key)" firmware_strings.txt
binwalk -E firmware.bin
analyzeHeadless /tmp/ghidra_project PLC_FW \
-import firmware.bin \
-processor ARM:LE:32:Cortex \
-postScript FindCryptoConstants.java \
-postScript FindHardcodedStrings.java \
-log /tmp/ghidra_analysis.log
步骤 3:分析PLC通信协议栈安全性
检查PLC如何处理工业协议请求,重点关注认证绕过、数据包解析中的缓冲区溢出以及命令注入漏洞。
"""PLC协议安全分析器。
测试PLC协议实现中的常见漏洞,
包括认证绕过、畸形数据包处理
和功能码访问控制。
警告:只在实验室/测试PLC上运行,绝不对生产系统使用。
"""
import socket
import struct
import sys
import time
from dataclasses import dataclass
@dataclass
class ProtocolTestResult:
test_name: str
target: str
protocol: str
result: str
severity: str
detail: str
class ModbusSecurityTester:
"""测试Modbus/TCP实现安全性。"""
def __init__(self, target_ip, target_port=502):
self.target = target_ip
self.port = target_port
self.results = []
def _send_modbus(self, unit_id, func_code, data=b""):
"""发送Modbus/TCP请求并返回响应。"""
mbap = struct.pack(">HHHB", 0x0001, 0x0000, len(data) + 2, unit_id)
pdu = struct.pack("B", func_code) + data
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((self.target, self.port))
sock.send(mbap + pdu)
response = sock.recv()
sock.close()
response
Exception e:
():
read_data = struct.pack(, , )
response = ._send_modbus(, , read_data)
response (response) > response[] != :
.results.append(ProtocolTestResult(
test_name=,
target=.target,
protocol=,
result=,
severity=,
detail=,
))
write_data = struct.pack(, , )
response = ._send_modbus(, , write_data)
response (response) > response[] != :
.results.append(ProtocolTestResult(
test_name=,
target=.target,
protocol=,
result=,
severity=,
detail=,
))
():
dangerous_funcs = {
: ,
: ,
: ,
}
fc, desc dangerous_funcs.items():
response = ._send_modbus(, fc, )
response (response) > :
error_code = response[]
error_code != (fc | ):
.results.append(ProtocolTestResult(
test_name=,
target=.target,
protocol=,
result=,
severity=,
detail=,
))
():
read_data = struct.pack(, , )
response = ._send_modbus(, , read_data)
response (response) > response[] != :
.results.append(ProtocolTestResult(
test_name=,
target=.target,
protocol=,
result=,
severity=,
detail=,
))
():
malformed = struct.pack(, , , ) +
:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout()
sock.connect((.target, .port))
sock.send(malformed)
time.sleep()
read_data = struct.pack(, , )
response = ._send_modbus(, , read_data)
sock.close()
response :
.results.append(ProtocolTestResult(
test_name=,
target=.target,
protocol=,
result=,
severity=,
detail=,
))
:
.results.append(ProtocolTestResult(
test_name=,
target=.target,
protocol=,
result=,
severity=,
detail=,
))
Exception e:
():
()
()
()
.test_authentication_required()
.test_function_code_access_control()
.test_invalid_unit_id()
.test_malformed_packet_handling()
r .results:
icon = r.result ==
()
()
()
.results
__name__ == :
(sys.argv) < :
()
()
sys.exit()
target = sys.argv[]
port = (sys.argv[]) (sys.argv) >
tester = ModbusSecurityTester(target, port)
tester.run_all_tests()
核心概念
| 术语 | 定义 |
|---|
| PLC固件(PLC Firmware) | 运行在可编程逻辑控制器上的嵌入式软件,包括实时操作系统、协议栈和I/O驱动程序 |
| 梯形图逻辑(Ladder Logic) | PLC的图形化编程语言,以继电器逻辑电路形式表示,以程序块形式存储在PLC内存中 |
| 功能块(Function Block) | 可重用的PLC编程元素,封装了具有定义输入/输出的逻辑,可分析是否存在恶意修改 |
| 固件完整性(Firmware Integrity) | 使用密码学哈希对比,验证PLC固件未被从供应商提供或审批版本中修改 |
| IEC 62443-4-2 | IEC 62443标准中的组件安全要求,定义包括PLC在内的IACS组件所需的安全能力 |
| JTAG/SWD | 用于固件提取和底层分析的硬件调试接口(联合测试行动组/串行线调试) |
工具和系统
- Binwalk: 用于扫描、提取和分析嵌入式固件镜像的固件分析工具
- Ghidra: NSA开发的逆向工程框架,支持PLC中常用的ARM、MIPS、PowerPC架构
- EMUX/FIRMADYNE: 用于嵌入式设备固件动态分析的固件仿真框架
- PLCinject: 用于分析PLC逻辑注入漏洞的研究工具(仅在授权实验室环境中使用)
- OpenPLC: 开源PLC平台,可用作安全研究的测试目标
输出格式
PLC固件安全分析报告
=======================================
设备: [PLC型号和固件版本]
分析日期: YYYY-MM-DD
方法论: 静态 + 动态分析
固件完整性:
SHA-256: [哈希值]
基线匹配: [是/否]
供应商签名有效: [是/否/未签名]
发现的漏洞:
[PLC-001] [严重程度] [标题]
CWE: [CWE-ID]
详情: [技术描述]
影响: [运营影响]
修复: [修复或缓解措施]