LCG加密:2字节生成8字节随机掩码
·
/**
* @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 | 看起来随机,但由确定公式生成,可重现 |
更多推荐



所有评论(0)