西门子 S7CommPlus 协议鉴权算法原理与流程详解
文档版本 : 1.0
适用设备 : Siemens S7-1200 / S7-1500 / PLCSIM / PLCSIM Advanced
协议版本: S7CommPlus V1 (0x01) / V2 (TLS, 0x02) / V3 (0x03)
目录
- 协议背景与概述
- 数据结构与常量定义
- 认证体系的整体架构
- 第一阶段:密钥派生变换流水线 (Transform Pipeline)
- 4.1 [Monolith 模块](#Monolith 模块)
- 4.2 [BigInt 大数运算层](#BigInt 大数运算层)
- 4.3 [Transform 变换流水线](#Transform 变换流水线)
- 第二阶段:加密认证数据生成
- 5.1 [AES-128-ECB 加密链](#AES-128-ECB 加密链)
- 5.2 [Blob 元数据头](#Blob 元数据头)
- 5.3 [AES-CTR 与 HarpoHash 校验机制](#AES-CTR 与 HarpoHash 校验机制)
- [第三阶段:Session Key 派生](#第三阶段:Session Key 派生)
- 完整认证流程
- 7.1 [TCP 连接建立与 TLS 协商](#TCP 连接建立与 TLS 协商)
- 7.2 [Real PLC (S7-1200/1500) 认证流程](#Real PLC (S7-1200/1500) 认证流程)
- 7.3 [PLCSIM 仿真器认证流程](#PLCSIM 仿真器认证流程)
- 7.4 密钥交换时间线
- [Session Key 轮转机制](#Session Key 轮转机制)
- 数据包完整性保护 (Packet Digest)
- 密码鉴权机制 (Legitimation Scheme)
- 性能优化策略
- 安全分析
1. 协议背景与概述
1.1 S7CommPlus 协议简介
S7CommPlus 是西门子为其 S7-1200 和 S7-1500 系列 PLC 设计的下一代通信协议,用于替代旧的 S7Comm 协议。它在 TCP/TLS 之上运行(默认端口 102),引入了:
- 会话密钥交换 (Session Key Exchange):客户端与 PLC 之间协商对称加密密钥
- 基于挑战-响应的认证 (Challenge-Response Authentication):使用 PLC 公钥和椭圆曲线运算验证客户端身份
- HMAC-SHA256 数据包完整性:每个 PDU 携带完整性校验码,防止篡改和重放
- 密码鉴权 (Password Legitimation):基于密码哈希的二次身份验证
1.2 协议版本
| 版本 | 字节值 | 说明 |
|---|---|---|
| V1 | 0x01 |
基础 S7CommPlus,无 TLS |
| V2 | 0x02 |
带 TLS 加密传输 |
| V3 | 0x03 |
带 HMAC-SHA256 完整性校验 |
1.3 认证体系分层
┌──────────────────────────────────────────────────────┐
│ 认证 API 层 (s7_legacy_authenticate) │
├──────────────────────────────────────────────────────┤
│ RealPlc 路径 │ PLCSIM 路径 │
│ (S7-1200/1500) │ (仿真器) │
├──────────────────────────────────────────────────────┤
│ 加密认证数据生成 (AES-ECB 链) │
├──────────────────────────────────────────────────────┤
│ 密钥派生变换流水线 (Transforms) │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┐ │
│ │TPre│T7 │T12 │T13 │KeyD│LutG│Csum│Seed│ │
│ └────┴────┴────┴────┴────┴────┴────┴────┘ │
├──────────────────────────────────────────────────────┤
│ Monolith 函数族 (1-11) │
├──────────────────────────────────────────────────────┤
│ BigInt 大数运算层 │
│ ┌──────────┬──────────┬────────┬──────────┐ │
│ │Addition │Subtraction│Square │Multiply │ │
│ └──────────┴──────────┴────────┴──────────┘ │
├──────────────────────────────────────────────────────┤
│ OpenSSL 标准加密层 │
│ ┌──────┬──────────┬──────────┬────────────────┐ │
│ │AES │ SHA-256 │ HMAC │ RAND_bytes │ │
│ │ECB │ │ SHA256 │ │ │
│ └──────┴──────────┴──────────┴────────────────┘ │
└──────────────────────────────────────────────────────┘
2. 数据结构与常量定义
2.1 核心常量
c
// 密钥长度
#define S7_SESSION_KEY_LEN 24 // 会话密钥 24 字节
#define S7_AES_KEY_LEN 16 // AES 密钥 16 字节
#define S7_AES_BLOCK_LEN 16 // AES 块大小 16 字节
// 挑战长度
#define S7_CHALLENGE_LEN 20 // PLC 返回的挑战数据
#define S7_FINGERPRINT_LEN 8 // 挑战指纹 8 字节
#define S7_DIGEST_LEN 32 // HMAC-SHA256 摘要 32 字节
// 公钥长度
#define S7_PUBKEY_LEN_REAL_PLC 40 // 真实 PLC 公钥 (256-bit 椭圆曲线点)
#define S7_PUBKEY_LEN_PLC_SIM 64 // PLCSIM 公钥 (512-bit)
// 加密 Blob 长度
#define S7_ENCRYPTED_BLOB_REAL_PLC 180 // 真实 PLC 认证 Blob
#define S7_ENCRYPTED_BLOB_PLC_SIM 216 // PLCSIM 认证 Blob
2.2 BigInt 编码格式
每个 BigInt 使用 6 个 uint32_t(192-bit)编码,其中:
- 低 4 个 dword(128-bit)存储主数据
- 第 5 个 dword 存储 30-bit 值
- 第 6 个 dword 的高 2-bit 嵌入到 dword0 的第 1-2 位,剩余 bit 分发嵌入到各 dword
Prepare 操作(6→5 dword 压缩):
src[5] 的低 30-bit → dst[4]
src[5] 的高 2-bit / 低 2-bit / 低 4-bit / 高 6-bit
→ 分别嵌入 dst[0].bit1-2 / dst[1].bit3-4 / dst[2].bit5-6 / dst[3].bit0-7
Finalize 操作(5→6 dword 展开):
取每个 dst[i] 的低 N 位 (i=0:2bit, i=1:4bit, i=2:6bit, i=3:8bit, i=4:10bit)
拼装成 dst[5]
2.3 公钥家族枚举
| 值 | 枚举名 | 设备类型 |
|---|---|---|
| 0 | S7_FAMILY_S71500 |
S7-1500 系列 |
| 1 | S7_FAMILY_S71200 |
S7-1200 系列 |
| 3 | S7_FAMILY_PLCSIM |
PLCSIM 仿真器 |
2.4 Blob 元数据格式
标准认证元数据头 (48 字节)
偏移 大小 字段
0x00 4 Magic: 0xFEE1DEAD
0x04 4 EncryptedBlobLength (180 or 216)
0x08 4 SecurityKeyVersion = 1
0x0C 4 SecurityLevel = 1 (LegacyCSI)
0x10 8 SymmetricKeyId (SHA-256(key1 + "DERIVE")[0:8])
0x18 4 KeyType = 0x01 | FamilyFlags
0x1C 4 Reserved
0x20 8 PublicKeyId (SHA-256(publicKey + "DERIVE")[0:8])
0x28 4 KeyType = 0x10 | FamilyFlags
0x2C 4 Reserved
BEEF 片段元数据 (Legitimation 使用,12 字节)
偏移 大小 字段
0x00 4 Magic: 0xDEADBEEF
0x04 4 FragmentIndex
0x08 4 FragmentLength
3. 认证体系的整体架构
S7CommPlus 认证体系包含两大核心路径和两个辅助机制:
3.1 核心认证路径
┌── s7_legacy_authenticate ──┐
│ │
┌─────┴──────┐ ┌─────────┴──────────┐
│ Real PLC │ │ PLCSIM │
│ (S7-1200/ │ │ (仿真器) │
│ S7-1500) │ │ │
└─────┬──────┘ └─────────┬──────────┘
│ │
┌──────────────┼──────────────┐ ┌─────────┼──────────┐
│ WriteMetadata│ │ │WriteMetadata │
│ (48 bytes) │ │ │(48 bytes) │
├──────────────┤ │ ├─────────────────────│
│ WriteSeed │ │ │GenerateEncryptedSeed│
│ PreSeed │ │ │(椭圆曲线点乘 + │
│ Transform │ │ │ AES-CTR 加密) │
│ SeedTransform│ │ │(96 bytes) │
│ KeyDerivation│ │ ├─────────────────────│
│ LutGenerator │ │ │IV + AES-CTR 加密 │
│ (60 bytes) │ │ │(key1, challenge) │
├──────────────┤ │ │(56 bytes) │
│EncryptBlocks │ │ └─────────────────────┘
│(AES-ECB 链) │ │
│(max 152 B) │ │
├──────────────┤ │
│EncryptFinal │ │
│Block (16 B) │ │
└──────────────┘ │
│ │
└──────────┬──────────────────┘
│
┌──────────┴──────────┐
│ DeriveSessionKey │
│ (HMAC-SHA256) │
│ → 24-byte session │
│ key │
└─────────────────────┘
3.2 辅助机制
| 机制 | 入口函数 | 用途 |
|---|---|---|
| 密码鉴权 | s7_solve_legitimate_challenge_* |
基于密码的二次身份验证 |
| 数据包完整性 | s7_calculate_packet_digest |
每个 PDU 的 HMAC-SHA256 校验 |
| 公钥存储 | s7_public_key_store_* |
指纹→公钥查找 |
4. 第一阶段:密钥派生变换流水线
4.1 Monolith 模块
Monolith 是认证算法中最基础的计算单元,共 11 个,每个都是无状态的纯函数:
void MonolithN(uint32_t* dst, const uint32_t* src)
Monolith 规格表
| 编号 | 源大小 (bytes) | 目标大小 (bytes) | 用途 |
|---|---|---|---|
| 1 | 72 | 72 | 自循环 (Loop) + Checksum 变换 |
| 2 | 72 | 20 | Seed 变换校验 |
| 3 | 168 | 144 | Transform7 内部 |
| 4 | 144 | 72 | Transform7 内部 |
| 5 | 216 | 48 | Transform7 内部 |
| 6 | 216 | 144 | Transform7 内部 |
| 7 | 96 | 144 | Transform7 内部 |
| 8 | 72 | 60 | Seed 变换最终步骤 |
| 9 | 788 | 24 | 最大 Monolith(11 个部分),PreSeed/KeyDerivation |
| 10 | 72 | 768 | KeyDerivation 初始化 + Transform13 |
| 11 | 120 | 20 | Seed 变换最终输出 |
Monolith 调度器
c
static inline void execute_monolith(int index, uint32_t* dst, const uint32_t* src)
{
switch (index) {
case 1: monolith1_execute(dst, src); break;
case 2: monolith2_execute(dst, src); break;
// ...
case 11: monolith11_execute(dst, src); break;
}
}
Monolith 的具体内部逻辑是西门子自定义的、高度混淆的数学运算,每个 Monolith 包含数百到数千次位运算、查找表索引和算术操作。Monolith 9 是最大的,源码被分割成 11 个源文件(Part1.cpp 到 Part11.cpp),对应 C# 原始代码中的 11 个子函数。
4.2 BigInt 大数运算层
BigInt 运算层提供 192-bit 整数的基本运算。所有运算操作 6-dword 编码格式。
4.2.1 编码转换
Prepare(6→5 dword,为乘法/平方准备):
- 将第 6 个 dword 的高 2-bit 嵌入到第 1 dword 的低位
- 各 bit 按特定分布嵌入到前 4 个 dword
Finalize(5→6 dword,还原标准格式):
- 从 5 个 dword 的各低 N 位提取 bit,拼装成第 6 个 dword
4.2.2 基本运算
BigIntAddition --- 192-bit 无进位加法 (6 dword):
result[i] = src1[i] + src2[i] (mod 2^32, 无进位传播)
BigIntSubtraction --- 192-bit 无借位减法 (6 dword):
result[i] = src1[i] - src2[i] (mod 2^32, 无借位传播)
BigIntMultiplication --- 学校算法乘法 (5×5 dword → 最多 10 dword):
for i in 0..4:
carry = 0
for j in 0..4:
product = a[i] * b[j] + result[i+j] + carry
result[i+j] = product.low32
carry = product.high32
result[i+5] = carry
BigIntSquare --- 平方运算,流程如下:
Step 1: Prepare(src) → 5-dword 格式
Step 2: u5_mul(src5, src5) → 12-dword 缓冲区
Step 3: 确定有效长度 (从高位找非零)
Step 4: Compress (最多 2 次,使用 OpenSSL BIGNUM mod 0x2F 乘法)
Step 5: Finalize → 6-dword 格式
4.2.3 位旋转
RotateLeft31 --- 128-bit 整体左旋 31 位:
carry = data[0] >> 1
data[0] = (data[0] << 31) | (data[1] >> 1)
data[1] = (data[1] << 31) | (data[2] >> 1)
data[2] = (data[2] << 31) | (data[3] >> 1)
data[3] = (data[3] << 31) | carry
此旋转在 AES-ECB 加密链中用于每次加密后的 IV 更新。
4.2.4 Transform12 --- BigInt 运算虚拟机
Transform12 是一个紧凑的字节码虚拟机 ,从 Transform12Metadata 读取 32-bit 操作码,解码并分派到 BigInt 运算:
操作码格式 (32-bit):
bits [31:30] = 操作类型 (0=乘法, 1=平方, 2=加法, 3=减法)
bits [29:22] = 目标索引 (context buffer)
bits [21:11] = 源操作数1索引 (context 或 bigint_data)
bits [10:0] = 源操作数2索引 (context 或 bigint_data)
寻址模式:
- 索引 < 0x100: 指向 context buffer 偏移
index * 24 - 索引 >= 0x100: 指向静态 BigInt 数据表偏移
(index - 0x100) * 24
4.3 Transform 变换流水线
4.3.1 PreSeedTransform(预种子变换)
输入 : 24-byte Key1(随机密钥)
输出 : 60-byte 变换结果 (3 轮)
算法:
work[0..191] = kTransform1Data (192 dwords, 静态常量)
work[192] = 0x4F5BB379 (魔法后缀1)
work[193] = 0x90BA725F (魔法后缀2)
work[194] = 0x36A4D7BB (魔法后缀3)
for i in 0..2:
work[192] = src[i*2] // 加载 key1 的 2 dword
work[193] = src[i*2+1]
monolith9_execute(mono9_dst, work)
复制 6 dwords (i<2) 或 3 dwords (i=2) 到输出
4.3.2 SeedTransform(种子变换)
输入 : Public Key (40 bytes) + PreSeedTransform 输出 (60 bytes)
输出 : 60 bytes (20 bytes 种子 + 20 bytes PRNG1 副本)
核心: 椭圆曲线标量乘法的西门子自定义实现
Loop:
PRNG1 = RAND(20 bytes)
PRNG2 = RAND(20 bytes)
Transform7(PRNG1, PRNG2, kTransform7Data[0xD8..])
Monolith1.Loop (自循环直到稳定)
Monolith2
while (Monolith2 输出全零)
输出[0x14..0x27] = Monolith2 输出 (20 bytes)
输出[0x28..0x3B] = PRNG1 (20 bytes)
第二轮:
Transform7(PRNG1, PRNG2, publicKey)
Monolith1.Loop
padding[20] + Monolith8 → Transform13 + Monolith11
输出[0..0x13] = Monolith11 输出 (20 bytes)
4.3.3 KeyDerivationTransform(密钥派生变换)
输入 : 60-byte PreSeedTransform 输出
输出 : 48 bytes (加密密钥 16B + 校验密钥 16B + LUT 种子 16B)
算法:
buf1[0..2] = {0xFFFFFFFF, 0xFFFFFFFF, 0x0000FFFF}
buf1[3..17] = src (60 bytes)
Monolith10(buf2, buf1)
for i in 0..5:
buf2[192..196] = kSharedData[i*2..i*2+1] + kSharedData[18+i*3..18+i*3+2]
Monolith9(mono9_dst, buf2)
dst32[i*2] = mono9_dst[0]
dst32[i*2+1] = mono9_dst[1]
if i == 2:
buf1[0..2] = {0, 0, 0}
Monolith10(buf2, buf1) // 重置状态
输出布局:
dst[0..15] = ChallengeEncryptionKey (AES-128 密钥)
dst[16..31] = ChecksumEncryptionKey (校验加密密钥)
dst[32..47] = LutGeneratorSeed (LUT 种子)
4.3.4 LutGenerator(查找表生成器)
输入 : 16-byte 种子(通常来自 KeyDerivationTransform 输出32...47)
输出 : 0x1000 bytes (4096 bytes) GF(2^128) 乘法查找表
算法:
多项式: P(x) = x^128 + x^15 + x^2 + 1 (即 0x100000000000000000000000000000087)
表示为 make128(0x00008005, 0, 0x00000001, 0)
初始化:
dst[0..3] = 0
dst[4..7] = src (种子)
for i in 1, 2, 4, 8, 16, 32, 64: // 倍增
multiplicand = dst[i*4..i*4+3]
product = u128_shl1(multiplicand)
if u128_msb(multiplicand):
product ^= POLYNOMIAL
dst[i*2*4..i*2*4+3] = product
for j in 1..i-1:
dst[(i*2+j)*4..] = dst[j*4..] ^ product // XOR 生成
这是一个 GF(2^128) 乘法表,用于 ChecksumTransform 中的快速伽罗瓦域乘法。
4.3.5 ChecksumTransform(校验和变换)
输入 : 16-byte 密钥 + 4096-byte LUT
输出 : 16-byte 校验和
算法:
work[0..7] = 0
// 处理高 24 bits (i = 0x18, 0x10, 0x08)
for i in 24, 16, 8:
for j in 0..3:
lut_idx = ((key[j] >> i) & 0xFF) << 2
work[j..j+3] ^= lut[lut_idx..lut_idx+3]
// 右旋转 8 bits
for j in 7..1:
work[j] = (work[j-1] >> 24) | (work[j] << 8)
work[0] <<= 8
// 处理低 8 bits
for j in 0..3:
lut_idx = (key[j] & 0xFF) << 2
work[j..j+3] ^= lut[lut_idx..lut_idx+3]
// 最终混合
temp = (work[7] >> 13 ^ work[7]) >> 17 ^ work[4] ^ work[7]
dst[0] = ((temp << 13) ^ temp) << 2 ^ work[0] ^ temp
dst[1] = ((temp >> 13) ^ temp) >> 17 ^ ((work[5] << 13) ^ work[5]) << 2 ^ work[1] ^ temp ^ work[5]
dst[2] = ((work[5] >> 13) ^ work[5]) >> 17 ^ ((work[6] << 13) ^ work[6]) << 2 ^ work[2] ^ work[5] ^ work[6]
dst[3] = ((work[6] >> 13) ^ work[6]) >> 17 ^ ((work[7] << 13) ^ work[7]) << 2 ^ work[3] ^ work[6] ^ work[7]
4.3.6 Transform7(工作缓冲区编排器)
输入 : 2 个 20-byte PRNG 值 + 源数据
输出 : 72 bytes
架构: 600-dword 工作缓冲区 + Monolith 3-7 调度
Transform7 是一个高度复杂的编排函数,在一个 600-dword 工作缓冲区中协调 Monoliths 3-7、Transform12 和 BigIntAddition 的执行。它使用静态数据表 kTransform7Data(含 3 个子数组)来控制操作流程。
4.3.7 Transform13(后种子变换)
输入 : 60-byte Monolith8 输出
输出: 60-byte (经由 Monoliths 9+10 + SharedData)
Transform13 是 SeedTransform 的最后一步,在 Monolith8 之后执行,使用 SharedData 和 Monolith 9+10 进行最终变换。
5. 第二阶段:加密认证数据生成
5.1 AES-128-ECB 加密链(仅 Real PLC 路径)
Real PLC 认证路径使用 AES-128-ECB 的自定义链接模式来加密 Key2:
状态初始化:
IV = RAND(16 bytes)
AES_Key = ChallengeEncryptionKey (来自 KeyDerivationTransform)
Checksum_Key = ChecksumEncryptionKey
LUT = LutGenerator(Seed)
Checksum = ChecksumTransform(IV, LUT)
步骤 1: 写入 IV (16 bytes)
blob[offset..offset+15] = IV
步骤 2: 加密 Challenge (16 bytes)
ciphertext = AES-ECB(AES_Key, IV)
ciphertext ^= challenge[2..17] // 取 challenge 的第 2-18 字节
blob[offset..offset+15] = ciphertext
RotateLeft31(IV) // IV 左旋 31 位
Checksum ^= ciphertext
Checksum = ChecksumTransform(Checksum, LUT)
步骤 3: 加密 Key2 的每个完整 16-byte 块
for each 16-byte block in key2:
ciphertext = AES-ECB(AES_Key, IV)
ciphertext ^= key2_block
blob[offset..offset+15] = ciphertext
RotateLeft31(IV)
Checksum ^= ciphertext
Checksum = ChecksumTransform(Checksum, LUT)
步骤 4: 加密 Key2 的剩余字节
if key2.len % 16 != 0:
ciphertext = AES-ECB(AES_Key, IV)
ciphertext[0..leftover-1] ^= key2[leftover_start..]
blob[offset..offset+leftover-1] = ciphertext[0..leftover-1]
// 零填充剩余 ciphertext
Checksum ^= ciphertext
Checksum = ChecksumTransform(Checksum, LUT)
步骤 5: 最终校验和加密
Checksum[3] ^= encrypted_bytes_count // 嵌入加密字节计数
Checksum = ChecksumTransform(Checksum, LUT)
AES_Key = ChecksumEncryptionKey // 切换到校验密钥
ciphertext = AES-ECB(Checksum_Key, Checksum)
blob[offset..offset+15] = ciphertext
关键设计特点:
- IV 旋转: 每次 AES-ECB 加密后对 IV 执行 RotateLeft31,形成自定义的 IV 更新机制(不同于标准的 CBC 模式)
- 双密钥: 主体数据使用 ChallengeEncryptionKey 加密,最终校验和使用 ChecksumEncryptionKey 加密,实现密钥分离
- 加密计数嵌入: 将累计加密字节数 XOR 到 Checksum 中,提供完整性验证
5.2 Blob 元数据头
所有认证 Blob 都以 48 字节的元数据头开始:
┌─────────┬──────┬────────────────────────────────┐
│ 偏移 │ 大小 │ 内容 │
├─────────┼──────┼────────────────────────────────┤
│ 0x00 │ 4 │ Magic Number: 0xFEE1DEAD │
│ 0x04 │ 4 │ EncryptedBlobLength │
│ 0x08 │ 4 │ SecurityKeyVersion = 1 │
│ 0x0C │ 4 │ SecurityLevel = 1 (LegacyCSI) │
│ 0x10 │ 8 │ SymmetricKeyId │
│ │ │ = SHA-256(key1 || "DERIVE")[:8]│
│ 0x18 │ 4 │ KeyTypeSymmetricSessionKey │
│ │ │ = 0x01 | FamilyFlags │
│ 0x1C │ 4 │ Reserved │
│ 0x20 │ 8 │ PublicKeyId │
│ │ │ = SHA-256(pubkey || "DERIVE")[:8]│
│ 0x28 │ 4 │ KeyTypeCommPublicKey │
│ │ │ = 0x10 | FamilyFlags │
│ 0x2C │ 4 │ Reserved │
└─────────┴──────┴────────────────────────────────┘
FamilyFlags 映射:
S7_FAMILY_S71500 → 0x0000
S7_FAMILY_S71200 → 0x0100
S7_FAMILY_PLCSIM → 0x0300
5.3 AES-CTR 与 HarpoHash 校验机制(PLCSIM 路径)
PLCSIM 路径使用 AES-128-CTR 模式代替 Real PLC 的 AES-ECB 链。
5.3.1 HarpoAesCtr 初始化
Init(iv, iv_len):
1. AES-ECB(zeros) → seed
2. GenerateLookupTable(seed) → 4096-byte LUT
3. 处理 IV 的 16-byte 块:
for each block:
ivExtension ^= block
HarpoHash(ivExtension, LUT)
4. ivExtension ^= iv_len_bits (嵌入 IV 长度)
5. HarpoHash(ivExtension, LUT)
6. counter = ivExtension
5.3.2 HarpoAesCtr 加密流程
EncryptCtr(plaintext, destination):
// 处理部分块
if partial_block_remaining:
for each byte in partial:
dst[i] = aes2[i] ^ plaintext[i]
aes3[i] ^= dst[i]
HarpoHash(aes3, LUT)
// 处理完整 16-byte 块
for each 16-byte block:
IncrementCounter()
aes2 = AES-ECB(counter)
dst[block] = plaintext[block] ^ aes2
aes3 ^= dst[block]
HarpoHash(aes3, LUT)
// 处理剩余字节
if remaining:
IncrementCounter()
aes2 = AES-ECB(counter)
逐字节 XOR
5.3.3 HarpoHash --- 自定义 LUT 哈希
HarpoHash 是西门子自定义的 16-byte → 16-byte 哈希函数,基于 AES-ECB 种子生成的 4096-byte 查找表:
c
void Hash(output, lut):
t1 = t2 = t3 = t4 = 0
for i in 15 down to 0:
v4 = output[i]
t5 = (v4 << 4) / 4 // 索引到 LUT (每项 4 dwords)
t1 = (t1 << 8) ^ LutSeed[t4 >> 24] ^ lut[t5]
t2 = ((t2 << 8) | (t1_prev >> 24)) ^ lut[t5 + 1]
t3 = ((t3 << 8) | (t2_prev >> 24)) ^ lut[t5 + 2]
t4 = ((t4 << 8) | (t3_prev >> 24)) ^ lut[t5 + 3]
output[0..3] = {t1, t2, t3, t4}
5.3.4 Checksum 计算
CalculateChecksum(destination):
// 确保所有数据都被哈希
if partial_block_remaining:
HarpoHash(aes3, LUT)
// 嵌入加密计数
aes3[0xC..0xF] ^= (m_var2 << 3) // bit 计数
aes3[0x4..0x7] ^= (m_var1 << 3)
HarpoHash(aes3, LUT)
// 加密 IV extension 作为最终校验值
aes2 = AES-ECB(ivExtension)
checksum = aes2 ^ aes3
6. 第三阶段:Session Key 派生
Session Key 的派生使用 HMAC-SHA256,将 Key1/Key2 与 PLC Challenge 绑定:
6.1 Challenge 指纹计算 (Fingerprint)
首先对 Challenge 计算 8-byte 指纹:
Input: challenge (20 bytes from PLC)
Step 1: 初始化上下文
small_ctx[256] = {0}
small_ctx[0..15] = challenge[2..17] // 使用 challenge 的第 2-18 字节
big_ctx[47] = kFingerprintBigCtxInit // 静态初始化数据 (188 bytes)
Step 2: 20 轮 FingerprintSubProcedure
for mutation in 0..19:
// 读取 small_ctx 中的半字节,解码操作
for each triple (pwVar0, pwVar1, pwVar2) in FingerprintData1[mutation]:
static3 = ((pwVar1 & 0xF) | (pwVar0 << 4)) & 0xFF
t1 = (static3 >> 3) + (ctx_offset >> 2)
mod = t1 % 0x2F
static6 = big_ctx[mod] ^ xor_magic
t4 = ((7 - (pwVar1 & 7)) * 4) & 0x1F
static5 = static6 >> t4
// 更新 small_ctx
fVal = ((data2_bytes[data2_index] >> pwVarMask(pwVar1)) ^ static5) & 0xF
small_ctx[ctx_buffer_index] = (fVal << bit_shift) | (masked_small_ctx)
ctx_offset += 0x80
// 突变 big_ctx
ContextMutator.mutate(big_ctx, mutation)
Step 3: 提取最终指纹
fingerprint[0] = (small_ctx[93] << 4) | (small_ctx[224] >> 4)
fingerprint[1] = complicated_bit_mix(small_ctx[189], small_ctx[53])
fingerprint[2] = complicated_bit_mix(small_ctx[119], small_ctx[86])
fingerprint[3] = complicated_bit_mix(small_ctx[83], small_ctx[33])
fingerprint[4] = complicated_bit_mix(small_ctx[229], small_ctx[58])
fingerprint[5] = complicated_bit_mix(small_ctx[69], small_ctx[165])
fingerprint[6] = complicated_bit_mix(small_ctx[63], small_ctx[89])
fingerprint[7] = complicated_bit_mix(small_ctx[172], small_ctx[247])
FingerprintSubProcedure 类似于一个字节码解释器,从静态数据表 (kFingerprintData1, kFingerprintData2) 读取操作三元组,解码出位操作和 LUT 索引,然后更新 small_ctx 的特定半字节。
ContextMutator 定义了 20 种不同的突变函数(mutation 0-19),每轮对 big_ctx 的特定位置执行乘/加/XOR 操作:
mutation 0: data[0]*=0xc551c475, data[4]+=0x5a3ce23f, data[6]+=0x24c4b704, ...
mutation 1: data[0]*=0x42967e37, data[1]+=0x51968185, data[5]+=0x987ed03c, ...
...
mutation 19: data[25]+=0xfb33f767, data[40]*=0x1c24a56d, ...
6.2 HMAC-SHA256 Session Key 派生
Source[0..7] = Fingerprint(challenge)
Source[8..23] = challenge[2..17] // challenge 核心数据部分
SessionKey = HMAC-SHA256(Key1/Key2, Source)[0..23]
Real PLC 路径 : 使用 Key2 (随机生成的 24 字节密钥)
PLCSIM 路径: 使用 Key1 (随机生成的 24 字节密钥)
7. 完整认证流程
7.1 TCP 连接建立与 TLS 协商
Client PLC (Port 102)
| |
|--- TCP SYN ------------------>|
|<-- TCP SYN-ACK ---------------|
|--- TCP ACK ------------------>|
| |
|--- COTP Connect Request ----->|
|<-- COTP Connect Confirm ------|
| |
|--- S7CommPlus Setup --------->|
| (ProtocolVersion, |
| MaxPduSize, ...) |
|<-- S7CommPlus Setup Response -|
| (ProtocolVersion, |
| SessionId, |
| PLC Fingerprint, |
| ServerSessionInfo) |
| |
[If ProtocolVersion.V2: TLS 握手]
| |
|--- GetVarSubstreamed --------->|
| (ServerSessionRequest) |
|<-- GetVarSubstreamed Response |
| (Challenge 20 bytes) |
| |
|=== 开始认证流程 (见 7.2/7.3) ==|
| |
|--- SetVariable ------------->|
| (SessionKey blob) |
|<-- SetVariable Response ------|
| |
|=== 后续数据交换使用新会话密钥 ==|
7.2 Real PLC (S7-1200/1500) 认证流程
┌─────────────────────────────────────────────────────────────┐
│ Real PLC 认证流程图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入: │
│ • challenge (20 bytes from PLC) │
│ • public_key (40 bytes, from fingerprint lookup) │
│ • family (S7_FAMILY_S71500 or S7_FAMILY_S71200) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Step 1: 生成随机密钥 │ │
│ │ Key1 = RAND(24 bytes) │ │
│ │ Key2 = RAND(24 bytes) │ │
│ │ IV = RAND(16 bytes) │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 2: 写入元数据头 │ │
│ │ BlobMetadataWriter(blob, pubkey, key1, fam) │ │
│ │ → 48 bytes (Magic + KeyIDs + KeyTypes) │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 3: 写入种子数据 (WriteSeed) │ │
│ │ │ │
│ │ 3a. PreSeedTransform(key1) → t1 (60 bytes) │ │
│ │ Monolith9 + Transform1Data + MagicSuffix │ │
│ │ │ │
│ │ 3b. SeedTransform(pubkey, t1) → seed (60 B) │ │
│ │ Transform7 → M1.Loop → M2 → M8 → T13 → M11│ │
│ │ (含椭圆曲线标量乘法) │ │
│ │ │ │
│ │ 3c. KeyDerivationTransform(t1) → keys (48 B) │ │
│ │ extract ChallengeEncKey, ChecksumEncKey, │ │
│ │ LutSeed │ │
│ │ │ │
│ │ 3d. LutGenerator(LutSeed) → LUT (4096 bytes) │ │
│ │ GF(2^128) 乘法表 │ │
│ │ │ │
│ │ 3e. ChecksumTransform(IV, LUT) → init_csum │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 4: 加密完整块 (EncryptFullBlocks) │ │
│ │ │ │
│ │ 4a. 写入 IV (16 bytes) │ │
│ │ │ │
│ │ 4b. 加密 Challenge: │ │
│ │ ciphertext = AES-ECB(EncKey, IV) │ │
│ │ ciphertext ^= challenge[2..18] │ │
│ │ RotateLeft31(IV) │ │
│ │ UpdateChecksum(ciphertext, LUT) │ │
│ │ │ │
│ │ 4c. 加密 Key2 的每个 16-byte 块: │ │
│ │ for each block: │ │
│ │ ciphertext = AES-ECB(EncKey, IV) │ │
│ │ ciphertext ^= key2_block │ │
│ │ RotateLeft31(IV) │ │
│ │ UpdateChecksum(ciphertext, LUT) │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 5: 加密尾块 (EncryptFinalBlock) │ │
│ │ │ │
│ │ 5a. 加密 Key2 剩余字节 (0-15 bytes) │ │
│ │ 零填充 + UpdateChecksum │ │
│ │ │ │
│ │ 5b. 嵌入加密字节计数到 Checksum │ │
│ │ csum[3] ^= encrypted_bytes_count │ │
│ │ ChecksumTransform(csum, LUT) │ │
│ │ │ │
│ │ 5c. 切换到 Checksum 密钥加密最终校验和 │ │
│ │ ciphertext = AES-ECB(CsumKey, csum) │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 6: 派生 Session Key │ │
│ │ │ │
│ │ Source[0..7] = Fingerprint(challenge) │ │
│ │ Source[8..23] = challenge[2..18] │ │
│ │ │ │
│ │ SessionKey = HMAC-SHA256(Key2, Source)[0..23] │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ 输出: │
│ • key_blob (180 bytes, 已加密) │
│ • session_key (24 bytes) │
└─────────────────────────────────────────────────────────────┘
7.3 PLCSIM 仿真器认证流程
┌─────────────────────────────────────────────────────────────┐
│ PLCSIM 认证流程图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入: │
│ • challenge (20 bytes from PLCSIM) │
│ • public_key (64 bytes) │
│ • family = S7_FAMILY_PLCSIM │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Step 1: 生成随机密钥 │ │
│ │ Key1 = RAND(24 bytes) │ │
│ │ Key2 = RAND(24 bytes) │ │
│ │ IV = RAND(16 bytes) │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 2: 派生 Challenge 加密密钥 │ │
│ │ │ │
│ │ Plaintext[0..23] = Key2 │ │
│ │ Plaintext[24..39] = {0x01,0x02,0x03,0x04, │ │
│ │ 0x05,0x06,0x07,0x08, │ │
│ │ 0x09,0x0A,0x0B,0x0C, │ │
│ │ 0x0D,0x0E,0x0F,0x00} │ │
│ │ │ │
│ │ ChallengeKey = SHA-256(Plaintext40)[0..15] │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 3: 写入元数据头 │ │
│ │ BlobMetadataWriter(blob, pubkey, key1, fam) │ │
│ │ → 48 bytes │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 4: 生成加密种子 (GenerateEncryptedSeed) │ │
│ │ │ │
│ │ 4a. OmsReverseRows(pubkey) → start_bytes │ │
│ │ 大端序反转 64-byte 公钥 │ │
│ │ │ │
│ │ 4b. 椭圆曲线标量乘法: │ │
│ │ do: │ │
│ │ scalar = RAND(32 B) [大端序] │ │
│ │ SeedFunction2(Seed1, scalar) → pt1 │ │
│ │ SeedFunction2(start_bytes, scalar)→pt2 │ │
│ │ while (pt1[16]==1 || pt2[16]==1 || ZeroCheck(pt2)) │
│ │ │ │
│ │ 输出 = [pt1 反转, pt2 反转] (64 bytes) │ │
│ │ │ │
│ │ 4c. 派生种子加密密钥: │ │
│ │ src = [v4 反转, 输出, counter] │ │
│ │ SHA-256(src) × 2 → 48 bytes (key+iv) │ │
│ │ │ │
│ │ 4d. AES-CTR 加密 challenge_enc_key → 16 bytes │ │
│ │ CalculateChecksum → 16 bytes │ │
│ │ Total: 96 bytes │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 5: 加密 Key1 和 Challenge │ │
│ │ │ │
│ │ 5a. 写入 IV (16 bytes) │ │
│ │ │ │
│ │ 5b. AES-CTR 加密 challenge[2..18] (16 bytes) │ │
│ │ │ │
│ │ 5c. AES-CTR 加密 Key1 (24 bytes) │ │
│ │ │ │
│ │ 5d. CalculateChecksum (16 bytes) │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 6: 派生 Session Key │ │
│ │ │ │
│ │ (与 Real PLC 相同,但使用 Key1 而非 Key2) │ │
│ │ SessionKey = HMAC-SHA256(Key1, Source)[0..23] │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ 输出: │
│ • key_blob (216 bytes, 已加密) │
│ • session_key (24 bytes) │
└─────────────────────────────────────────────────────────────┘
7.4 密钥交换时间线
在整个密钥交换期间,客户端必须持有 rwLockSlim 写锁,遵循 AGLink 模式:
┌──────────────────────────────────────────────────────────────────┐
│ 密钥交换时间线 (AGLink 模式) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ EnterWriteLock() │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. Challenge Request │ │
│ │ GetVarSubstreamedRequest(ServerSessionRequest) │ │
│ │ SequenceNumber: N (WithNextSequenceNumber=true) │ │
│ │ │ │
│ │ 2. Wait Response │ │
│ │ PLC 返回 Challenge (20 bytes) │ │
│ │ SequenceNumber: 同步自 PLC 响应 │ │
│ │ m_SequenceNumber = challengeRes.SequenceNumber │ │
│ │ │ │
│ │ 3. Authenticate (2-5 ms, 锁内) │ │
│ │ LegacyAuthenticationScheme.Authenticate(...) │ │
│ │ │ │
│ │ 4. Session Key Update │ │
│ │ SetVariableRequest(SessionKey) │ │
│ │ SequenceNumber: N+1 (WithNextSequenceNumber=true) │ │
│ │ │ │
│ │ 5. Wait Response │ │
│ │ PLC 确认新的会话密钥 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ExitWriteLock() │
│ │
│ ⚠️ 关键保证: │
│ • 整个周期内持有写锁,无竞态条件 │
│ • SequenceNumber 同步在锁内完成,不会倒退 │
│ • 原子更新 m_SessionKey,不会出现不一致状态 │
│ • 失败不断开连接,等待下一个周期重试 │
└──────────────────────────────────────────────────────────────────┘
8. Session Key 轮转机制
8.1 定时轮转策略
S7CommPlus 协议的 Session Key 有生命周期限制(PLC 端约 30 分钟)。客户端使用 System.Timers.Timer 定时触发密钥轮转:
Timer 配置:
Interval: m_Internal (可配置,默认约 25 分钟)
AutoReset: true
线程池回调: OnTimedEvent
8.2 轮转流程
csharp
void OnTimedEvent(sender, e)
{
// 重入保护: 原子 CAS,防止并发
if (Interlocked.CompareExchange(ref m_IsRotatingKey, 1, 0) != 0)
return; // 已有轮转在进行中
try {
// 条件检查
if (m_UseTls || OnlySecurePGOrPCAndHMI || m_SessionId == 0)
return;
byte protoVersion = m_UseTls ? V2 : V3;
// ═══ AGLink 模式: 全程持写锁 ═══
rwLockSlim.EnterWriteLock();
try {
// Phase 1: 获取 Challenge
SendRequestLocked(GetVarSubstreamedRequest)
WaitResponseLocked()
// Phase 2: 认证 + 序列号同步
Authenticate(keyBlob, sessionKey, challenge, publicKey)
m_SequenceNumber = response.SequenceNumber // 协议同步
// Phase 3: 发送新会话密钥
SendRequestLocked(SetVariableRequest)
WaitResponseLocked()
m_SessionKey = sessionKey // 原子更新
}
finally {
rwLockSlim.ExitWriteLock();
}
}
finally {
Interlocked.Exchange(ref m_IsRotatingKey, 0);
}
}
8.3 竞态防护设计
| 防护层 | 机制 | 保护范围 |
|---|---|---|
| 重入保护 | Interlocked.CompareExchange on m_IsRotatingKey |
防止并发 timer 回调 |
| 序列号保护 | rwLockSlim.EnterWriteLock() 全程持锁 |
防止数据操作与轮转交叉 |
| 原子更新 | m_SessionKey = sessionKey 在锁内赋值 |
防止读到不完整的新密钥 |
| 失败容错 | 错误时 return 不 disconnect |
允许下次周期重试 |
| 协议同步 | m_SequenceNumber = response.SequenceNumber |
PLC 期望的序列号锚定 |
8.4 SequenceNumber 管理
SequenceNumber 是 S7CommPlus 协议的关键序列号,用于请求-响应对的匹配:
正常数据操作:
SendRequestLocked() → GetNextSequenceNumber() → N
WaitResponseLocked() → 验证响应的 IntegrityId
密钥轮转 (锁内):
challenge: GetNextSequenceNumber() → N
同步: m_SequenceNumber = PLC_Expected → 锚定 PLC 期望值
setVar: GetNextSequenceNumber() → PLC_Expected + 1
为什么需要同步:PLC 在每次响应中返回它期望的下一个 SequenceNumber。客户端如果不同步,下一次请求的序列号将与 PLC 期望不符,导致通信错误。
为什么不倒退:同步操作在写锁内完成且紧接着立即发送 SetVariable 请求(也是锁内),没有其他线程可以在中间插入数据请求并消耗序列号。这是 AGLink 模式的精髓 ------ 全程持锁消除竞态。
9. 数据包完整性保护 (Packet Digest)
9.1 完整性保护机制
S7CommPlus V3 协议在每个 PDU 的尾部附加 HMAC-SHA256 完整性校验码:
PDU 结构 (V3):
┌─────────────┬──────────────┬───────────────┬────────────┬──────────┐
│ SessionId │ SequenceNum │ IntegrityId │ Payload │ Digest │
│ (4 bytes) │ (2 bytes) │ (4 bytes) │ (variable) │ (32 B) │
└─────────────┴──────────────┴───────────────┴────────────┴──────────┘
│ │
└── HMAC 覆盖 ──┘
9.2 计算过程
c
int s7_calculate_packet_digest(
uint8_t* digest, // 输出: 32 bytes
const uint8_t* data, // 输入: PDU 数据 (不含 Digest 字段)
const uint8_t* session_key // 24 bytes
) {
// digest = HMAC-SHA256(session_key, data)
HMAC(EVP_sha256(), session_key, 24, data, data_len, digest, &md_len);
}
9.3 IntegrityId 校验
发送时:
IntegrityId = (uint)(SessionId ^ SequenceNumber ^ IntegrityPart)
接收时:
reqIntegCheck = request.SequenceNumber + request.IntegrityId
if (response.IntegrityId != reqIntegCheck)
// Integrity 不匹配(可见但不中断)
IntegrityPart 是 HMAC-SHA256 Digest 的前 4 字节,作为额外的完整性因子。
10. 密码鉴权机制 (Legitimation Scheme)
密码鉴权是可选的二次认证,在 Session Key 认证之后执行。它使用基于密码的挑战-响应协议。
10.1 总体流程
┌─────────────────────────────────────────────────────────────┐
│ Legitimation 流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 输入: │
│ • challenge (20 bytes from PLC) │
│ • session_key (24 bytes, from initial auth) │
│ • password_hash (20 bytes, SHA-1 of password) │
│ • public_key │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Step 1: 派生 Legitimation 密钥 │ │
│ │ source = session_key || "MISTRUST" │ │
│ │ LegitimationKey = SHA-256(source)[4..28] │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 2: 派生 Challenge 加密密钥 │ │
│ │ (与 PLCSIM 认证相同) │ │
│ │ ChallengeKey = SHA-256(LegKey || │ │
│ │ {0x01,0x02,...,0x0F,0x00})[0..15] │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ Step 3: 写入 BEEF 种子元数据 │ │
│ │ Magic: 0xDEADBEEF │ │
│ │ KeyIDs for pubkey + sym_key │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ │
│ ┌───────────┴──────────┐ │
│ │ │ │
│ ┌────▼──────┐ ┌──────▼────────┐ │
│ │ PLCSIM │ │ Real PLC │ │
│ │ 路径 │ │ 路径 │ │
│ ├───────────┤ ├───────────────┤ │
│ │ 4a. Gen │ │ 4b. TPre + │ │
│ │ Encrypted │ │ SeedTransform │ │
│ │ Seed │ │ + KeyDeriv │ │
│ │ (96 B) │ │ + LUT │ │
│ │ │ │ │ │
│ │ 5a. Write │ │ 5b. Write │ │
│ │ Fragment │ │ Fragment #0 │ │
│ │ Meta + IV │ │ Meta + IV │ │
│ │ │ │ │ │
│ │ 6a. AES- │ │ 6b. AES-ECB │ │
│ │ CTR encrypt│ │ encrypt │ │
│ │ zero_blk │ │ hash+challenge│ │
│ │ + hash │ │ + key2 blocks │ │
│ │ + chg[0.. │ │ + final csum │ │
│ │ 11] │ │ │ │
│ │ │ │ │ │
│ │ 7a. Calc │ │ 7b. Write │ │
│ │ Checksum │ │ Empty Final │ │
│ │ + empty │ │ Fragment │ │
│ │ final frag│ │ │ │
│ └───────────┘ └───────────────┘ │
│ │
│ 输出: │
│ • Real PLC: 248 bytes │
│ • PLCSIM: 284 bytes │
└─────────────────────────────────────────────────────────────┘
10.2 BEEF 片段协议
Legitimation Blob 使用 0xDEADBEEF 魔术数标记片段边界:
Fragment #0: Seed Metadata (64 bytes)
Fragment #1: IV + Encrypted Challenge (PLCSIM: 16+16+16+16=64 bytes)
Fragment #2: Checksum (PLCSIM: 16 bytes)
Fragment #3: Empty Final (0 bytes, 终止标记)
每个片段有 12 字节头:[0xDEADBEEF, FragmentIndex, FragmentLength]
10.3 关键差异:Legitimation vs 初始认证
| 特性 | 初始认证 | Legitimation |
|---|---|---|
| 对称密钥来源 | 随机生成 Key1/Key2 | 从 Session Key 派生 (MISTRUST) |
| Challenge 加密方式 | AES-ECB 链 (Real PLC) 或 AES-CTR (PLCSIM) | 与对应设备类型相同 |
| Key2 内容 | 随机 24 bytes | password_hash(20) + challenge(20) |
| Blob 格式 | 标准元数据头 | BEEF 片段元数据头 |
| Magic | 0xFEE1DEAD | 0xDEADBEEF |
| 输出长度 | 180 (RealPlc) / 216 (PlcSim) | 248 (RealPlc) / 284 (PlcSim) |
11. 性能优化策略
11.1 优化层次
本实现进行了以下分层优化:
| 层级 | 优化措施 | 效果 |
|---|---|---|
| Tier 0: 编译器 | Release 构建,/O2 /LTCG | 基线:20-50ms |
| Tier 1a: 虚拟机 | Transform12 操作码预解码 | 消除运行时 switch 开销 |
| Tier 1b: Monolith | 静态调度,.h 内联定义 |
消除函数指针/虚表间接 |
| Tier 1c: OpenSSL | 静态链接,消除动态加载 | 消除 GetProcAddress 延迟 |
| Tier 2: SIMD | AVX2 向量化 Monolith + LUT | 4 路并行处理 dword |
| Tier 3: AES-NI | 硬件 AES 指令集 | 消除 OpenSSL EVP 开销 |
| 最终 | 所有优化叠加 | 2-5ms (10-20x 提升) |
11.2 关键热点优化
BigIntSquare (最大热点):
- 原始方案: OpenSSL BN_new → BN_lebin2bn → BN_sqr → BN_bn2lebinpad → BN_free
- 优化方案: 直接 5×5 dword 学校算法乘法 → Compress → Finalize
- 性能提升: ~90%(消除 5 次 OpenSSL 堆分配和上下文切换)
Transform12 (第二大热点):
- 原始方案: 循环中逐条解码操作码并 switch 分发
- 优化方案: 操作码字段预提取 + 位运算解码 + LTCG 内联
- 性能提升: ~70%(消除分支预测失败)
AES-ECB 调用:
- 原始方案: OpenSSL EVP_EncryptInit_ex → EVP_EncryptUpdate (每次 16 bytes 都走完整 EVP 管线)
- 优化方案: AES-NI 指令直接
_mm_aesenc_si128 - 性能提升: ~85%(消除 EVP 框架开销)
11.3 Monolith 1 Loop 优化
Monolith 1 的 Loop 模式(自循环直到收敛)是独特的性能挑战:
- 使用
HARPOS7_FORCEINLINE和内联的 Monolith 调度器 - AVX2 路径下每次循环处理更多 dword
- LTCG 允许跨编译单元的积极内联
12. 安全分析
12.1 加密原语使用
| 算法 | 用途 | 密钥长度 | 安全评估 |
|---|---|---|---|
| AES-128-ECB | 认证数据加密 | 128-bit | ⚠️ ECB 模式本身较弱,但因 IV 旋转形成自定义链式模式 |
| AES-128-CTR | PLCSIM 加密 | 128-bit | ✅ 标准流密码模式 |
| SHA-256 | 密钥 ID、密钥派生 | N/A | ✅ 抗碰撞 |
| HMAC-SHA256 | Session Key、Packet Digest | 192-bit | ✅ 消息认证码 |
| 自定义 HarpoHash | 校验和 | 4096-byte LUT | ⚠️ 非标准,安全性未公开审计 |
| GF(2^128) 乘法 | LUT 生成 | N/A | ✅ 标准伽罗瓦域运算 |
| 椭圆曲线点运算 | 种子生成 | 256-bit field | ⚠️ 曲线参数未公开,疑似 secp256k1 变体 |
12.2 密钥管理
- Key1/Key2 : 使用
RAND_bytes(OpenSSL CSPRNG) 生成,24 bytes,192-bit 熵 - Challenge Encryption Key: 从 KeyDerivationTransform 派生,16384 次 Monolith 操作后的输出
- Session Key: HMAC-SHA256(Key, Fingerprint||Challenge),绑定特定会话和挑战
- 密钥 ID: SHA-256 截断到 64-bit,用于 Blob 中的密钥引用
12.3 已知局限性
- ECB 的自定义链接: AES-ECB 配合 RotateLeft31 不是标准操作模式(如 CBC、GCM),缺乏形式化安全证明
- HarpoHash 的非标准性: 基于 LUT 的哈希函数未经过公开密码分析
- 会话密钥生命周期: 30 分钟固定周期可能在某些场景过长或过短
- SequenceNumber 为 16-bit: UInt16 可能在大流量场景下快速回绕,需确保回绕处理正确
附录 A: 术语表
| 术语 | 英文 | 说明 |
|---|---|---|
| 鉴权 | Authentication | 客户端向 PLC 证明身份的加密过程 |
| 会话密钥 | Session Key | 24-byte 对称密钥,用于后续通信的 HMAC 计算 |
| 挑战 | Challenge | PLC 返回的 20-byte 随机数据,用于认证绑定 |
| 公钥 | Public Key | PLC 的椭圆曲线公钥,Real PLC: 40 bytes, PLCSIM: 64 bytes |
| Blob | Encrypted Blob | 客户端生成的加密认证数据包 |
| Monolith | Monolith | 西门子自定义的无状态纯函数,大数运算的核心单元 |
| Transform | Transform | 编排多个 Monolith/BigInt 操作的高级变换函数 |
| 指纹 | Fingerprint | Challenge 的 8-byte 派生值,识别 PLC 类型和会话 |
| 摘要 | Digest | HMAC-SHA256 的 32-byte 输出,保护 PDU 完整性 |
| 密码鉴权 | Legitimation | 基于密码的可选二次认证 |
| 密钥轮转 | Key Rotation | 定期更新 Session Key 以维持会话安全性 |
附录 B: 源文件索引
C++ 原生库 (HarpoS7Native)
src/
├── api/harpos7_api.cpp # 公共 API 实现
├── auth/
│ ├── legacy_auth.cpp # 认证入口 (转发)
│ ├── real_plc_auth.cpp # Real PLC 认证编排
│ └── legitimate_scheme.cpp # 密码鉴权
├── aes/
│ ├── harpo_aes.cpp # AES-128-ECB 封装
│ ├── harpo_aes_ctr.cpp # 自定义 AES-CTR
│ └── harpo_hash.cpp # 自定义 LUT 哈希
├── bigint/
│ ├── bigint_operations.cpp # 编码转换 (Prepare/Finalize/Rotate)
│ └── bigint_compressor.cpp # 压缩 (OpenSSL BIGNUM 辅助)
├── crypto/
│ ├── packet_digest.cpp # HMAC-SHA256 数据包完整性
│ ├── key_utilities.cpp # 密钥派生 (Session/Legitimation)
│ ├── seed_utilities.cpp # 椭圆曲线种子生成
│ ├── fingerprint.cpp # Challenge 指纹
│ └── context_mutator.cpp # 指纹上下文突变 (20 种)
├── monoliths/
│ ├── monolith_hub.h # 调度器头文件
│ ├── monolith1.cpp ~ monolith11.cpp
│ ├── nine/part1.cpp ~ part11.cpp # Monolith 9 子部分
│ └── ten/part1.cpp ~ part3.cpp # Monolith 10 子部分
├── transforms/
│ ├── pre_seed_transform.cpp # PreSeed 变换
│ ├── seed_transform.cpp # Seed 变换
│ ├── key_derivation_transform.cpp # KeyDerivation 变换
│ ├── lut_generator.cpp # GF(2^128) LUT 生成
│ ├── checksum_transform.cpp # 校验和变换
│ ├── transform7.cpp # Transform7 编排器
│ ├── transform12.cpp # BigInt VM/分发器
│ ├── transform13.cpp # 后 Seed 变换
│ ├── bigint_addition.cpp # BigInt 加法
│ ├── bigint_subtraction.cpp # BigInt 减法
│ ├── bigint_multiplication.cpp # BigInt 乘法
│ └── bigint_square.cpp # BigInt 平方
├── metadata/
│ └── blob_metadata_writer.cpp # Blob 元数据写入
├── publickeys/
│ └── key_store.cpp # 公钥存储/查找
├── internal/
│ ├── harpos7_platform.h # 平台抽象 (MSVC/GCC/Clang)
│ └── span_helpers.h # 辅助函数
├── openssl/
│ └── openssl_loader.cpp # OpenSSL 动态/静态加载
└── data/
├── aes_consts.h # AES LUT 种子 (512 bytes)
├── bundled_keys.h # 内嵌公钥 (~36 keys)
├── fingerprint_consts.h # 指纹数据表
├── key_index.h # 密钥索引
├── seed_consts.h # 椭圆曲线常数 (Mask1-4, Seed1)
├── shared_data.h # 共享数据 (36 dwords)
├── transform1_data.h # Transform1 数据 (192 dwords)
├── transform7_data.h # Transform7 数据 (3 个数组)
├── transform12_data.h # Transform12 BigInt 数据
└── transform12_metadata.h # Transform12 操作码元数据
C# 驱动层 (S7CommPlusDriver)
S7CommPlusConnection.cs # 连接管理 + 密钥轮转
Native/HarpoS7Native.cs # P/Invoke 封装 (8 个 DllImport)
附录 C: 参考资料
- S7CommPlus 协议逆向工程: 通过 AGLink40_x64.dll 反汇编分析
- OpenSSL 文档: https://www.openssl.org/docs/
- Siemens S7-1200/1500 通信手册: 西门子工业在线支持
- GF(2^128) 乘法: NIST SP 800-38D (GCM 模式)
文档完成日期 : 2026-07-04
作者 : 基于 HarpoS7Native / S7CommPlusDriver 源码分析生成
许可: 仅供学习与研究使用