/**
 * @brief  使用验证码对密文进行可逆异或混淆
 * @param  data: 8字节数据
 * @param  verCode: 2字节验证码(设置显示的4位HEX)
 */
static void MixVerificationCode(uint8_t data[8], const uint8_t verCode[2])
{
    /* 将2字节验证码作为LCG种子(大端序组装) */
    uint16_t state = ((uint16_t)verCode[0] << 8) | verCode[1];
    int i;

    for (i = 0; i < 8; i++)
    {
        /* 16位线性同余生成器(LCG):next = (a × current + c) mod m */
        state = (uint16_t)(state * 25173u + 13849u);
        /* 取高8位作为掩码字节 */
        data[i] ^= (uint8_t)(state >> 8);
    }
}

1. 这个函数要解决什么问题?

输入:2字节验证码(如 0xA5, 0xB3)
目标:生成8字节掩码,用来异或8字节密文

问题:2字节只有16位,但需要64位(8字节)的掩码
解决:用2字节作为"种子",通过数学公式"生长"出8字节

最简单的做法(但不好):

// 简单重复:2字节重复4次凑8字节
data[0] ^= verCode[0];  // 0xA5
data[1] ^= verCode[1];  // 0xB3
data[2] ^= verCode[0];  // 0xA5  ← 重复了!
data[3] ^= verCode[1];  // 0xB3  ← 重复了!
...
// 问题:掩码有明显规律,容易被分析破解

更好的做法(本函数):用LCG把2字节"膨胀"成8个不同的字节。


2. LCG 是什么?

2.1 全称:Linear Congruential Generator(线性同余生成器)

本质:一个数学递推公式,能从一个初始数产生一串"看起来随机"的数。

公式:next = (a × current + c) mod m

其中:
  current = 当前的数(状态)
  next    = 下一个数
  a       = 乘数(multiplier)
  c       = 增量(increment)
  m       = 模数(modulus)

2.2 生活类比

想象一个时钟,表盘上有 m 个刻度(0 到 m-1):

        ┌───── 12点 ─────┐
       11                  1
      10     时钟表盘      2
       9    (m个刻度)      3
        8                 4
         7              5
          └──── 6 ────┘

每次操作:
  1. 当前位置 × a  (跳跃)
  2. 再 + c        (偏移)
  3. 超过表盘就绕回来(mod m)

每次都跳到一个新位置,看起来像是随机跳跃。

2.3 具体数值代入

本函数用的参数:

a = 25173    乘数
c = 13849    增量  
m = 65536    模数(= 2^16,即uint16_t的自然溢出)
假设种子 state = 0xA5B3 = 42419

第1次:state = (42419 × 25173 + 13849) mod 65536
             = (1067,704,687 + 13849) mod 65536
             = 1,067,718,536 mod 65536
             = 1,067,718,536 - 16289 × 65536
             = 某个 0~65535 之间的数
             → 取高8位作为第1个掩码字节

第2次:state = (上次结果 × 25173 + 13849) mod 65536
             → 取高8位作为第2个掩码字节

... 一共8次,得到8个不同的掩码字节

3. 逐行代码详解

3.1 组装种子

uint16_t state = ((uint16_t)verCode[0] << 8) | verCode[1];
verCode[0] = 0xA5,  verCode[1] = 0xB3

verCode[0] << 8:
  0xA5 = 1010 0101
  左移8位 → 1010 0101 0000 0000 = 0xA500

| verCode[1]:
  0xA500 | 0x00B3 = 0xA5B3

结果:state = 0xA5B3 = 42419(十进制)

这是LCG的"种子"(初始值),决定了后续所有输出
不同的验证码 → 不同的种子 → 完全不同的掩码序列

3.2 LCG迭代 + 异或

for (i = 0; i < 8; i++)
{
    state = (uint16_t)(state * 25173u + 13849u);
    data[i] ^= (uint8_t)(state >> 8);
}

拆解每一步:

② state × 25173 + 13849
这就是LCG公式:next = (a × current + c) mod m

为什么用 (uint16_t) 强转?
  → uint16_t 范围是 0~65535
  → 乘法结果超过65535时自动截断(只保留低16位)
  → 这个截断效果 等价于 mod 65536
  → 所以不需要显式写 % 65536

举例:
  state = 42419
  42419 × 25173 = 1,067,704,687
  1,067,704,687 + 13849 = 1,067,718,536

  1,067,718,536 用二进制表示(32位):
  0011 1111 1010 1110 | 1001 0011 0000 1000
                        ↑ 只保留低16位 ↑
  
  (uint16_t) → 0x9308 = 37640
  
  所以新的 state = 37640
③ 取高8位
(uint8_t)(state >> 8)
state = 0x9308

