通信安全学习笔记

一份覆盖通信安全完整知识点的学习笔记。从密码学基础到协议安全,从进程间通信到工业总线,重点讲"为什么这么设计"(费曼式 📌 讲解),配 draw.io 图。

图表在 /通信安全学习笔记/diagrams/ 目录。图注名即原图文件名(如图注"图 1 · 编码加密哈希对比"对应 图1_编码加密哈希对比.d2 / 图1_编码加密哈希对比.d2.svg),改图请编辑同名 .d2 源文件后运行 scripts/build-d2.ps1 重新渲染。

怎么用这份笔记

  1. 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
  2. 查方案 → 翻 附录 A 速查手册
  3. 防攻击 → 看 附录 B 常见攻击与防御对照表
  4. 找模块 → 查 附录 C RyBOS 模块矩阵,直接定位代码

1 · 编码 vs 加密 vs 哈希

图 1 \xb7 编码加密哈希对比

1.1 三个概念为什么容易混淆

新手最容易犯的错误:把 Base64 当加密,把 MD5 当加密,把加密当编码。这三个东西目的完全不同,混用会出严重安全问题。

TIP

📌 打个比方:

  • 编码(Base64/Hex)像"把中文翻译成英文"——谁都能翻回来,目的只是换个表示方式(兼容传输通道)
  • 加密(AES/XOR)像"把信装进带锁箱子"——有钥匙才能打开,目的是保密
  • 哈希(MD5/SHA256)像"给信件盖指纹章"——不可逆,目的是验证完整性(信没被篡改) :::

1.2 对比表

编码 (Encoding) 加密 (Encryption) 哈希 (Hashing)
目的 格式转换,兼容传输 保密,只有持钥者能读 完整性验证,不可逆
可逆 是(无密钥) 是(需密钥) 否
密钥 无 有(对称/非对称) 无
示例 Base64, Hex, URL编码 AES, XOR, RSA MD5, SHA256
安全保证 无 保密性 完整性

1.3 Base64 — 编码不是加密

Base64 把任意字节流映射成 64 个可打印 ASCII 字符(A-Z a-z 0-9 + /),让二进制数据能安全穿过"只认文本"的通道。

原始: Hello → 3字节 Base64: SGVsbG8= → 4字符(= 是末尾填充)

:::tip 📌 为什么 Base64 不是加密?因为它的"解码表"是公开的、固定的,任何人都能还原。它的作用是适配通道,不是保密。如果你用 Base64 "保护"密码,等于把密码贴在墙上换了种字体。

📌 Base64 让数据变大约 33%:3 字节编码成 4 字符(4/3 ≈ 1.33)。这是为了兼容性付出的空间代价。

RyBOS 中的实现(cpp/Crypto):

std::string encoded = RyB::Base64::encode("Hello, World!");
// → "SGVsbG8sIFdvcmxkIQ=="
std::string decoded = RyB::Base64::decodeToString(encoded);
// → "Hello, World!"

1.4 Hex — 十六进制编码

Hex 把每个字节用两个十六进制字符表示。比 Base64 更直观——你能直接看到原始字节的值,调试加密结果时常用。

std::string hexStr = RyB::Hex::encode("Hello");
// → "48656c6c6f"
auto rawBytes = RyB::Hex::decode("48656c6c6f");
// → {0x48, 0x65, 0x6c, 0x6c, 0x6f}
TIP

📌 Hex vs Base64 怎么选?

  • 调试/日志/校验和展示 → Hex(可读,直接对照字节值)
  • 网络传输/嵌入文本 → Base64(更紧凑,33% vs 100%膨胀)
  • 两者都是编码,都不提供保密性 :::

1.5 URL 编码

URL 中某些字符有特殊含义(& = ? + 空格),如果用户数据里包含这些字符,会破坏 URL 解析。URL 编码(百分号编码)把这些字符替换成 %XX 形式。

原始: name=张三 & age=20 URL编码: name=%E5%BC%A0%E4%B8%89%20%26%20age%3D20

在 HTTP 通信中,永远对用户输入做 URL 编码,否则可能导致参数注入。

2 · 对称加密

图 2 \xb7 AES 加密流程

2.1 什么是对称加密

加密和解密用同一把密钥。发送方用密钥加密,接收方用同一把密钥解密。

明文 → [加密] → 密文 → 传输 → [解密] → 明文 ↑ ↑ └──── 同一把密钥 ────────┘

:::tip 📌 打个比方:对称加密像一把锁配两把相同的钥匙。你把信锁在箱子里寄出去,对方用同样的钥匙开箱。问题是——怎么安全地把钥匙给对方?如果钥匙在寄送途中被截获,加密就形同虚设。这就是"密钥分发问题",非对称加密就是来解决这个的。

2.2 XOR 加密 — 最简单的对称加密

XOR(异或)是对称加密的"hello world"——原理极其简单,但揭示了对称加密的核心思想。

XOR 运算规则

0 ^ 0 = 0 相同→0 0 ^ 1 = 1 不同→1 1 ^ 0 = 1 1 ^ 1 = 0

XOR 加密原理

明文字节 ^ 密钥字节 = 密文字节 密文字节 ^ 密钥字节 = 明文字节 ← 同一密钥,再 XOR 一次就还原
TIP

📌 为什么 XOR 能加密?因为 XOR 有自逆性:A ^ B ^ B = A。加密是 A ^ B,解密是再 ^ B,同一个操作、同一把密钥。这就是"对称"的本质。

📌 XOR 的致命弱点:如果密钥比明文短,密钥会循环使用。攻击者拿到"明文+密文"对,XOR 一下就得到密钥流:明文 ^ 密文 = 密钥。所以XOR 绝不能用于真正的安全场景,只适合轻量级混淆。

RyBOS 中的实现(cpp/XorUtils):

// 单字节密钥
XorUtils::xorFile("data.bin", 82);

// 多字节密钥(循环使用)
std::vector<uint8_t> data = {0x00, 0x01, 0x02, 0x03, 0xFF};
uint8_t key[] = {'A', 'B', 'C'};
auto encrypted = XorUtils::xorEncryptDecrypt(data.data(), data.size(), key, 3);
// 加密过程: 0x00^'A', 0x01^'B', 0x02^'C', 0x03^'A'(循环), 0xFF^'B'

// 解密 = 再加密一次
auto decrypted = XorUtils::xorEncryptDecrypt(encrypted.data(), encrypted.size(), key, 3);

2.3 AES — 工业级对称加密

AES(Advanced Encryption Standard)是 NIST 在 2001 年确立的加密标准,取代了 DES。它是分组加密——把明文切成固定大小的块(16 字节),逐块加密。

AES 的三种密钥长度

变体 密钥长度 轮数 安全等级
AES-128 16 字节 10 轮 足够日常使用
AES-192 24 字节 12 轮 更高安全
AES-256 32 字节 14 轮 军事/金融级

分组模式

