AES-CTR PRNG Implementation in OpenSSL with Consistent Output and Memory Leak Issues

59 Views Asked by At

I'm implementing a Pseudo-Random Number Generator (PRNG) using AES-CTR mode through OpenSSL for a project aimed at secure data wiping (nwipe). My goal is to achieve consistent random numbers when wiping a disk with the same seed across multiple runs. However, I encounter unexpected behavior on the 4th run where the random numbers start to differ despite identical input parameters. Additionally, I'm struggling with a potential memory leak that I can't pinpoint.

Initialization Call:

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)); // Using calloc for memory allocation and initialization
        if (*state == NULL) {
            nwipe_log(NWIPE_LOG_FATAL, "Failed to allocate memory for AES CTR PRNG state.");
            return -1; // Return an error code indicating a problem
        }
    }

    aes_ctr_prng_init((aes_ctr_state_t*)*state, (unsigned long*)(seed->s), seed->length / sizeof(unsigned long));

    return 0; // Success
}

Random Number Generation Call:

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;  // Move to the next block
    }

    // Handle remaining bytes if count is not a multiple of SIZE_OF_AES_CTR_PRNG
    const size_t remain = count % SIZE_OF_AES_CTR_PRNG;
    if(remain > 0) {
        unsigned char temp_output[16];  // Temporary buffer for the last block
        aes_ctr_prng_genrand_uint128_to_buf((aes_ctr_state_t*) *state, temp_output);
        memcpy(bufpos, temp_output, remain);
    }

    return 0;  // Success
}

Struct Definition:

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;

The PRNG itself.

Random Number Generation Function:

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 internally calls CRYPTO_ctr128_encrypt_ctr32
}

And it's init function i implemented.

Initializing Function:

void aes_ctr_prng_init(aes_ctr_state_t* state, unsigned long init_key[], unsigned long key_length) {
    unsigned char key[32]; // Platz für einen 256-Bit Schlüssel

    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); // Generiere den endgültigen Schlüssel

    state->ctx = EVP_CIPHER_CTX_new();
    if (state->ctx != NULL) {
        EVP_EncryptInit_ex(state->ctx, EVP_aes_256_ctr(), NULL, key, state->ivec);
    }
}

Key Initialization and Random Generation Implementation: The initialization zeroes out IV, counter, and ecount before use. The key is derived using SHA256 on the provided seed, and AES-256-CTR mode is initialized. Random numbers are generated by encrypting a buffer in place.

Issue Encountered: Despite ensuring identical seed and parameters for each run, the 4th iteration produces different random numbers. Valgrind reports a conditional jump based on uninitialized values within the OpenSSL CRYPTO_ctr128_encrypt_ctr32 function, suggesting a potential issue with how the state or buffer is initialized or used.

Valgrind Log:

==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)
...

Question:

  1. How can I ensure consistent random numbers across multiple runs with the same seed?
  2. How do I address the uninitialized value(s) that Valgrind is reporting? Is there a step I'm missing in initializing my AES-CTR PRNG state? I can't figure out any issues with my approach of initializing the memory like i did. But obviously it causes errors, always after the 3rd run, the 4th produces different data.

Any insights or suggestions to debug these issues would be greatly appreciated. Thank you!

1

There are 1 best solutions below

0
FBDIMM On

Thanks a lot folks for your help. I was able to fix the issue. What caused the issue, was likely a buffer overflow of bufpos, corrupting the memory of the other parameters. I've modified the code now, providing a temporary buffer, and writing 4x 32-Bit instead of one-time 128-Bit, and now it's performing properly!

Before:

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 internally calls CRYPTO_ctr128_encrypt_ctr32
}

After:

/* Generates pseudorandom numbers and writes them to a buffer.
   - state: Pointer to the initialized AES CTR PRNG state.
   - bufpos: Target buffer where the pseudorandom numbers will be written. */
void aes_ctr_prng_genrand_uint128_to_buf( aes_ctr_state_t* state, unsigned char* bufpos )
{
    unsigned char temp_buffer[16]; /* Intermediate buffer for 128 bits */
    int outlen;

    /* Generate pseudorandom numbers in the intermediate buffer */
    EVP_EncryptUpdate( state->ctx, temp_buffer, &outlen, temp_buffer, sizeof( temp_buffer ) );

    /* Write the data from the intermediate buffer to bufpos in four 32-bit steps.
       This process is crucial to prevent a buffer overflow of bufpos, as it ensures
       that exactly 16 bytes (128 bits) of pseudorandom data are safely transferred
       into bufpos. Copying the data in controlled 32-bit segments allows for precise
       management of the memory space allocated to bufpos, mitigating the risk of
       writing beyond its allocated size. */
    int i = 0;
    while( i < 4 )
    {
        /* Copy each 32-bit segment from the intermediate buffer to the target buffer.
           This step-by-step approach is essential for maintaining the integrity of
           the buffer and ensuring that only the intended amount of data is written.
           The ternary operator is used here for illustrative purposes and does not
           alter the functionality. */
        memcpy( bufpos + ( i * 4 ), temp_buffer + ( i * 4 ), 4 );
        i = ( i < 4 ) ? i + 1 : 4;  // Ternary operator to increment i or keep it at 4
    }
}

nwipe-aes-ctr