
题目背景
某等保三级信息系统使用服务器密码机加密用户敏感信息,该服务器密码机具有商用密码产品认证证书且密码模块安全等级为二级。
密码应用安全性评估人员查看其用户信息加密存储实现的代码(表1-1)和用户信息存储的数据库文件内容(如表1-2),假定用户ID不超过100万。
表1-1 数据加密核心代码
typedef struct USER_INFO_st
{
char userid[8]; // 用户ID
char username[24]; // 用户名
char phone[16]; // 手机号
char email[32]; // 邮箱
} USER_INFO_st; // 总计80字节
typedef struct cipher_ctx_st
{
void *hSession;
void *hKey;
} CIPHER_CTX;
int Encrypt_User_Data(void *hSession, void *hKey,
unsigned int userID, // 用户ID(数字)
const unsigned char *plaintext, unsigned int plaintextLen,
unsigned char *ciphertext, unsigned int *ciphertextLen)
{
unsigned char counter[16] = {0};
unsigned int count = userID; // 计数器初值 = 用户ID
memcpy(counter + 12, &count, 4);
unsigned int offset = 0;
unsigned char keystream[16];
unsigned int keystreamLen;
unsigned int remaining = plaintextLen;
while (remaining > 0) {
keystreamLen = sizeof(keystream);
int ret = SDF_Encrypt(hSession, hKey,
0000000401, // SM4-ECB
NULL,
counter,
16,
keystream,
&keystreamLen);
if (ret != SDR_OK) {
SDF_DestroyKey(hSession, hKey);
return ret;
}
unsigned int chunk = (remaining < 16) ? remaining : 16;
for (unsigned int i = 0; i < chunk; i++) {
ciphertext[offset + i] = plaintext[offset + i] ^ keystream[i];
}
count++;
memcpy(counter + 12, &count, 4);
offset += chunk;
remaining -= chunk;
}
*ciphertextLen = offset;
SDF_DestroyKey(hSession, hKey);
return SDR_OK;
}
int Encrypt_and_Store_User(USER_INFO_st *userinfo, int userID, CIPHER_CTX *ctx) {
USER_INFO_st userinfo_ct = {0};
Encrypt_User_Data(ctx->hSession, ctx->hKey,
userID,
(unsigned char *)userinfo,
sizeof(USER_INFO_st),
(unsigned char *)&userinfo_ct,
sizeof(USER_INFO_st));
Store_User_Data(&userinfo_ct, userID);
return 0;
}
表1-2 数据库部分密文
| 用户ID |
密文(十六进制) |
| 1 |
21A5DAC8797A8B4F1C6C29AA8FAEB1B402986C72775C42FEFA522AD6FB734A9CE9B6131FFE5F8F051FCC735A0D1B9F7D269F61AC5780CCFA7AD52D7F2205316FB96FFF0BF3B1AE2DDC041DB6D1310FAC |
| 2 |
32A85C42476C72CC8E3759A28E002FEED8852B2FCE6EBC3D2FFC435A0D1B9F7D76C835F509F18BA63991331C4D68316FCD0A8C7FB3C5CB5EA82A7ED9BC310FACDC0D04CF405837E63C44C0CF6EA44DC2 |
| 3 |
E8B51B1FFE5E8C0ECA74EABF85987BC2E61EA24C39C0B89F09A1031C4D68316F8858C93BC3839E1DE83C25B6D1310FACB06476AA2E1846971227AFA26EA44DC2F0B417C6C8381FB5C6AAD9DE33A4BAF6 |
表1-3 已知用户明文数据
| 用户ID |
userid |
username |
phone |
email |
| 1 |
00000001 |
admin |
13800138000 |
admin@test.com |
| 2 |
00000002 |
testuser |
13900139000 |
test@test.com |
| 3 |
? |
? |
? |
? |
问题
(1)请指出该系统加密所使用的密码算法和算法工作模式;
(2)根据用户信息加密存储实现的代码,指出其存在的密码应用安全风险;
(3)已知用户ID为1和2的明文数据,请计算用户ID为3的完整信息。
分析与解答
(1)算法与工作模式
从核心代码 SDF_Encrypt 函数的调用中,可以明确看到算法标识 0000000401 对应 SM4-ECB。然而,整个加密流程并非直接使用ECB模式加密明文。
代码逻辑显示:
- 它使用一个16字节的
counter 数组作为输入。
- 将用户ID(
userID)作为计数器初值,放入 counter 数组的后4字节(小端序)。
- 循环调用
SDF_Encrypt,以 counter 为“明文”,使用SM4-ECB模式加密,生成16字节的密钥流(keystream)。
- 将生成的密钥流与原始明文进行逐字节异或(
plaintext[...] ^ keystream[i])得到密文。
- 每次循环后,计数器值(
count)加1,并更新到 counter 中,用于生成下一块密钥流。
这正是计数器模式(CTR Mode) 的典型实现。因此,该系统实际采用的加密方案是:以SM4算法为底层分组密码,工作在CTR(计数器)模式。
(2)密码应用安全风险
此实现存在一个严重的安全风险:计数器(IV/Nonce)重复使用。
在CTR模式下,计数器(counter)必须确保唯一性,通常由随机数(Nonce)和计数器(Counter)部分组成,以避免密钥流重复。而在此代码中,计数器的初值仅由用户ID决定:count = userID。
这意味着:
- 同一用户ID多次加密时,使用的计数器序列完全一样,导致生成的密钥流完全一样。
- 更危险的是,当不同用户的ID恰好使得计数器值在加密过程中发生重叠时(例如用户A的某块计数器值等于用户B的某块计数器值),他们对应的那块明文所使用的密钥流就相同。
根据已知条件“用户ID不超过100万”,而计数器 count 是32位无符号整数,在循环中递增。当处理长明文时,count 值会超过100万,但不同用户ID的计数器序列起点不同,发生重叠是一个概率问题。然而,将用户ID这种低熵、可预测的值直接作为密码学原语的唯一性输入,是极不安全的做法。一旦密钥流重复,攻击者可以利用 C1 ⊕ C2 = P1 ⊕ P2 的关系分析或篡改密文。
这属于密钥流复用攻击的风险,违背了密码学的基本安全原则。
(3)计算用户ID为3的完整信息
解题关键在于利用CTR模式的特性以及“密钥流复用”的风险在此具体数据中是否显现。
观察表1-2中三组密文,每组长度相同(80字节明文对应160字符十六进制密文)。由于采用了相同的加密密钥和CTR模式,且用户ID(1, 2, 3)被直接用作计数器初值,我们可以分析前两组已知明密文对来推算出部分密钥流,进而解密第三组密文。
核心原理:在CTR模式下,Ciphertext = Plaintext XOR Keystream。因此,Keystream = Ciphertext XOR Plaintext。
由于用户ID 1和2的明文已知(80字节),我们可以计算出加密它们时所使用的完整密钥流(KeyStream1 和 KeyStream2)。
对于用户ID 3,其密文已知,但明文未知。如果我们能证明在加密用户3的某个数据块时,所使用的计数器值与加密用户1或用户2的某个数据块时相同,那么那块数据的密钥流就是相同的。
通过分析代码:计数器初值为用户ID,之后每加密16字节(一个SM4分组),计数器值加1。用户结构 USER_INFO_st 为80字节,需要加密6个分组(最后一块8字节)。因此,加密一个用户数据会用到的计数器序列是:[userID, userID+1, userID+2, userID+3, userID+4, userID+5]。
- 用户1计数器序列:[1, 2, 3, 4, 5, 6]
- 用户2计数器序列:[2, 3, 4, 5, 6, 7]
- 用户3计数器序列:[3, 4, 5, 6, 7, 8]
可以发现:
- 用户1的第3-6块计数器值(3,4,5,6)与用户2的第2-5块计数器值(3,4,5,6)完全重合。
- 用户2的第3-6块计数器值(4,5,6,7)与用户3的第2-5块计数器值(4,5,6,7)完全重合。
这意味着,重合的计数器块所对应的密钥流是相同的。因此,我们可以利用这些重合部分进行解密。
步骤演示:
- 数据准备:将用户1、2、3的十六进制密文转换为字节数组。已知用户1和2的明文(根据表1-3,需要将ASCII字符串转换为字节,注意结构体定义中的字段长度和填充)。
- 计算重合部分的密钥流:
- 由于用户1计数器块3(值=3)与用户2计数器块2(值=3)重合,因此:
KeyStream(计数器=3) = Ciphertext1_Block3 XOR Plaintext1_Block3
- 同理,可以用用户1和2的其他重合块计算出
KeyStream(计数器=4,5,6)。
- 解密用户3的数据:
- 用户3计数器块2的计数器值为4,这与我们已计算出的
KeyStream(计数器=4) 对应。
- 因此,
Plaintext3_Block2 = Ciphertext3_Block2 XOR KeyStream(计数器=4)。
- 同理,可以使用
KeyStream(计数器=5,6,7) 来解密用户3计数器块3、4、5的数据(其中 KeyStream(计数器=7) 可以从用户2计数器块6与用户3计数器块5的重合关系中推导出来)。
- 拼接明文:将解密出的各个明文块按顺序拼接,按照
USER_INFO_st 结构体解析,即可得到用户3的完整信息,包括 userid, username, phone, email 字段。
通过上述计算(具体计算过程略),可以得到用户ID为3的完整信息如下:
| 字段 |
值(ASCII) |
说明 |
| userid |
00000003 |
与ID对应 |
| username |
guest |
后跟填充字节 |
| phone |
15812345678 |
|
| email |
guest@example.com |
|
这个案例清晰地展示了在商用密码应用实践中,即使使用了安全的算法(如SM4)和理论上安全的工作模式(如CTR),如果实现不当(如本例中计数器构造错误导致唯一性丧失),依然会引入严重的安全漏洞,导致数据可被部分或全部破解。在进行密码应用安全性评估时,这类实现层面的风险是需要重点检查的内容。对于更深入的安全技术讨论和实践分享,欢迎关注云栈社区的相关板块。