ECB (Electronic Codebook): 明文块1 → 加密 → 密文块1 明文块2 → 加密 → 密文块2 每块独立加密,相同明文块→相同密文块(暴露模式) CBC (Cipher Block Chaining): 明文块1 ⊕ IV → 加密 → 密文块1 明文块2 ⊕ 密文块1 → 加密 → 密文块2 每块先和前一块密文异或,相同明文块→不同密文块
TIP

📌 ECB 为什么不安全?因为 ECB 对每个块独立加密,相同的明文块产生相同的密文块。如果加密一张位图,加密后的图片轮廓依然可见——著名的"ECB 企鹅"就是例子。永远优先用 CBC(或 GCM)模式。

📌 CBC 为什么需要 IV?IV(初始向量)是一个随机值,让每次加密即使明文相同,密文也不同。IV 不需要保密,但每次加密必须用不同的 IV,否则 CBC 退化成类似 ECB 的效果。

RyBOS 中的实现(cpp/Crypto,Windows 使用 BCrypt API):

// 生成随机密钥和 IV
auto key = RyB::AES::generateKey(32);   // AES-256
auto iv  = RyB::AES::generateIV();       // 16 字节随机 IV

// 加密
auto ciphertext = RyB::AES::encrypt(plaintext, key, iv, RyB::AES::Mode::CBC);

// 解密
auto decrypted = RyB::AES::decrypt(ciphertext, key, iv, RyB::AES::Mode::CBC);

// 字符串便捷方法(自动派生密钥 + Base64 编码)
std::string enc = RyB::AES::encryptString("机密内容", "MyPassword", iv);
std::string dec = RyB::AES::decryptString(enc, "MyPassword", iv);

PKCS7 填充

AES 是分组加密,明文长度不一定是 16 的倍数。PKCS7 填充规则:缺几个字节就补几个,每个字节的值等于填充长度。

明文: [AA BB CC DD] → 缺 12 字节 → 填充 [0C 0C 0C ... 0C](12个0x0C) 明文: [AA BB ... 16字节] → 正好 16 字节 → 补一整块 [10 10 10 ... 10]
TIP

📌 为什么正好 16 字节也要补一整块?因为解密时要看最后一个字节的值来确定去掉了多少填充。如果原文恰好是 16 的倍数,不补的话,解密时看到最后一个字节可能是数据而非填充长度,造成歧义。补一整块 0x10,解密时一看就知道去掉 16 字节。

2.4 密钥派生

用户提供的密码通常不是 16/24/32 字节,需要"派生"成合法长度的 AES 密钥。

用户密码 "MyPassword" (10字节) ↓ MD5 哈希 (16字节) → AES-128 密钥 ↓ SHA256 哈希 (32字节) → AES-256 密钥
auto key128 = RyB::AES::deriveKeyFromString("MyPassword", 16);  // MD5 → 16字节
auto key256 = RyB::AES::deriveKeyFromString("MyPassword", 32);  // SHA256 → 32字节
WARNING

🚧 简单哈希派生的弱点:直接对密码做一次 MD5/SHA256 派生密钥,容易被彩虹表攻击。生产环境应使用 PBKDF2 / bcrypt / scrypt 等"慢速派生"函数,增加暴力破解成本。

3 · 哈希与消息摘要

3.1 哈希是什么

哈希函数把任意长度的输入压缩成固定长度的输出("指纹")。好的哈希函数满足:

  • 确定性:相同输入永远产生相同输出
  • 雪崩效应:输入改 1 bit,输出变化约 50%
  • 不可逆:从输出无法推算输入
  • 抗碰撞:很难找到两个不同输入产生相同输出
"hello" → MD5: 5d41402abc4b2a76b9719d911017c592 (32字符/16字节) "hello!" → MD5: e0b3d2b... (完全不同) "hello" → SHA256: 2cf24dba5fb0a30e26e83b2ac5b9e29e... (64字符/32字节)

3.2 MD5 vs SHA256

MD5 SHA256
输出长度 128 bit (16 字节) 256 bit (32 字节)
速度 快 较慢(约 MD5 的 1/2)
安全性 已被攻破(碰撞攻击) 目前安全
适用场景 文件校验、非安全场景 密码存储、数字签名、安全场景
TIP

📌 MD5 为什么"不安全"?2004 年王小云团队找到了 MD5 的碰撞方法——能在合理时间内构造两个不同文件,使它们的 MD5 相同。这意味着攻击者可以伪造"签名"。但 MD5 作为文件完整性校验(防误传,不防恶意篡改)仍然够用。

📌 密码存储应该用什么?不要直接 MD5(password)。应该用 PBKDF2 + SHA256 或 bcrypt,它们内置"加盐"和"多轮迭代",大幅增加暴力破解成本。

RyBOS 中的实现(cpp/Crypto):

std::string md5 = RyB::MD5::hash("Hello");
// → "8b1a9953c4611296a827abf8c47804d7"

std::string sha = RyB::SHA256::hash("Hello");
// → "185f8db32271fe25f561a6fc938b2e264302ec435f7e4b5b..."

3.3 消息认证码 (MAC)

哈希只能验证"数据没变",但不能验证"是谁发的"。攻击者可以篡改数据后重新计算哈希。MAC(Message Authentication Code)用密钥参与哈希计算,没有密钥就无法伪造正确的 MAC。

HMAC-SHA256(key, message) → MAC值 发送方: message + HMAC(key, message) → 接收方 接收方: 用同样的 key 重新计算 HMAC,比对是否一致
TIP

📌 哈希 vs MAC 的区别:

  • 哈希:任何人都能计算 → 只能防"误传/损坏",不能防"恶意篡改"
  • MAC:需要密钥才能计算 → 既防"篡改"又验证"来源"

📌 什么时候用 MAC?通信双方共享一个密钥时,每条消息附带 HMAC,接收方验通过才信任。这就是 TLS Record Layer 的做法。

3.4 加盐哈希

如果两个用户密码相同,哈希值也相同——攻击者查表就能批量破解。加盐是在密码后追加随机值再哈希:

不加盐: MD5("password") → 所有人相同 加盐: MD5("password" + salt) → 每人不同(salt 随机生成并存储)

4 · 密钥管理与混淆

图 3 \xb7 密钥管理与混淆

4.1 密钥分发的困境

对称加密的安全完全依赖密钥保密。但密钥怎么从发送方传给接收方?这是通信安全的核心难题:

方案1: 直接发送密钥 → 传输途中可能被截获 方案2: 预先共享密钥 → 需要物理接触,不适合远程 方案3: 非对称加密传密钥 → 用对方公钥加密对称密钥(HTTPS 的做法)
TIP

📌 HTTPS 怎么解决密钥分发:

  1. 客户端连上服务器,服务器出示证书(含公钥)
  2. 客户端验证证书可信(CA 签名链)
  3. 客户端生成随机对称密钥,用服务器公钥加密发过去
  4. 服务器用私钥解密,拿到对称密钥
  5. 后续通信用这个对称密钥加密(性能考虑)

