为何Chrome密码解密C脚本失败而Python正常?UTF-8是诱因吗?
Chrome密码解密C版失败问题排查与C语言学习建议
问题背景
接触C语言仅2个月,将Python版Chrome密码恢复器改写为C版时遭遇解密失败。Python脚本可正常解密,但C版调用EVP_DecryptFinal_ex时提示Decryption failed,且已确认Python与C代码中使用的IV、密文、AES密钥字节完全一致。
Python核心解密代码
def decrypt_payload(cipher, payload): return cipher.decrypt(payload) def generate_cipher(aes_key, iv): return AES.new(aes_key, AES.MODE_GCM, iv) def decrypt_password(ciphertext, secret_key): try: print("Original Password: ", ciphertext) initialisation_vector = ciphertext[3:15] print("Initialization vector: ", initialisation_vector) encrypted_password = ciphertext[15:-16] print("Encrypted_password: ", encrypted_password) print("AES key: ", secret_key) cipher = AES.new(secret_key, AES.MODE_GCM, initialisation_vector) decrypted_pass = decrypt_payload(cipher, encrypted_password) decrypted_pass = decrypted_pass.decode() print("Decrypted Password: ", decrypted_pass) sys.exit() return decrypted_pass except Exception as e: print("%s"%str(e)) print("[ERR] Unable to decrypt, Chrome version <80 not supported. Please check.") return ""
Python解密成功输出
Original Password: b'v10\xb5\xee\n\xa4k\xd5\xc8@\xca\xcfWn\xcd\xcb\xbb\x16\x11gp\x81\xbf\x0e\xaf4:\xf3IJk4>\xee#\x1cl\x1c' Initialization vector: b'\xb5\xee\n\xa4k\xd5\xc8@\xca\xcfWn' Encrypted_password: b'\xcd\xcb\xbb\x16\x11gp\x81' AES key: b'\xa7\x91%\r08\xe0\xaf\x96\xd65i\x98;\xbb\x0c\xcb\xf4\tH\x90\xa3\x90U\xb6#\xb7!\x95R1\xbc' Decrypted Password: H***** (Right Password)
C版解密代码
void decryptPassword(char* encPass) { // 12 byte initialization vector unsigned char initialization_vector[12]; memcpy(initialization_vector, encPass + 3, 12); printf("Initialization vector: "); for (int i = 0; i < 12; i++) { printf("\\x%02x", initialization_vector[i]); } printf("\n"); // Get encrypted password int encrypted_len = strlen(encPass) - 31; unsigned char encrypted_password[encrypted_len]; memcpy(encrypted_password, encPass + 15, encrypted_len); printf("Encrypted_password: "); for (int i = 0; i < encrypted_len; i++) { printf("\\x%02x", encrypted_password[i]); } printf("\n"); printf("AES key: "); for (int i = 0; i < 32; i++) { printf("\\x%02x", AESkey[i]); } printf("\n"); EVP_CIPHER_CTX *ctx; int len; int plaintext_len; unsigned char plaintext[32]; unsigned char tag[16]; AES_KEY aes_key; if (AES_set_decrypt_key(AESkey, 256, &aes_key) < 0) { fprintf(stderr, "Could not set decryption key."); exit(1); } if (!(ctx = EVP_CIPHER_CTX_new())) { handleErrors(); } if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL)) { handleErrors(); } if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL)) { handleErrors(); } if (1 != EVP_DecryptInit_ex(ctx, NULL, NULL, AESkey, initialization_vector)) { handleErrors(); } if (1 != EVP_DecryptUpdate(ctx, NULL, &len, encPass + 15, encrypted_len)) { handleErrors(); } if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, encPass + 15 + encrypted_len)) { handleErrors(); } if (1 != EVP_DecryptFinal_ex(ctx, plaintext, &len)) { fprintf(stderr, "Decryption failed."); exit(1); } plaintext_len = len; if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag)) { handleErrors(); } EVP_CIPHER_CTX_free(ctx); printf("decrypted_password: %s\n", plaintext); }
C版运行输出
Original Password: \x76\x31\x30\xb5\xee\x0a\xa4\x6b\xd5\xc8\x40\xca\xcf\x57\x6e\xcd\xcb\xbb\x16\x11\x67\x70\x81\xbf\x0e\xaf\x34\x3a\xf3\x49\x4a\x6b\x34\x3e\xee\x23\x1c\x6c\x1c Initialization vector: \xb5\xee\x0a\xa4\x6b\xd5\xc8\x40\xca\xcf\x57\x6e Encrypted_password: \xcd\xcb\xbb\x16\x11\x67\x70\x81 AES key: \xa7\x91\x25\x0d\x30\x38\xe0\xaf\x96\xd6\x35\x69\x98\x3b\xbb\x0c\xcb\xf4\x09\x48\x90\xa3\x90\x55\xb6\x23\xb7\x21\x95\x52\x31\xbc Decryption failed.
问题排查与修正
1. 核心错误:EVP_DecryptUpdate调用参数错误
你在调用EVP_DecryptUpdate时将输出缓冲区设为NULL,这会导致解密过程无法生成明文数据,后续EVP_DecryptFinal_ex验证tag时必然失败。Python的AES.decrypt会直接处理密文并返回明文,而OpenSSL的EVP接口要求必须通过EVP_DecryptUpdate输出明文。
修正:将EVP_DecryptUpdate的输出缓冲区改为plaintext,并记录明文长度:
// 替换原EVP_DecryptUpdate调用 if (1 != EVP_DecryptUpdate(ctx, plaintext, &len, encPass + 15, encrypted_len)) { handleErrors(); } plaintext_len = len;
2. 冗余代码:无用的AES_set_decrypt_key调用
你使用的是OpenSSL的EVP高级加密接口,AES_set_decrypt_key是低级接口的函数,与当前上下文无关,直接删除即可,避免代码混淆。
3. 潜在问题:二进制数据的长度计算
strlen(encPass)仅适用于ASCII字符串,Chrome的加密密码是二进制数据,若其中包含\0空字节,strlen会提前终止,导致长度计算错误。正确做法是在传递加密数据时,同时传入其真实的字节长度,而非依赖strlen:
// 修改函数签名,增加长度参数 void decryptPassword(char* encPass, int encPass_len) { // ... int encrypted_len = encPass_len - 3 - 12 - 16; // 总长度 - 前缀(3) - IV(12) - tag(16) // ... }
4. 最终修正:添加字符串终止符
解密后的明文是UTF-8字节流,没有自动添加\0终止符,直接用printf("%s", plaintext)会输出乱码,需要手动添加:
plaintext[plaintext_len] = '\0'; printf("decrypted_password: %s\n", plaintext);
修正后的完整核心代码片段
void decryptPassword(char* encPass, int encPass_len) { unsigned char initialization_vector[12]; memcpy(initialization_vector, encPass + 3, 12); int encrypted_len = encPass_len - 3 - 12 - 16; unsigned char encrypted_password[encrypted_len]; memcpy(encrypted_password, encPass + 15, encrypted_len); EVP_CIPHER_CTX *ctx; int len; int plaintext_len; unsigned char plaintext[32]; if (!(ctx = EVP_CIPHER_CTX_new())) { handleErrors(); } if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL)) { handleErrors(); } if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL)) { handleErrors(); } if (1 != EVP_DecryptInit_ex(ctx, NULL, NULL, AESkey, initialization_vector)) { handleErrors(); } if (1 != EVP_DecryptUpdate(ctx, plaintext, &len, encPass + 15, encrypted_len)) { handleErrors(); } plaintext_len = len; if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, encPass + 15 + encrypted_len)) { handleErrors(); } if (1 != EVP_DecryptFinal_ex(ctx, plaintext + plaintext_len, &len)) { fprintf(stderr, "Decryption failed.\n"); EVP_CIPHER_CTX_free(ctx); exit(1); } plaintext_len += len; plaintext[plaintext_len] = '\0'; EVP_CIPHER_CTX_free(ctx); printf("decrypted_password: %s\n", plaintext); }
C语言学习建议
- 重视二进制数据处理:C语言没有原生的字节数组类型,字符串和二进制数据都是
char*,但两者本质不同。处理二进制数据时,必须携带真实长度,绝对不能用strlen、strcpy等字符串函数。 - 吃透OpenSSL接口:OpenSSL的EVP接口是加密标准实现,一定要仔细阅读官方文档或可靠教程,不要凭Python的经验套用法则。比如GCM模式下,
EVP_DecryptFinal_ex的作用是验证tag,而非生成最后一段明文(除非密文长度是块大小的整数倍)。 - 调试技巧:用
printf逐字节打印二进制数据对比,或者用GDB断点查看内存中的字节值,确认IV、密钥、密文、tag的每一个字节都与Python端一致。 - 从基础库入手:先掌握C标准库的内存操作(
memcpy、memset)、错误处理,再接触第三方库。避免跳过基础直接写复杂功能。 - 多写小案例:比如先实现简单的AES-ECB加密解密,再过渡到GCM模式,逐步熟悉加密库的调用逻辑,不要一开始就处理Chrome密码这种带有格式的复杂场景。
内容的提问来源于stack exchange,提问作者M A I N
相关产品推荐
相关产品推荐