state >> 8:
  0x9308 → 0x0093

(uint8_t) → 0x93

为什么取高8位而不是低8位?

  LCG的一个已知特性:
  低位的随机性差,高位的随机性好

  举例(看低1位的规律):
    偶数 × 奇数 + 奇数 = 奇数  →  低1位 = 1
    奇数 × 奇数 + 奇数 = 偶数  →  低1位 = 0  ← 不停交替!
  
  低1位在 0 和 1 之间来回跳 → 完全可预测 → 不"随机"

  高位就没有这种明显规律 → 更像随机数
④ 异或到数据
data[i] ^= mask_byte;
XOR(异或)的性质:

  A ^ B ^ B = A    (异或两次同一个值,恢复原值)

这就是为什么同一个函数可以用于:
  加密端:data ^ mask → 混淆数据
  解密端:混淆数据 ^ mask → data(还原)

前提:两边用相同的 verCode → 相同的种子 → 相同的 mask 序列

3.3 为什么LCG参数不能随意选择

3.3.1 核心原因:数学约束

比如SHA-256常数是"任意选的好常数",但LCG参数必须满足严格的数学条件。

两者用途完全不同:

SHA-256常数:作为初始值/混合种子 → 只要"不特殊"就行
LCG参数:    作为递推公式系数     → 必须满足全周期定理,否则会出Bug

3.3.2 实际验证:用SHA-256常数当LCG参数会怎样?
SHA-256的前两个常数(截取低16位):
  H0 = 0x6A09E667 → 取低16位 → 0xE667 = 58983(做乘数a)
  H1 = 0xBB67AE85 → 取低16位 → 0xAE85 = 44677(做增量c)

检查 Hull-Dobell 全周期条件(m = 65536 = 2^16):
条件1:gcd(c, m) = 1?
  c = 44677(奇数),m = 65536(2的幂)
  gcd(44677, 65536) = 1 ✅

条件2:a-1 能被 m 的所有质因子整除?
  a - 1 = 58982
  m 的质因子 = {2}
  58982 / 2 = 29491 ✅

条件3:若 4|m,则 4|(a-1)?
  4 | 65536?  是
  4 | 58982?  58982 / 4 = 14745.5  ❌ 不能整除!
╔══════════════════════════════════════════════════╗
║  条件3不满足!用SHA-256常数做LCG参数 → 不是全周期  ║
║  周期可能只有 32768 或更短                        ║
║  某些种子会进入短循环 → 掩码重复 → 安全性下降      ║
╚══════════════════════════════════════════════════╝

而 25173 和 13849:

条件1:gcd(13849, 65536) = 1    ✅  13849是奇数
条件2:(25173-1) = 25172, 25172/2 = 12586  ✅
条件3:25172/4 = 6293           ✅  能整除

三个条件全部满足 → 保证全周期 = 65536

3.3.3 两种常数的本质区别

SHA-256 常数 LCG 参数
选择标准 "Nothing Up My Sleeve"(无后门)来源公开透明即可 必须满足全周期数学条件+ 统计随机性测试
来源

质数平方根的小数部分

纯数学常数

数学推导 + 实验筛选

经典文献验证

约束 几乎无约束,只要非特殊值

严格约束:

① gcd(c,m)=1

② (a-1)%p==0

③ 4|m → 4|(a-1)

用途

哈希初始值

异或混合种子,不参与递推

递推公式系数

决定序列质量

换一个会怎样

通常没问题

安全性几乎不变

可能破坏全周期性

序列质量严重下降


3.3.4 25173 和 13849 的出处

这组参数来自 Knuth《计算机程序设计艺术》 及经典数值计算文献,经过:

第1步:数学筛选
  从满足 Hull-Dobell 三个条件的所有 (a, c) 组合中筛选
  16位LCG中满足条件的 a 值:a mod 4 == 1 的奇数
  如:5, 9, 13, ... 25173, ...(有很多候选)

第2步:统计测试
  对候选参数跑随机性测试(谱测试/spectral test)
  衡量生成序列的"均匀性"和"不可预测性"
  
  谱测试检查的是:
    生成的随机数在多维空间中是否均匀分布
    差的参数 → 数字落在几条平行线上(有规律)
    好的参数 → 数字均匀填满整个空间

第3步:从通过测试的参数中选择
  25173 和 13849 就是通过严格筛选后的推荐值
3.3.5 直观对比:好参数 vs 坏参数、
好参数(25173, 13849):         坏参数(随便选的):
生成序列的2D散点图               生成序列的2D散点图

  ·· · ·  · · ··· ·              ·    ·    ·    ·
  · ·· ···· · ·· ·               ·    ·    ·    ·
  ··· · · ·· ··· ·                ·    ·    ·    ·
  · · ····· ·· · ·               ·    ·    ·    ·
  ·· · ·· ·· ·· ··                ·    ·    ·    ·
  ↑ 看起来均匀随机                ↑ 明显落在平行线上
                                    (有规律可循!)