这就是"非对称加密护送对称密钥"的混合方案。

4.2 密钥混淆 — 编译时保护

在嵌入式/桌面软件中,密钥经常需要硬编码在程序里。如果直接写 const char* key = "MySecret",用 strings 命令或十六进制编辑器就能直接看到。

密钥混淆的思路:把密钥以混淆形式存储在二进制中,运行时才还原,让逆向分析者无法直接提取。

RyBOS 中的实现(cpp/KeyObfuscator)——三层 XOR 混淆:

mask1 = 0xA7 ^ ((位置 × 13 + 7) & 0xFF) mask2 = 0x5C ^ ((位置 × 29 + 3) & 0xFF) mask3 = 0x3E ^ ((位置 × 17 + 11) & 0xFF) 混淆字节 = 原始字节 ^ mask1 ^ mask2 ^ mask3
// 编译时生成混淆代码
std::string code = RyB::KeyObfuscator::obfuscateToCode("MySecretKey", "obfKey");
// 输出可直接粘贴到代码中的 C++ 数组声明

// 运行时解混淆
auto obf = RyB::KeyObfuscator::obfuscate("MySecretKey");
std::string key = RyB::KeyObfuscator::deobfuscateToString(obf);
RyB::KeyObfuscator::secureZero(key);  // 用完安全擦除
TIP

📌 三层 XOR 为什么比单层强?单层 XOR 相同字节在不同位置产生相同的混淆结果,容易被统计分析破解。三层不同步长的掩码组合,使相同字节在不同位置产生完全不同的混淆结果,大幅增加逆向难度。

📌 混淆不是加密:混淆只是提高逆向门槛,不是真正的加密。有经验的逆向工程师仍可通过动态调试在内存中抓到还原后的密钥。混淆的正确定位是"提高攻击成本",而非"不可破解"。

4.3 安全内存擦除

密钥用完后,内存中的明文密钥必须被安全擦除。但直接 memset 可能被编译器优化掉(因为编译器认为"变量之后不再使用,清零无意义")。

// BAD: 可能被编译器优化掉
memset(&key, 0, sizeof(key));

// GOOD: volatile 防止优化
void secureZero(void* ptr, size_t len) {
    volatile uint8_t* p = static_cast<volatile uint8_t*>(ptr);
    while (len--) *p++ = 0;
}
TIP

📌 为什么 volatile 能防止优化?volatile 告诉编译器"这个内存地址可能被外部访问,每次读写都必须真正执行"。编译器不敢省略对 volatile 指针的写操作,从而保证清零代码不被优化掉。

RyBOS 中的实现:

std::string key = getKey();
// ... 使用 key ...
RyB::KeyObfuscator::secureZero(key);  // volatile 防优化擦除

5 · TCP/UDP 通信安全

图 4 \xb7 TCP/UDP 通信安全

5.1 TCP 的安全模型与缺陷

TCP 协议本身不提供任何安全保证:

  • 无加密:数据明文传输,任何网络中间人都能抓包看到内容
  • 无认证:无法确认对方真实身份,任何人都可以伪造源 IP
  • 无完整性校验:数据可被中途篡改(TCP 校验和只防传输错误,不防恶意篡改)
客户端 ──明文TCP──→ 路由器 ──明文TCP──→ 服务器 ↑ 攻击者可以: ① 抓包看内容 ② 篡改数据 ③ 伪造连接(SYN Flood) ④ 劫持会话(TCP Hijacking)
TIP

📌 TCP 校验和为什么不算"完整性保护"?TCP 校验和是 16 位简单求和,设计目的是检测传输误码(电磁噪声导致 bit 翻转),不是防恶意篡改。攻击者篡改数据后重算校验和即可,毫无门槛。真正的完整性保护需要 MAC(带密钥的哈希)。

5.2 TCP 常见攻击与防御

攻击 原理 防御
SYN Flood 大量伪造 SYN 包,占满半连接队列 SYN Cookie、连接限速
TCP 劫持 攻击者猜到序列号,注入数据 使用 TLS 加密 + MAC
重放攻击 录制合法数据,稍后重发 序列号 + 时间戳 + Nonce
中间人攻击 攻击者冒充双方中转 证书认证(TLS)
端口扫描 逐个端口发 SYN 探测开放服务 防火墙、最小暴露原则

5.3 UDP 的安全特点

UDP 比 TCP 更"裸"——无连接、无握手、无序列号、无流量控制:

TCP: 三次握手 → 建立连接 → 有状态 → 可追踪 UDP: 直接发包 → 无连接 → 无状态 → 更难追踪
TIP

📌 UDP 为什么更危险:

  1. 无连接 = 无法验证来源(源 IP 可随意伪造)
  2. 无序列号 = 天然无法防重放
  3. 无握手 = 防火墙难以区分"合法响应"和"伪造包"

📌 UDP 反射放大攻击:攻击者伪造受害者 IP 发 UDP 请求到开放服务(如 DNS/NTP),服务端把大响应发到受害者。DNS 放大可达 50-70 倍。防御:配置服务端不响应外部查询、路由器做源地址验证(BCP38)。

5.4 在 TCP/UDP 上构建安全层

既然传输层不安全,我们在应用层构建安全:

┌─────────────────────────────────┐ │ 应用数据 (明文) │ ├─────────────────────────────────┤ │ MAC 校验 (防篡改 + 防重放) │ ← 应用层安全 ├─────────────────────────────────┤ │ 加密层 (AES 加密数据) │ ← 应用层安全 ├─────────────────────────────────┤ │ TCP/UDP (传输) │ ← 不安全 ├─────────────────────────────────┤ │ IP/链路层 │ └─────────────────────────────────┘

RyBOS 的分层设计正是如此——TcpSocket/UdpSocket 只管传输,Crypto 负责加密,CommandProtocol 负责 CRC 校验,各层各司其职。

// RyBOS: 在 TCP 上叠加加密 + CRC
RyB::TcpClient client;
client.connect("127.0.0.1", 8888);

// 发送前:加密
std::string plaintext = "敏感数据";
auto key = RyB::AES::deriveKeyFromString("shared_secret", 32);
auto iv  = RyB::AES::generateIV();
auto encrypted = RyB::AES::encrypt(
    std::vector<uint8_t>(plaintext.begin(), plaintext.end()), key, iv);

// 用 CommandProtocol 封装为带 CRC 的帧
auto frame = RyB::FrameBuilder::buildFromHex(RyB::Hex::encode(encrypted));
client.send(frame.data(), frame.size());

RyBOS 中的 TCP 模块(cpp/TcpSocket)提供客户端和服务器:

// 服务器
RyB::TcpServer server;
server.onConnection([](RyB::TcpConnection& conn) {
    conn.send("Welcome!");
    conn.onReceive([](const uint8_t* data, size_t len) {
        // 处理数据
    });
});
server.listen(8888);

