基于OpenSSL的AES-CTR PRNG实现:一致性输出与内存泄漏问题
问题:nwipe中AES-CTR PRNG的一致性失效与Valgrind未初始化值警告
背景
我在为nwipe项目实现基于OpenSSL的AES-CTR模式伪随机数生成器(PRNG),要求相同种子多次运行时生成的随机数完全一致。但实际测试中,第4次运行时随机数出现差异,同时Valgrind检测到OpenSSL内部函数存在未初始化值导致的条件跳转。
代码实现
初始化调用
int nwipe_aes_ctr_prng_init(NWIPE_PRNG_INIT_SIGNATURE) { nwipe_log(NWIPE_LOG_NOTICE, "Initialising AES CTR PRNG"); if (*state == NULL) { *state = calloc(1, sizeof(aes_ctr_state_t)); // 使用calloc分配并初始化内存 if (*state == NULL) { nwipe_log(NWIPE_LOG_FATAL, "Failed to allocate memory for AES CTR PRNG state."); return -1; // 返回错误码表示异常 } } aes_ctr_prng_init((aes_ctr_state_t*)*state, (unsigned long*)(seed->s), seed->length / sizeof(unsigned long)); return 0; // 成功 }
随机数生成调用
int nwipe_aes_ctr_prng_read(NWIPE_PRNG_READ_SIGNATURE) { u8* restrict bufpos = buffer; size_t words = count / SIZE_OF_AES_CTR_PRNG; for(size_t ii = 0; ii < words; ++ii) { aes_ctr_prng_genrand_uint128_to_buf((aes_ctr_state_t*) *state, bufpos); bufpos += SIZE_OF_AES_CTR_PRNG; // 移动到下一个块 } // 处理count不是SIZE_OF_AES_CTR_PRNG整数倍的剩余字节 const size_t remain = count % SIZE_OF_AES_CTR_PRNG; if(remain > 0) { unsigned char temp_output[16]; // 最后一个块的临时缓冲区 aes_ctr_prng_genrand_uint128_to_buf((aes_ctr_state_t*) *state, temp_output); memcpy(bufpos, temp_output, remain); } return 0; // 成功 }
结构体定义
typedef struct { EVP_CIPHER_CTX *ctx; unsigned char ivec[AES_BLOCK_SIZE]; unsigned int num; unsigned char ecount[AES_BLOCK_SIZE]; } aes_ctr_state_t;
随机数生成函数
void aes_ctr_prng_genrand_uint128_to_buf(aes_ctr_state_t* state, unsigned char* bufpos) { int outlen; EVP_EncryptUpdate(state->ctx, bufpos, &outlen, bufpos, 16); // OpenSSL内部调用CRYPTO_ctr128_encrypt_ctr32 }
初始化函数
void aes_ctr_prng_init(aes_ctr_state_t* state, unsigned long init_key[], unsigned long key_length) { unsigned char key[32]; // 用于存储256位密钥 memset(state->ivec, 0, AES_BLOCK_SIZE); state->num = 0; memset(state->ecount, 0, AES_BLOCK_SIZE); SHA256_CTX sha256; SHA256_Init(&sha256); SHA256_Update(&sha256, (unsigned char*)init_key, key_length * sizeof(unsigned long)); SHA256_Final(key, &sha256); // 生成最终密钥 state->ctx = EVP_CIPHER_CTX_new(); if (state->ctx != NULL) { EVP_EncryptInit_ex(state->ctx, EVP_aes_256_ctr(), NULL, key, state->ivec); } }
实现逻辑
初始化阶段将IV、计数器num和ecount置零,通过SHA256对种子生成256位密钥,初始化AES-256-CTR模式;随机数通过原地加密缓冲区生成。
遇到的问题
- 相同种子多次运行时,第4次迭代生成的随机数出现差异
- Valgrind检测到OpenSSL的
CRYPTO_ctr128_encrypt_ctr32函数存在基于未初始化值的条件跳转,日志如下:
==40641== Conditional jump or move depends on uninitialised value(s) ==40641== at 0x4B9F565: CRYPTO_ctr128_encrypt_ctr32 (ctr128.c:183) ==40641== by 0x4C655A1: ossl_cipher_hw_generic_ctr (ciphercommon_hw.c:117) ==40641== by 0x4C60FCC: ossl_cipher_generic_stream_update (ciphercommon.c:469) ==40641== by 0x4B61076: EVP_EncryptUpdate (evp_enc.c:643) ==40641== by 0x414314: aes_ctr_prng_genrand_uint128_to_buf (aes_ctr_prng.c:133) ==40641== by 0x417F5F: nwipe_aes_ctr_prng_read (prng.c:293) ...
核心疑问
- 如何确保相同种子多次运行时,PRNG输出完全一致?
- 如何解决Valgrind报告的未初始化值问题,是否遗漏了状态初始化步骤?
解决方案建议
针对一致性问题
彻底重置PRNG状态
当前代码中,若*state非空会直接复用旧状态,但未清理OpenSSL的EVP_CIPHER_CTX内部残留数据,导致输出不一致。修改初始化逻辑:if (*state != NULL) { // 清理旧的加密上下文 if ((*state)->ctx != NULL) { EVP_CIPHER_CTX_free((*state)->ctx); (*state)->ctx = NULL; } // 重置所有状态字段 memset((*state)->ivec, 0, AES_BLOCK_SIZE); (*state)->num = 0; memset((*state)->ecount, 0, AES_BLOCK_SIZE); } else { *state = calloc(1, sizeof(aes_ctr_state_t)); if (*state == NULL) { nwipe_log(NWIPE_LOG_FATAL, "Failed to allocate memory for AES CTR PRNG state."); return -1; } }统一种子的字节级处理
原代码将种子强制转换为unsigned long*,依赖平台相关的类型大小和端序,可能导致SHA256输入不一致。改为直接按字节处理种子:// 重写初始化函数参数 void aes_ctr_prng_init(aes_ctr_state_t* state, const unsigned char* init_seed, size_t seed_length) { unsigned char key[32]; memset(state->ivec, 0, AES_BLOCK_SIZE); state->num = 0; memset(state->ecount, 0, AES_BLOCK_SIZE); SHA256_CTX sha256; SHA256_Init(&sha256); SHA256_Update(&sha256, init_seed, seed_length); SHA256_Final(key, &sha256); state->ctx = EVP_CIPHER_CTX_new(); if (state->ctx != NULL) { EVP_EncryptInit_ex(state->ctx, EVP_aes_256_ctr(), NULL, key, state->ivec); } } // 调用时传递原始字节流 aes_ctr_prng_init((aes_ctr_state_t*)*state, seed->s, seed->length);验证计数器初始状态
每次初始化后打印ivec、num的值,确保全为0,避免OpenSSL内部对计数器的隐性修改导致状态不一致。
针对Valgrind未初始化值问题
修复PRNG生成逻辑
原代码使用未初始化的缓冲区作为EVP_EncryptUpdate的输入,CTR模式下会将未初始化数据与密钥流异或,导致输出依赖未初始化值。改为输入全零缓冲区:void aes_ctr_prng_genrand_uint128_to_buf(aes_ctr_state_t* state, unsigned char* bufpos) { int outlen; unsigned char zero_buf[16] = {0}; EVP_EncryptUpdate(state->ctx, bufpos, &outlen, zero_buf, 16); }初始化OpenSSL上下文
EVP_CIPHER_CTX_new可能不会完全初始化所有字段,添加重置操作确保状态干净:state->ctx = EVP_CIPHER_CTX_new(); if (state->ctx != NULL) { EVP_CIPHER_CTX_reset(state->ctx); EVP_EncryptInit_ex(state->ctx, EVP_aes_256_ctr(), NULL, key, state->ivec); }显式初始化栈变量
在nwipe_aes_ctr_prng_read中,显式初始化临时缓冲区:unsigned char temp_output[16] = {0};
内容的提问来源于stack exchange,提问作者FBDIMM
相关产品推荐
相关产品推荐

