openssl / openssl

TLS/SSL and crypto library

Home Page:https://www.openssl.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

HKDF + BLAKE2S256 outputs uninitialized memory

guidovranken opened this issue · comments

#include <openssl/kdf.h>
#include <openssl/evp.h>

#define CF_CHECK_EQ(expr, res) if ( (expr) != (res) ) { goto end; }
#define CF_CHECK_NE(expr, res) if ( (expr) == (res) ) { goto end; }

int main(void)
{
    const uint8_t password[] = {0x1a, 0x2d};
    const uint8_t salt[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00};
    const uint8_t info[] = {};

    EVP_PKEY_CTX* pctx = NULL;
    const EVP_MD* md = NULL;

    size_t out_size = 13;
    uint8_t* out = malloc(out_size);

    /* Initialize */
    {
        CF_CHECK_NE(md = EVP_blake2s256(), NULL);
        CF_CHECK_NE(pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL), NULL);
        CF_CHECK_EQ(EVP_PKEY_derive_init(pctx), 1);
        CF_CHECK_EQ(EVP_PKEY_CTX_set_hkdf_md(pctx, md), 1);
        CF_CHECK_EQ(EVP_PKEY_CTX_set1_hkdf_key(pctx, password, sizeof(password)), 1);
        CF_CHECK_EQ(EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, sizeof(salt)), 1);
        CF_CHECK_EQ(EVP_PKEY_CTX_add1_hkdf_info(pctx, info, sizeof(info)), 1);
    }

    /* Process/finalize */
    {
        CF_CHECK_EQ(EVP_PKEY_derive(pctx, out, &out_size) , 1);

        CF_CHECK_NE(out, NULL);
    }

    for (size_t i = 0; i < out_size; i++) {
        printf("0x%02X, ", out[i]);
    }
    printf("\n");

end:
    EVP_PKEY_CTX_free(pctx);

    free(out);

    return 0;
}

This should print:

0x62, 0xF9, 0x92, 0x31, 0x76, 0x0B, 0xED, 0xD7, 0x23, 0x19, 0xCC, 0x6C, 0xAD,

But instead it outputs random, uninitialized memory. Bug was introduced very recently (it was still working correctly on November 7). Found by OSS-Fuzz.

valgrind --track-origins=yes ./a.out

==2650445== Use of uninitialised value of size 8
==2650445==    at 0x4A9363A: _itoa_word (_itoa.c:180)
==2650445==    by 0x4AAF574: __vfprintf_internal (vfprintf-internal.c:1687)
==2650445==    by 0x4A99D3E: printf (printf.c:33)
==2650445==    by 0x314260: main (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)
==2650445==  Uninitialised value was created by a stack allocation
==2650445==    at 0x3A66A6: kdf_hkdf_derive (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)
==2650445== 
==2650445== Conditional jump or move depends on uninitialised value(s)
==2650445==    at 0x4A9364C: _itoa_word (_itoa.c:180)
==2650445==    by 0x4AAF574: __vfprintf_internal (vfprintf-internal.c:1687)
==2650445==    by 0x4A99D3E: printf (printf.c:33)
==2650445==    by 0x314260: main (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)
==2650445==  Uninitialised value was created by a stack allocation
==2650445==    at 0x3A66A6: kdf_hkdf_derive (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)
==2650445== 
==2650445== Conditional jump or move depends on uninitialised value(s)
==2650445==    at 0x4AB0228: __vfprintf_internal (vfprintf-internal.c:1687)
==2650445==    by 0x4A99D3E: printf (printf.c:33)
==2650445==    by 0x314260: main (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)
==2650445==  Uninitialised value was created by a stack allocation
==2650445==    at 0x3A66A6: kdf_hkdf_derive (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)
==2650445== 
==2650445== Conditional jump or move depends on uninitialised value(s)
==2650445==    at 0x4AAF6EE: __vfprintf_internal (vfprintf-internal.c:1687)
==2650445==    by 0x4A99D3E: printf (printf.c:33)
==2650445==    by 0x314260: main (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)
==2650445==  Uninitialised value was created by a stack allocation
==2650445==    at 0x3A66A6: kdf_hkdf_derive (in /home/jhg/ossl-libgcrypt-kdf/cryptofuzz/a.out)

Assuming the repro code's info array is indeed supposed to be empty (this is invalid C and clang refuses it), I can repro this being 6d1e730 (and repro the correct output on bookworm openssl and pre-#22444 trunk).

This is quite surprising given that this PR only touched the hash algorithm driver itself, and not any KDF code.

Building with debug on and a small bisexion, this corresponds to providers/implementations/kdfs/hkdf.c#HKDF()'s prk variable. Prior to the patch, the prk_len variable is 32, and with the patch it's 64. Assuming it's supposed to match the hash's width, it should be 32 of course.

What's less rosy is that it's actually

│B+    382      sz = EVP_MD_get_size(evp_md);
│      383      if (sz < 0)
│      384          return 0;
│      385      prk_len = (size_t)sz;

so this means I replaced all the 512s with a macro parameter but not the 64s, of which there appears to be just one:

static int blake##variantsize##_get_params(OSSL_PARAM params[]) \
{ \
    return ossl_digest_default_get_params(params, BLAKE##VARIANT##_BLOCKBYTES, 64, 0); \
} \

which is mildly surprising a test didn't catch (and more so, mayhap, that it wasn't a macro already).

Replacing that with BLAKE##VARIANT##_OUTBYTES yields:

$ LD_LIBRARY_PATH=$PWD valgrind -s  --track-origins=yes ./iss
==21055== Memcheck, a memory error detector
==21055== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==21055== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==21055== Command: ./iss
==21055==
0x62, 0xF9, 0x92, 0x31, 0x76, 0x0B, 0xED, 0xD7, 0x23, 0x19, 0xCC, 0x6C, 0xAD,
==21055==
==21055== HEAP SUMMARY:
==21055==     in use at exit: 0 bytes in 0 blocks
==21055==   total heap usage: 4,240 allocs, 4,240 frees, 233,813 bytes allocated
==21055==
==21055== All heap blocks were freed -- no leaks are possible
==21055==
==21055== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

#22710

Thank you. With my fuzzer I've confirmed that your patch resolves the issue. Tested on x64 and x86.