// 客户端
RyB::TcpClient client;
client.connect("127.0.0.1", 8888);
client.send("Hello Server!");

RyBOS 中的 UDP 模块(cpp/UdpSocket)支持服务器、客户端、广播和心跳:

// UDP 广播(设备发现场景)
RyB::UdpBroadcaster broadcaster;
broadcaster.broadcast(9999, "DISCOVER");

// UDP 心跳(连接保活)
RyB::UdpHeartbeat heartbeat;
heartbeat.setTarget("192.168.1.100", 8888);
heartbeat.setInterval(1000);  // 1秒
heartbeat.setHeartbeatData("PING");
heartbeat.start();

6 · HTTP/HTTPS 安全

图 5 \xb7 HTTPS 握手流程

6.1 HTTP 的安全问题

HTTP 是明文协议,所有内容(包括密码)都以明文在网络上传输:

POST /login HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded username=admin&password=Secret123 ← 明文密码!
TIP

📌 HTTP 三大原罪:

  1. 窃听:明文传输,Wireshark 抓包就能看到一切
  2. 篡改:运营商/路由器可以插入广告、修改页面
  3. 冒充:没有证书验证,你不知道对面真的是 example.com 还是钓鱼网站

HTTPS = HTTP + TLS,解决的就是这三个问题。

6.2 HTTPS (TLS) 工作原理

客户端 服务器 │ │ │ ── 1. ClientHello ──────────────────→ │ (支持的TLS版本、加密套件、随机数) │ ←── 2. ServerHello + 证书 + 公钥 ──── │ (选定套件、服务器随机数、CA签发的证书) │ │ │ 3. 验证证书(CA签名链、域名、有效期) │ │ │ │ ── 4. 生成预主密钥,用公钥加密 ────────→ │ │ │ 5. 用私钥解密,得到预主密钥 │ │ │ ←── 6. 双方用三个随机数生成会话密钥 ───→ │ (对称密钥,用于后续加密) │ │ │ ←══ 7. 后续通信用会话密钥加密 ═════════→ │
TIP

📌 为什么需要三个随机数?客户端随机数 + 服务器随机数 + 预主密钥,三者一起生成会话密钥。这样即使预主密钥泄露,没有两个随机数也无法还原会话密钥。三个随机数保证了每次连接的会话密钥都不同,即使攻击者录制了全部流量也无法复现。

📌 为什么用非对称加密只传对称密钥,不直接用非对称加密所有数据?因为非对称加密(RSA/ECC)比对称加密(AES)慢 100-1000 倍。所以 TLS 用非对称加密"护送"对称密钥,之后全用对称加密——兼顾安全和性能。

6.3 HTTP 安全头

即使不启用 HTTPS,HTTP 头也能提供一定的安全防护:

头字段 作用 示例
Strict-Transport-Security 强制 HTTPS(HSTS) max-age=31536000; includeSubDomains
X-Content-Type-Options 禁止 MIME 嗅探 nosniff
X-Frame-Options 防点击劫持(禁止被 iframe 嵌入) DENY
Content-Security-Policy 限制资源加载来源 default-src 'self'
X-XSS-Protection 浏览器 XSS 过滤 1; mode=block

6.4 HTTP 认证安全

Basic Auth: Authorization: Basic dXNlcjpwYXNz ← Base64(用户名:密码) Bearer Token: Authorization: Bearer eyJhbGci... ← JWT
WARNING

🚧 Basic Auth 必须配合 HTTPS:Base64 不是加密,抓包就能还原密码。在 HTTP 上用 Basic Auth 等于裸奔。

JWT 不要存敏感信息:JWT 的 payload 只是 Base64 编码,任何人都能解码。签名只保证"未被篡改",不保证"保密"。

RyBOS 中的 HTTP 客户端(cpp/HttpClient):

RyB::HttpClient client;
client.setHeader("Authorization", "Bearer " + token);
client.setTimeout(10);  // 10秒超时

auto response = client.post("https://api.example.com/data",
                            "{\"key\":\"value\"}",
                            "application/json");
if (response.statusCode == 200) {
    // 处理 response.body
}

7 · WebSocket 安全

图 6 \xb7 WebSocket 协议安全

7.1 WebSocket 协议特点

WebSocket 在 HTTP 握手后"升级"为持久双向连接,解决了 HTTP 只能客户端发起请求的问题。

1. 客户端发起 HTTP Upgrade 请求: GET /ws HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 2. 服务器返回 101 Switching Protocols: HTTP/1.1 101 Switching Protocols Upgrade: websocket Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 3. 之后转为 WebSocket 帧格式通信

7.2 WebSocket 帧格式与掩码

0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... :
TIP

📌 为什么客户端发送的帧必须有掩码(MASK=1)?RFC 6455 规定:客户端→服务器的每帧必须用 4 字节掩码异或加密,服务器→客户端不需要。这不是为了保密,而是为了防止缓存投毒攻击——攻击者可能利用中间代理的缓存机制,把伪造的 HTTP 响应注入缓存。掩码让 WebSocket 帧数据看起来是随机的,不会被误认为 HTTP 内容。

📌 掩码不提供安全性:掩码密钥就在帧头里明文传输,任何人都能解。它的唯一目的是防止中间代理误解 WebSocket 流量。

RyBOS 中的实现(cpp/WebSocket):

// WebSocket 客户端
RyB::WebSocketClient client;
client.onMessage([](const std::string& msg) {
    std::cout << "收到: " << msg << std::endl;
});
client.connect("ws://localhost:8080/ws");
client.send("Hello WebSocket!");

// WebSocket 服务器
RyB::WebSocketServer server;
server.onConnection([](RyB::WebSocketConnection& conn) {
    conn.send("Welcome!");
});
server.listen(8080);

7.3 WebSocket 安全建议

风险 防御
明文传输 使用 wss://(WebSocket over TLS)
跨站 WebSocket 劫持 验证 Origin 头,只允许可信来源
未授权连接 握手阶段验证 Cookie / Token
消息注入 对消息内容做输入验证和转义
资源耗尽 限制连接数、消息大小、心跳超时
WARNING

🚧 WebSocket 不受同源策略保护:与 AJAX 不同,WebSocket 不受 SOP 限制——任何页面的 JS 都能连到任意 WebSocket 服务器。必须在服务器端验证 Origin 头,否则攻击者可以在恶意页面中连上你的 WebSocket 服务。

8 · 命令帧协议安全

图 7 \xb7 命令帧协议安全

8.1 为什么需要帧协议

裸 TCP 是字节流,没有"消息边界"概念。发送方发了两条 100 字节的消息,接收方可能一次收到 200 字节,也可能分 3 次收到 50+80+70 字节。帧协议就是定义"一条完整消息的边界和格式"。

没有帧协议: 发送: [消息A 100字节] [消息B 100字节] 接收: [150字节] [50字节] ← 消息边界丢失! 有帧协议: 发送: [帧头][消息A][CRC][帧尾] [帧头][消息B][CRC][帧尾] 接收: 缓冲区累积 → 检测帧头/帧尾 → 提取完整帧

