西门子S7CommPlus协议鉴权算法原理与流程详解

西门子 S7CommPlus 协议鉴权算法原理与流程详解

文档版本 : 1.0

适用设备 : Siemens S7-1200 / S7-1500 / PLCSIM / PLCSIM Advanced

协议版本: S7CommPlus V1 (0x01) / V2 (TLS, 0x02) / V3 (0x03)


目录

  1. 协议背景与概述
  2. 数据结构与常量定义
  3. 认证体系的整体架构
  4. 第一阶段:密钥派生变换流水线 (Transform Pipeline)
    • 4.1 [Monolith 模块](#Monolith 模块)
    • 4.2 [BigInt 大数运算层](#BigInt 大数运算层)
    • 4.3 [Transform 变换流水线](#Transform 变换流水线)
  5. 第二阶段:加密认证数据生成
    • 5.1 [AES-128-ECB 加密链](#AES-128-ECB 加密链)
    • 5.2 [Blob 元数据头](#Blob 元数据头)
    • 5.3 [AES-CTR 与 HarpoHash 校验机制](#AES-CTR 与 HarpoHash 校验机制)
  6. [第三阶段:Session Key 派生](#第三阶段:Session Key 派生)
  7. 完整认证流程
    • 7.1 [TCP 连接建立与 TLS 协商](#TCP 连接建立与 TLS 协商)
    • 7.2 [Real PLC (S7-1200/1500) 认证流程](#Real PLC (S7-1200/1500) 认证流程)
    • 7.3 [PLCSIM 仿真器认证流程](#PLCSIM 仿真器认证流程)
    • 7.4 密钥交换时间线
  8. [Session Key 轮转机制](#Session Key 轮转机制)
  9. 数据包完整性保护 (Packet Digest)
  10. 密码鉴权机制 (Legitimation Scheme)
  11. 性能优化策略
  12. 安全分析

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 已知局限性

  1. ECB 的自定义链接: AES-ECB 配合 RotateLeft31 不是标准操作模式(如 CBC、GCM),缺乏形式化安全证明
  2. HarpoHash 的非标准性: 基于 LUT 的哈希函数未经过公开密码分析
  3. 会话密钥生命周期: 30 分钟固定周期可能在某些场景过长或过短
  4. 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 源码分析生成

许可: 仅供学习与研究使用

相关推荐
yuhaiqiang2 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
硕风和炜2 小时前
【LeetCode: 2492. 两个城市间路径的最小分数 + DFS】
java·算法·leetcode·深度优先·dfs·bfs·并查集
我是一颗柠檬3 小时前
【Java项目技术亮点】加权轮询负载均衡算法
java·算法·负载均衡
灯厂码农3 小时前
C语言动态内存分配完全指南(malloc、calloc、realloc、free)
java·c语言·算法
geovindu4 小时前
python: Functional Options Pattern
开发语言·后端·python·设计模式·惯用法模式·函数式选项模式
HavenlonLabs4 小时前
Havenlon 对抗性完整(十七):安全不是“防住攻击”,而是控制失败方式
网络·人工智能·架构·安全威胁分析·安全架构·havenlon
凯瑟琳.奥古斯特4 小时前
K次取反最大化数组和解法(力扣1005)
开发语言·c++·算法·leetcode·职场和发展
fei_sun5 小时前
路径MTU发现
linux·运维·网络
Jerry5 小时前
LeetCode 203. 移除链表元素
算法