3.3.6 如果非要用SHA-256常数?

需要调整到满足条件的最近值:

/*
 * 基于SHA-256常数,调整为合法LCG参数
 * 原始:H0低16位 = 0xE667 = 58983
 * 调整:需要 (a-1) % 4 == 0,即 a % 4 == 1
 *       58983 % 4 = 3(不满足)
 *       58985 % 4 = 1 ✅ → a = 58985
 * 
 * 但这已经不是原来的SHA-256常数了!
 * 而且没有经过谱测试验证随机性质量
 */
#define LCG_A  58985u   // 0xE669(从SHA-256调整而来,但未经测试)
#define LCG_C  44677u   // 0xAE85(SHA-256 H1低16位,恰好是奇数)

// 可以用,但随机性质量未知 → 不如用经典参数

3.3.7 所以为什么不用SHA-256常数做LCG参数?
1. 会坏事
   SHA-256常数直接用 → 不满足全周期条件
   → 某些种子会进入短循环 → 掩码可能重复 → 安全漏洞

2. 场景不同
   SHA-256常数:设计来做"初始值",无数学约束
   LCG参数:设计来做"递推系数",有严格数学约束

3. 不可替代
   25173, 13849 经过数十年数学验证和统计测试
   是16位LCG的经典推荐参数
   换成未经验证的参数 → 风险未知

4. 完整执行过程追踪

验证码:verCode = {0xA5, 0xB3}
种子:  state = 0xA5B3

原始密文 data = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]

═══ i=0 ═══
  state = (0xA5B3 × 25173 + 13849) mod 65536
        = 某个值,假设 = 0x9308
  掩码  = 0x93 >> 8 → 0x93
  data[0] = 0x11 ^ 0x93 = 0x82

═══ i=1 ═══
  state = (0x9308 × 25173 + 13849) mod 65536
        = 假设 = 0x7A2F
  掩码  = 0x7A
  data[1] = 0x22 ^ 0x7A = 0x58

═══ i=2 ═══
  state = (0x7A2F × 25173 + 13849) mod 65536
        = 假设 = 0xE164
  掩码  = 0xE1
  data[2] = 0x33 ^ 0xE1 = 0xD2

... 以此类推,共8轮

═══ 结果 ═══
  掩码序列:[0x93, 0x7A, 0xE1, 0x4B, 0xD7, 0x0C, 0xF5, 0x6E]
            ↑ 每个都不同,看起来像随机数

  原始:[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]
  混淆:[0x82, 0x58, 0xD2, 0x0F, 0x82, 0x6A, 0x82, 0xE6]
        ↑ 面目全非

5. Hull-Dobell 定理(全周期条件)

5.1 LCG会不会出现循环?

state 只有 65536 种可能的值(0~65535)
经过若干次迭代后,state 一定会回到某个之前出现过的值
从那以后就开始循环

最好的情况:遍历完所有 65536 个值后才重复(全周期)
最坏的情况:只经过几个值就开始循环(短周期)

5.2 Hull-Dobell 定理告诉了我们什么?

当且仅当满足以下三个条件时,LCG 有全周期(周期 = m):

条件1:c 与 m 互素(最大公约数 = 1)
  m = 65536 = 2^16(质因子只有2)
  c = 13849(奇数,不含因子2)
  → gcd(13849, 65536) = 1 ✅

条件2:a - 1 能被 m 的所有质因子整除
  m = 65536,质因子 = {2}
  a - 1 = 25172
  25172 / 2 = 12586(能整除)✅

条件3:如果 4 | m,则 4 | (a-1)
  4 | 65536?是(65536 / 4 = 16384)
  4 | 25172?是(25172 / 4 = 6293)✅

三个条件都满足 → 周期 = 65536(全周期)

意味着:从任何种子出发,经过 65536 步后才会回到起点
中间会不重复地经过所有 0~65535 的值
8步(我们只用8步)绝不会产生重复的 state 值

6. 各种"碰撞/猜中"概率分析

6.1 你最可能关心的概率

(1)攻击者猜对验证码的概率

验证码 = 2字节 = 16位
可能的值:2^16 = 65,536 种

随机猜中的概率:1/65,536 ≈ 0.0015%

(2)不同验证码产生相同掩码序列的概率

掩码序列 = 8字节 = 64位
理论空间:2^64 ≈ 1.8 × 10^19 种

但!种子只有16位,所以最多只能产生 65,536 种不同的掩码序列
(65,536 种中挑2个恰好相同?需要具体分析)