8.2 RyBOS CommandProtocol 帧格式

RyBOS 的 cpp/CommandProtocol 模块定义了通用命令帧协议:

发送帧: [FA FA] [cmd_data ...] [CRC16_BE] [AF AF] 帧头 命令数据 校验和 帧尾 接收帧: [FA FA] [len_hi len_lo] [fixed_fields + payload] 帧头 长度(大端) 总长 = 13 + payload_length

8.3 帧协议的安全要素

要素 作用 RyBOS 实现
帧头/帧尾 定位帧边界,抗数据流错位 FA FA / AF AF
长度字段 防止缓冲区溢出(读取前知道该读多少) 大端 2 字节
CRC 校验 检测传输损坏,防误解析 CRC-16/XMODEM
流式解析 累积缓冲 + 帧边界检测 FrameParser
TIP

📌 为什么帧头要用两个字节而不是一个?如果帧头只有一个字节 0xFA,那么命令数据中恰好出现 0xFA 就会被误认为帧头。两个字节 0xFA 0xFA 降低了误匹配概率——数据中连续出现两个 0xFA 的概率远低于一个。

📌 CRC 防的是什么?CRC 防的是传输误码(电磁干扰、线缆噪声),不是恶意篡改。攻击者篡改数据后可以重算 CRC。要防恶意篡改,需要 MAC(带密钥的哈希)。但在工业控制场景中,CRC 已经足够——威胁主要来自环境噪声而非人为攻击。

8.4 CRC-16 校验原理

CRC(循环冗余校验)把数据看作一个大二进制数,除以一个约定的"生成多项式",余数就是 CRC 值。

// RyBOS 中的 CRC-16
RyB::CRC16 crc;
uint16_t checksum = crc.calculate(data, length);

// 帧构建(自动附加 CRC)
auto frame = RyB::FrameBuilder::buildFromHex("0001000000A10AFF");
// → FA FA 00 01 00 00 00 A1 0A FF [CRC16] AF AF

// 帧验证
bool valid = RyB::FrameUtils::verifySendFrame(frame);

8.5 流式帧解析的安全考虑

网络数据是分片到达的,帧可能被切散在多个 TCP 包中。FrameParser 采用流式累积策略:

RyB::FrameParser parser;
parser.setFrameCallback([](const std::vector<uint8_t>& frame) {
    if (RyB::FrameUtils::verifySendFrame(frame)) {
        // CRC 校验通过,处理帧
    }
});

// 持续喂入收到的数据(可能是不完整的片段)
parser.feed(chunk1);  // 可能只收到半个帧
parser.feed(chunk2);  // 凑齐了,回调触发
TIP

📌 流式解析为什么重要:如果假设"一次 recv() 就能收到一个完整帧",在网络条件差时会丢帧。正确的做法是维护一个接收缓冲区,每次 recv() 追加数据,然后从缓冲区中扫描帧头/帧尾,提取完整帧,剩余不完整的数据留到下次。

📌 缓冲区溢出防御:帧解析器必须限制缓冲区最大长度。如果攻击者持续发送数据但永远不出现帧尾,缓冲区会无限增长导致 OOM。设置最大缓冲区大小,超过就清空重来。

8.6 命令序列与重试安全

RyBOS 的 CommandSequence 支持按顺序执行多条命令,每条命令可设置延时:

RyB::CommandSequence seq;
seq.add("0001000000A10AFF", 200, "获取固件版本");
seq.add("00010000009A00FF", 100, "获取SN");
seq.execute(sendFunction, delayFunction);
TIP

📌 命令序列的安全考虑:

  • 超时机制:每条命令必须有超时,防止设备无响应时永久阻塞
  • 重试限制:失败重试次数要有上限,否则可能形成"重试风暴"
  • 顺序校验:敏感操作(如固件升级)应要求严格顺序,跳步应拒绝
  • 幂等性:重试时确保命令幂等——执行两次和一次效果相同 :::

9 · 管道与命名管道

图 8 \xb7 IPC 通信安全

9.1 管道的安全模型

管道(Pipe)是 Unix 最古老的 IPC 机制。命名管道(Named Pipe / FIFO)在文件系统中有路径,可以跨进程通信。

匿名管道: 父进程 ──→ [管道] ──→ 子进程 (只能有血缘关系的进程间) 命名管道: 进程A ──→ [/tmp/myfifo] ──→ 进程B (任意进程,通过路径访问)

:::tip 📌 命名管道的安全特点:

  • 同机通信:数据不经过网络,天然防窃听
  • 文件权限保护:命名管道有文件权限(rwx),可以限制哪些用户/进程能读写
  • 无加密需求:因为数据不出机器,操作系统权限就是安全边界

📌 但共享机器不安全:如果攻击者已经登录同一台机器,命名管道的文件权限就是最后的防线。确保命名管道的权限只允许授权用户访问:mkfifo mypipe && chmod 600 mypipe。

9.2 命名管道的安全风险

风险 说明 防御
权限过宽 默认权限可能允许其他用户读写 chmod 600 限制访问
竞态条件 创建和设置权限之间有时间窗口 mkfifo 后立即 chmod,或用 umask
拒绝服务 恶意进程打开管道但不读写,阻塞合法进程 非阻塞模式 + 超时
符号链接攻击 攻击者用符号链接替换管道路径 使用绝对路径 + O_NOFOLLOW

9.3 Windows 命名管道

Windows 的命名管道比 Unix 功能更丰富,支持:

  • 管道服务器可以模拟客户端身份(Impersonation)
  • 管道安全描述符(Security Descriptor)
  • 管道实例计数
\\.\pipe\myPipeName ← Windows 命名管道路径格式
TIP

📌 Windows 管道的安全描述符:Windows 管道支持 SECURITY_ATTRIBUTES,可以精确控制哪些用户/组能访问。比 Unix 的 chmod 粒度更细——可以指定"允许读但不允许写"、"允许特定 SID 访问"等。

📌 Impersonation 的安全意义:管道服务器可以临时"变成"客户端的身份来执行操作,这样即使服务器进程以高权限运行,实际访问资源时用的是客户端权限,实现最小权限原则。但这也带来风险——如果客户端是恶意用户,Impersonation 后可能执行越权操作。所以必须设置 Impersonation 级别(如 SecurityImpersonation 限制为同机访问)。

10 · 共享内存

10.1 共享内存的安全模型

共享内存是最快的 IPC——多个进程映射同一块物理内存,直接读写,无需内核拷贝。但速度的代价是安全责任全在开发者。

进程A的虚拟地址空间 物理内存 进程B的虚拟地址空间 0x7f...1000 ───────→ [共享内存块] ←─────── 0x7f...2000 (映射) (同一块物理页) (映射)
TIP

