首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
密码应用
发新帖
3
1
[原创]西门子 S7CommPlus 协议鉴权算法原理与流程详解
发表于: 2026-7-4 17:13
2334
[原创]西门子 S7CommPlus 协议鉴权算法原理与流程详解
Coder_shen
2026-7-4 17:13
2334
# 西门子 S7CommPlus 协议鉴权算法原理与流程详解 > **文档版本**: 1.0 > **适用设备**: Siemens S7-1200 / S7-1500 / PLCSIM / PLCSIM Advanced > **协议版本**: S7CommPlus V1 (0x01) / V2 (TLS, 0x02) / V3 (0x03) --- ## 目录 1. [协议背景与概述](#1-协议背景与概述) 2. [数据结构与常量定义](#2-数据结构与常量定义) 3. [认证体系的整体架构](#3-认证体系的整体架构) 4. [第一阶段:密钥派生变换流水线 (Transform Pipeline)](#4-第一阶段密钥派生变换流水线) - 4.1 [Monolith 模块](#41-monolith-模块) - 4.2 [BigInt 大数运算层](#42-bigint-大数运算层) - 4.3 [Transform 变换流水线](#43-transform-变换流水线) 5. [第二阶段:加密认证数据生成](#5-第二阶段加密认证数据生成) - 5.1 [AES-128-ECB 加密链](#51-aes-128-ecb-加密链) - 5.2 [Blob 元数据头](#52-blob-元数据头) - 5.3 [AES-CTR 与 HarpoHash 校验机制](#53-aes-ctr-与-harpohash-校验机制) 6. [第三阶段:Session Key 派生](#6-第三阶段session-key-派生) 7. [完整认证流程](#7-完整认证流程) - 7.1 [TCP 连接建立与 TLS 协商](#71-tcp-连接建立与-tls-协商) - 7.2 [Real PLC (S7-1200/1500) 认证流程](#72-real-plc-s7-12001500-认证流程) - 7.3 [PLCSIM 仿真器认证流程](#73-plcsim-仿真器认证流程) - 7.4 [密钥交换时间线](#74-密钥交换时间线) 8. [Session Key 轮转机制](#8-session-key-轮转机制) 9. [数据包完整性保护 (Packet Digest)](#9-数据包完整性保护) 10. [密码鉴权机制 (Legitimation Scheme)](#10-密码鉴权机制) 11. [性能优化策略](#11-性能优化策略) 12. [安全分析](#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 嵌入到 dword[0] 的第 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 文档: <mark class="encrypted">293K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2G2M7r3g2F1M7%4y4D9i4K6u0W2L8%4u0Y4i4K6u0r3k6r3!0U0M7#2)9J5c8R3`.`.</mark> - Siemens S7-1200/1500 通信手册: 西门子工业在线支持 - GF(2^128) 乘法: NIST SP 800-38D (GCM 模式) --- > **文档完成日期**: 2026-07-04 > **作者**: 基于 HarpoS7Native / S7CommPlusDriver 源码分析生成 > **许可**: 仅供学习与研究使用
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
3
点赞
・
1
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
感谢你分享这么好的资源!
2026-7-16 10:05
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
2
)
yuanyouran
雪 币:
305
活跃值:
(3075)
能力值:
( LV2,RANK:10 )
在线值:
发帖
5
回帖
184
粉丝
0
关注
私信
yuanyouran
2
楼
感谢分享
2026-7-4 19:03
0
maarek
雪 币:
6
活跃值:
(61)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
4
粉丝
1
关注
私信
maarek
3
楼
“哇,这篇文章太棒了!我平时很少浏览安卓版块,但这篇文章真的值得一看。写得真好,谢谢分享!”
2026-7-5 04:44
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Coder_shen
4
发帖
4
回帖
10
RANK
关注
私信
他的文章
基于C#实现CodesysV3符号变量访问
253
[原创]C#实现西门子S71200与S71500控制器优化DB块符号访问
5048
基于 C# 零依赖西门子 API 实现从博途工程文件解析符号变量树
158
[原创]西门子 S7CommPlus 协议鉴权算法原理与流程详解
2333
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部