具体分析:

// LCG: f(x) = (25173 * x + 13849) mod 65536
// 全周期 → f 是一个双射(一对一映射)
// 
// 种子 s1 ≠ s2
// → f(s1) ≠ f(s2)            (第1步的state一定不同)
// → 但 f(s1)>>8 可能等于 f(s2)>>8  (高8位可能碰巧相同)

// 第1个掩码字节相同的概率:
//   state有16位,高8位相同意味着低8位不同
//   对于每个高8位值,有256个state映射到它
//   概率 = 256/65536 = 1/256

// 要8个掩码字节全部相同:
//   每一步的高8位都要碰巧相同
//   粗略估计(非独立,但数量级对):
//   ≈ (1/256)^8 = 1/2^64 ≈ 5.4 × 10^-20

结论:不同验证码产生完全相同掩码的概率约 1/2^64,可以忽略。


6.2 但这里有个实际的安全上限

关键事实:种子只有 2^16 = 65,536 种

这意味着攻击者的策略是:
  不需要猜掩码(2^64种可能)
  只需要猜验证码(2^16种可能)

所以真正的安全强度取决于验证码空间:

╔═══════════════════════════════════╗
║  有效安全强度 = 16位 = 1/65,536    ║
╚═══════════════════════════════════╝

攻击场景分析

场景1:攻击者不知道验证码
  → 需要猜测验证码:65,536 种可能
  → 每次猜错就要生成一个完整的密码并尝试
  → 还需要知道密码生成的方式(才能让PC端生成对应密码)
  → 实际攻击成本很高

场景2:攻击者截获了你设计的密码
  → 该密码绑定了当时的验证码
  → 验证码在有效期失效后变化 → 截获的密码失效
  → 需要重新获取新验证码

场景3:攻击者暴力枚举
  → 遍历所有65,536个验证码
  → 需要设备配合输入65,536次?不现实
  → 离线计算?需要知道你的密钥,以及密钥生成的密码方式

6.3 各层防护的碰撞概率汇总(拿我使用过的XTEA来说)

                    碰撞/猜中概率         等效安全位数
                    ──────────────        ────────────
验证码猜中          1/65,536              16位
                    (0.0015%)

XTEA密钥猜中        1/2^128               128位
                    (≈ 2.9 × 10^-39)

预留字节碰巧正确    1/256                  8位
                    (0.39%)

校验和碰巧正确      1/65,536              16位
                    (0.0015%)

总误判概率          ≈ 1/16,777,216        24位
(预留+校验和)     (0.000006%)

掩码序列碰撞        ≈ 1/2^64              64位
                    (≈ 5.4 × 10^-20)

6.4 如果你觉得 1/65,536 不够安全

可以把验证码从2字节扩展到更长:

验证码长度 显示形式 可能数量 猜中概率
2字节 4位HEX(A5B3) 65,536 1/65,536

3字节

6位HEX(A5B3C7)

16,777,216

1/16,777,216

4字节

8位HEX(A5B3C7D9)

4,294,967,296

1/4.3×10^9

如果要扩展到3字节,MixVerificationCode 修改如下:

static void MixVerificationCode(uint8_t data[8], const uint8_t *verCode, 
                                 uint8_t verLen)
{
    uint32_t state = 0;
    int i;

    /* 组装种子(支持变长验证码) */
    for (i = 0; i < verLen; i++)
    {
        state = (state << 8) | verCode[i];
    }

    /* 使用32位LCG(适配更大种子) */
    for (i = 0; i < 8; i++)
    {
        /* 32位LCG,Numerical Recipes推荐参数 */
        state = state * 1664525u + 1013904223u;
        data[i] ^= (uint8_t)(state >> 24);  /* 取最高8位 */
    }
}

但对于我目前的场景(密码、人工获取验证码),2字节/4位HEX(1/65,536)已经足够安全,因为实际攻击需要同时满足我涉及的密码生成的4种条件(此处不展示,可自定义涉及)

7. 关键专有名词汇总

术语 全称 含义
LCG Linear Congruential Generator 线性同余生成器,用公式 next=(a*cur+c)%m 产生伪随机数
种子(seed) LCG的初始值,决定整个序列;相同种子→相同序列
全周期 Full Period LCG遍历所有可能值后才重复,周期=m
Hull-Dobell Hull-Dobell Theorem 判断LCG是否全周期的数学定理
mod Modulo 取模/取余运算,17 mod 5 = 2
互素 Coprime 两个数的最大公约数为1
XOR Exclusive OR 异或运算,A^B^B=A(自逆性)
掩码(mask) 用来异或的数据,起到"遮盖/混淆"作用
伪随机 Pseudo-Random 看起来随机,但由确定公式生成,可重现
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