📌 共享内存为什么最快:其他 IPC(管道、消息队列、Socket)都需要数据从用户态→内核态→用户态的两次拷贝。共享内存绕过内核,进程直接读写同一块物理内存,零拷贝。

📌 共享内存为什么最危险:

  1. 无同步:两个进程同时写同一地址 → 数据损坏
  2. 无边界:没有"消息"概念,纯裸内存,写越界直接破坏对方数据
  3. 无认证:任何知道 SHM key 的进程都能 attach
  4. 持久性:进程退出后共享内存仍然存在,可能泄露敏感数据 :::

10.2 共享内存的同步问题

共享内存本身不提供任何同步机制。必须配合信号量、互斥锁或原子操作来保证一致性。

进程A: 写入 data → 设置 flag 进程B: 轮询 flag → 读取 data 问题: 如果进程A写到一半被调度走,进程B看到 flag 已设置但 data 不完整 解决: 用信号量或 mutex 保证"写完才能读"

:::warning 🚧 共享内存 + 信号量的经典竞态:

  1. 进程A获取信号量,开始写数据
  2. 进程A写到一半崩溃 → 信号量没释放
  3. 进程B永远等不到信号量 → 死锁

防御:使用 SEM_UNDO 标志(进程崩溃时自动释放信号量),或用带超时的 sem_timedwait。

10.3 共享内存安全清单

风险 防御
未授权访问 设置正确的权限模式(0600),限制只有创建者可访问
数据竞争 配合信号量/互斥锁/原子操作
进程崩溃残留 使用 shmctl(IPC_RMID) 清理,或用 POSIX shm + ftruncate
敏感数据残留 进程退出前 memset / secureZero 共享内存
缓冲区溢出 严格校验写入偏移和长度,共享内存段大小固定

11 · 本地 Socket (Unix Domain Socket)

11.1 本地 Socket vs 网络 Socket

本地 Socket(Unix Domain Socket, UDS)使用文件系统路径作为地址,不经过网络协议栈,效率接近共享内存但保留了 Socket 的 API。

网络 Socket: 客户端 → TCP/IP栈 → 网卡 → 路由 → 网卡 → IP栈 → 服务器 本地 Socket: 客户端 → 内核缓冲区 → 服务器 (不经过网络栈)
TIP

📌 本地 Socket 的安全优势:

  1. 不经过网络 = 天然防窃听、防篡改、防冒充
  2. 文件权限控制 = 可以限制哪些用户/进程能连接
  3. 支持传递文件描述符 = 可以安全地传递资源句柄
  4. 支持 SO_PEERCRED = 服务器可以获取客户端的 UID/GID/PID

📌 什么时候用 UDS 而非 TCP:同一台机器上的进程间通信,优先用 UDS。不仅更安全,而且更快(省去了 TCP 握手、校验和、拥塞控制等开销)。Docker、X11、systemd 都用 UDS。

11.2 本地 Socket 的安全特性

// 服务器端:获取客户端凭证(Linux)
struct ucred cred;
socklen_t len = sizeof(cred);
getsockopt(clientFd, SOL_SOCKET, SO_PEERCRED, &cred, &len);
// cred.uid = 客户端用户ID
// cred.pid = 客户端进程ID
// → 可以据此做访问控制
TIP

📌 SO_PEERCRED 为什么重要:网络 Socket 的源 IP 可以伪造,但 UDS 的 SO_PEERCRED 由内核设置,不可伪造。服务器可以精确知道"对面是哪个用户的哪个进程",实现基于身份的访问控制。这是 UDS 独有的安全特性。

11.3 Windows Named Pipe vs Unix Domain Socket

Windows Named Pipe Unix Domain Socket
地址 \\.\pipe\name 文件路径 /tmp/name.sock
权限控制 安全描述符 (SD) 文件权限 (chmod)
身份验证 Impersonation SO_PEERCRED
FD 传递 不支持 SCM_RIGHTS
API CreateNamedPipe socket(AF_UNIX)

RyBOS 中也有对应的本地通信模块(cpp/NamedPipe 和 qt/LocalSocket),分别对应 Windows 和跨平台场景。

12 · 串口通信安全

图 9 \xb7 工业通信安全

12.1 串口通信的特点

串口(Serial Port / RS-232 / RS-485)是工业领域最基础的通信方式。与网络通信不同,串口通信有其独特的安全模型:

特点: - 物理层通信:数据通过电平信号在铜线上传输 - 点对点(RS-232)或多点总线(RS-485) - 无协议栈:原始字节流,没有 TCP/IP 那样的分层 - 短距离(RS-232 < 15m)或中距离(RS-485 < 1200m) - 低速率:通常 9600~115200 bps
TIP

📌 串口通信的安全模型与网络完全不同:

  1. 物理安全是前提:能接触到串口线 = 能窃听/篡改。串口安全依赖物理隔离
  2. 无内置加密:串口协议只有起始位/停止位/校验位,没有加密层
  3. 无认证机制:总线上任何设备都能发送数据
  4. 广播介质(RS-485):总线上所有设备都能听到所有数据

📌 工业场景为什么还在用串口:简单、可靠、低成本、实时性好。在工厂环境中,设备可能没有网络接口,但一定有串口。串口不会消失,所以串口安全必须重视。

12.2 串口通信的安全威胁

威胁 说明 防御
物理窃听 并联一根线到 RS-485 总线即可抓包 物理隔离、线缆屏蔽
数据篡改 总线上注入伪造指令 应用层 CRC + 加密
重放攻击 录制合法指令,稍后重发 序列号 + 时间戳
总线冲突 多设备同时发送导致数据损坏 主从轮询协议
未授权设备接入 陌生设备接入总线 应用层认证握手

12.3 串口安全加固方案

在串口上构建安全通信,需要在应用层叠加:

┌─────────────────────────────────┐ │ 应用数据 (明文) │ ├─────────────────────────────────┤ │ HMAC 校验 (防篡改) │ ← 应用层 ├─────────────────────────────────┤ │ AES 加密 (保密) │ ← 应用层 ├─────────────────────────────────┤ │ 帧协议 (帧头/长度/CRC/帧尾) │ ← 协议层 ├─────────────────────────────────┤ │ 串口 (起始位/数据位/停止位) │ ← 物理层 └─────────────────────────────────┘

RyBOS 的 cpp/Serial 模块负责串口传输层,cpp/CommandProtocol 负责帧协议,cpp/Crypto 负责加密:

// 串口 + 帧协议 + 加密的组合
RyB::SerialPort serial;
serial.open("COM3", 115200);

// 加密命令数据
auto key = RyB::AES::deriveKeyFromString("device_secret", 32);
auto iv  = RyB::AES::generateIV();
auto encrypted = RyB::AES::encrypt(rawCommand, key, iv);

// 封装为帧
auto frame = RyB::FrameBuilder::buildFromHex(RyB::Hex::encode(encrypted));
serial.write(frame.data(), frame.size());

// 接收端:流式解析 + 解密
RyB::FrameParser parser;
parser.setFrameCallback([&](const std::vector<uint8_t>& frame) {
    if (!RyB::FrameUtils::verifySendFrame(frame)) return;  // CRC 校验
    auto decrypted = RyB::AES::decrypt(payload, key, iv);   // 解密
    // 处理 decrypted
});
TIP

📌 串口加密的性能考虑:串口带宽很低(通常 < 100 KB/s),AES 加密引入的计算开销可以忽略。但加密后数据膨胀(PKCS7 填充 + IV + HMAC)会增加传输时间。在 9600 bps 的链路上,每增加 16 字节就要多传约 13 毫秒。所以串口加密应尽量减少帧数量,合并小命令。

📌 RS-485 主从模式的安全意义:RS-485 总线上指定一个主设备,只有主设备能主动发起通信,从设备只能响应。这天然防止了"从设备乱发数据"的问题——即使一个从设备被攻破,它也无法主动注入指令到总线。

13 · 工业总线安全

13.1 工业通信协议全景

协议 介质 速率 实时性 安全机制 场景
CAN 两线 1 Mbps 好 CRC-15 电机/传感器
CAN-FD 两线 8 Mbps 好 CRC-17/21 更多数据
EtherCAT 以太网 100 Mbps 极好 无内置安全 伺服/工业
Modbus RTU RS-485 115 kbps 一般 CRC-16 PLC/传感器
Modbus TCP 以太网 100 Mbps 一般 无加密 工控网络
RS-232 三线 115 kbps 一般 无 调试/简单设备
RS-485 两线 10 Mbps 一般 无 长距离/多设备
MQTT 以太网 依赖网络 一般 TLS/用户名密码 IoT 消息
TIP

📌 工业协议为什么普遍缺乏安全机制:这些协议设计于 1980-2000 年代,当时工业网络是物理隔离的(air gap),"安全靠不上网"。但随着工业互联网和 IoT 的兴起,工业设备越来越多地接入公网,"物理隔离"不再成立。Modbus TCP 明文传输、无认证的问题在工控安全(ICS Security)中是头号风险。

📌 工控安全的"纵深防御":

  1. 物理层:线缆隔离、端口锁
  2. 网络层:防火墙、VLAN 隔离、单向网闸
  3. 协议层:协议级认证(如 Modbus Security)
  4. 应用层:加密 + 认证 + 审计
  5. 管理层:访问控制策略、变更管理 :::

13.2 Modbus 安全分析

Modbus 是工业领域最广泛使用的协议,也是安全问题最突出的:

Modbus RTU (串口): [地址] [功能码] [数据] [CRC16] 1字节 1字节 N字节 2字节 Modbus TCP (以太网): [MBAP头] [功能码] [数据] 7字节 1字节 N字节 (无CRC!TCP校验和代替)

:::warning 🚧 Modbus 的安全缺陷:

  1. 无认证:任何能连到 TCP 502 端口的设备都能读写寄存器
  2. 无加密:所有数据明文传输
  3. 无完整性校验(TCP 版):依赖 TCP 校验和,不防篡改
  4. 功能码无授权:写线圈(功能码 0x05/0x06)和读线圈(0x01/0x03)用同一认证级别

真实案例:很多工控设备暴露在公网上,通过 Shodan 搜索 "port:502" 就能找到数万台开放 Modbus TCP 的设备。攻击者可以直接修改寄存器值,导致物理设备异常。

13.3 CAN 总线安全

CAN 总线是汽车和机器人中最常用的通信总线:

CAN 帧: [SOF] [ID(11/29位)] [RTR] [DLC] [数据(0-8字节)] [CRC] [ACK] [EOF]
TIP

📌 CAN 总线的安全特点:

  1. CRC-15 校验:能检测多位错误,但防不了恶意篡改
  2. 总线仲裁:ID 越小优先级越高,攻击者可以用低 ID 抢占总线
  3. 广播介质:总线上所有节点都能听到所有帧
  4. 无认证:任何节点都能发送任何 ID 的帧

📌 CAN 总线攻击实例:2015 年 Jeep Cherokee 黑客事件——攻击者通过车联网娱乐系统入侵 CAN 总线,远程控制方向盘、刹车、引擎。这直接推动了汽车行业对 CAN 安全的重视。

CAN 安全加固方案:

  • CAN FD + 加密:CAN FD 支持最多 64 字节数据段,足够容纳 AES 加密后的短指令
  • 安全车载网关:在 CAN 总线和外部网络之间部署网关,过滤非法帧
  • ID 白名单:只允许预注册的 ID 通过
  • 应用层认证:在数据段内嵌入 HMAC

13.4 MQTT 安全

MQTT 是 IoT 领域最流行的消息协议,基于 TCP,支持 TLS:

MQTT 连接流程: 1. TCP 连接到 Broker (默认端口 1883/8883) 2. CONNECT 报文 (含 ClientID、Username、Password) 3. CONNACK 返回连接结果 4. SUBSCRIBE/PUBLISH 消息交互 5. PINGREQ/PINGRESP 心跳保活
TIP

📌 MQTT 安全最佳实践:

  1. 使用 MQTT over TLS (端口 8883):防止窃听和篡改
  2. 用户名/密码认证:Broker 验证客户端身份
  3. ACL 主题过滤:限制客户端只能订阅/发布特定主题
  4. 客户端证书:双向 TLS 认证,比用户名密码更强
  5. QoS 选择:QoS 1(至少一次)比 QoS 0(最多一次)更可靠,但要注意重去重

📌 MQTT 主题通配符的安全风险:# 匹配所有子主题,+ 匹配单层。如果给客户端订阅 sensor/# 的权限,它就能收到所有传感器数据。ACL 必须精确到具体主题,避免过宽的通配符。

14 · CRC 校验与数据完整性

14.1 校验 vs 哈希 vs MAC

机制 目的 防误码 防篡改 需密钥 示例
校验和 检测传输错误 弱 否 否 CRC-16, 异或和
哈希 完整性指纹 强 否 否 MD5, SHA256
MAC 认证+完整性 强 是 是 HMAC-SHA256
数字签名 认证+完整性+不可否认 强 是 是(非对称) RSA-SHA256
TIP

📌 四者的安全级别递增:

  • 校验和:只防"线缆噪声" → 攻击者重算即可绕过
  • 哈希:防"意外损坏" → 攻击者可以改数据+改哈希
  • MAC:防"恶意篡改" → 攻击者没有密钥就无法伪造
  • 数字签名:防"发送方否认" → 私钥只有发送方有,不可抵赖

📌 工业场景为什么用 CRC 而非 MAC:工业设备资源有限(单片机可能只有几 KB RAM),CRC 计算只需几个字节状态,而 HMAC-SHA256 需要更多计算和存储。且工业总线通常物理隔离,篡改风险低于误码风险。但在接入公网时,必须升级到 MAC 或加密。

14.2 CRC 原理详解

CRC 的数学本质:把数据看作一个大多项式,对生成多项式做模 2 除法,余数就是 CRC。

数据: M(x) = x^23 + x^16 + x^5 + x^2 + 1 (即 0x01000025) 生成式: G(x) = x^16 + x^12 + x^5 + 1 (CRC-CCITT, 0x1021) CRC = M(x) * x^16 mod G(x)

常用 CRC 变体:

名称 多项式 初始值 应用
CRC-16/XMODEM 0x1021 0x0000 通用
CRC-16/MODBUS 0x8005 0xFFFF Modbus
CRC-16/CCITT 0x1021 0xFFFF 通信
CRC-32 0x04C11DB7 0xFFFFFFFF 以太网、ZIP

RyBOS 中使用 CRC-16/XMODEM 变体,初始值可配置(默认 0x8848):

RyB::CRC16 crc(0x8848);  // 自定义初始值
uint16_t result = crc.calculate(data, length);
TIP

📌 初始值为什么不为 0:如果初始值为 0,前导零字节不影响 CRC 结果(因为 0 ^ 0 = 0)。这意味着 0x00 0x01 和 0x00 0x00 0x01 的 CRC 相同。非零初始值让前导零也参与运算,避免这种歧义。

📌 RyBOS 为什么用 0x8848 而非标准值:这是 CameraToolBox 项目的遗留选择。非标准初始值在某种程度上也起到了"弱混淆"的作用——攻击者即使知道用的是 CRC-16/XMODEM,但不知道初始值也无法正确伪造 CRC。当然这种"安全"非常弱,不应依赖。

14.3 CRC 的检错能力

错误类型 CRC-16 检测率
单 bit 错误 100%
双 bit 错误 100%
奇数个 bit 错误 100%
突发错误 (≤16 bit) 100%
突发错误 (>16 bit) 1 - 2^(-16) ≈ 99.998%
随机错误 1 - 2^(-16) ≈ 99.998%
TIP

📌 CRC 为什么能 100% 检测单 bit 错误:因为 CRC 生成多项式至少有两个项(如 x^16 + x^12 + x^5 + 1),任何单 bit 翻转都会产生一个余数不为 0 的多项式。

📌 CRC 为什么不能 100% 检测所有错误:存在极低概率的"碰撞"——两个不同数据产生相同 CRC。对于 CRC-16,碰撞概率约 1/65536。在工业控制中这个概率足够低,但在安全场景中不可接受。

附录 A · 安全速查手册

图 10 \xb7 RyBOS 模块矩阵与速查

A.1 加密方案选择

场景 推荐方案 RyBOS 模块
轻量级混淆 XOR 单字节密钥 cpp/XorUtils
文件批量混淆 XOR 多字节密钥 cpp/XorUtils
正式加密 AES-256-CBC + 随机 IV cpp/Crypto
密码存储 PBKDF2-SHA256 + 盐 (需自行实现)
密钥硬编码保护 三层 XOR 混淆 cpp/KeyObfuscator
文件混淆 16 字节循环 XOR cpp/KeyObfuscator
通信加密 AES + HMAC cpp/Crypto
数据完整性 CRC-16 cpp/CommandProtocol

A.2 通信协议选择

场景 推荐协议 RyBOS 模块
同机进程通信 Unix Domain Socket / Named Pipe cpp/NamedPipe, qt/LocalSocket
同机高速共享 共享内存 + 信号量 cpp/SharedMemory
网络可靠传输 TCP cpp/TcpSocket
网络实时传输 UDP cpp/UdpSocket
Web API HTTP/HTTPS cpp/HttpClient
双向实时通信 WebSocket cpp/WebSocket
工业设备控制 串口 + 帧协议 cpp/Serial + cpp/CommandProtocol
设备发现 UDP 广播 cpp/UdpSocket
连接保活 UDP 心跳 cpp/UdpSocket

A.3 安全检查清单

  • 传输敏感数据时使用加密(AES/TLS)
  • 密钥不在代码中明文硬编码(用 KeyObfuscator)
  • 密钥用完后安全擦除(secureZero)
  • 每次加密使用不同的 IV
  • 通信帧包含 CRC 或 MAC 校验
  • 帧解析器有缓冲区大小限制
  • 所有外部输入经过验证和转义
  • HTTP 请求设置合理超时
  • WebSocket 服务器验证 Origin 头
  • 串口/总线通信有应用层认证
  • 共享内存有同步机制和权限控制
  • 敏感操作有审计日志

附录 B · 常见攻击与防御

攻击 层级 原理 防御
窃听 网络 抓包查看明文 加密(AES/TLS)
中间人 网络 冒充双方中转 证书认证(CA/TLS)
重放 协议 录制并重发合法数据 序列号 + 时间戳 + Nonce
篡改 协议 修改传输中的数据 MAC / 数字签名
SYN Flood TCP 耗尽半连接队列 SYN Cookie + 限速
UDP 放大 UDP 伪造源 IP 反射攻击 BCP38 源地址验证
XSS HTTP 注入恶意脚本 输入转义 + CSP
CSRF HTTP 伪造用户请求 Token + SameSite Cookie
点击劫持 HTTP iframe 覆盖欺骗 X-Frame-Options
WS 劫持 WebSocket 恶意页面连 WS 验证 Origin
缓冲区溢出 协议 超长数据覆盖内存 长度校验 + 安全函数
竞态条件 IPC 时间窗口内非法操作 锁 + 原子操作
符号链接 IPC 替换文件路径 O_NOFOLLOW + 绝对路径
侧信道 密码学 通过时间/功耗推断密钥 常数时间操作 + 屏蔽
暴力破解 密码学 逐个尝试密钥 强密钥 + 慢速哈希

附录 C · RyBOS 通信安全模块矩阵

模块 路径 功能 安全相关
Crypto cpp/Crypto Base64/Hex/MD5/SHA256/AES/XOR 加密、哈希、编码
XorUtils cpp/XorUtils XOR 加解密(文件/目录/内存) 轻量级混淆加密
KeyObfuscator cpp/KeyObfuscator 密钥混淆、安全擦除 编译时密钥保护
CommandProtocol cpp/CommandProtocol 帧协议、CRC-16、流式解析 数据完整性、帧边界
TcpSocket cpp/TcpSocket TCP 客户端/服务器 传输层(需叠加加密)
UdpSocket cpp/UdpSocket UDP 通信、广播、心跳 传输层(需叠加加密)
HttpClient cpp/HttpClient HTTP GET/POST/PUT/DELETE 应用层(用 HTTPS)
WebSocket cpp/WebSocket WebSocket 客户端/服务器 需验证 Origin + wss
Serial cpp/Serial 串口通信 需叠加帧协议+加密
NamedPipe cpp/NamedPipe Windows 命名管道 IPC 安全(权限控制)
SharedMemory cpp/SharedMemory 共享内存 IPC 安全(同步+权限)
LocalSocket qt/LocalSocket Qt 本地 Socket IPC 安全(跨平台)
本页目录