什么可以解释对free()的调用堆腐败?

时间:2015-03-29 22:40:06

标签: c++ c memory memory-management openssl

我已经调试了几天的崩溃,这发生在OpenSSL的深处(与维护者here讨论)。我花了一些时间进行调查,所以我试着让这个问题变得有趣且内容丰富。

首先,为了给出一些上下文,我的最小样本再现崩溃如下:

#include <openssl/crypto.h>
#include <openssl/ec.h>
#include <openssl/objects.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/engine.h>

int main()
{
    ERR_load_crypto_strings(); OpenSSL_add_all_algorithms(); 
    ENGINE_load_builtin_engines();

    EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_sect571k1);
    EC_GROUP_set_point_conversion_form(group, POINT_CONVERSION_UNCOMPRESSED);
    EC_KEY* eckey = EC_KEY_new();
    EC_KEY_set_group(eckey, group);
    EC_KEY_generate_key(eckey);
    BIO* out = BIO_new(BIO_s_file());
    BIO_set_fp(out, stdout, BIO_NOCLOSE);
    PEM_write_bio_ECPrivateKey(out, eckey, NULL, NULL, 0, NULL, NULL); // <= CRASH.
}

基本上,此代码生成椭圆曲线键并尝试将其输出到stdout。类似的代码可以在openssl.exe ecparam和在线维基上找到。它在Linux上运行正常(valgrind报告根本没有错误)。 它只在Windows上崩溃(Visual Studio 2013 - x64)。我确保正确的运行时链接到(/MD在我的情况下,对于所有依赖项)。

害怕没有邪恶,我在x64-debug中重新编译了OpenSSL(这次将/MDd中的所有内容链接起来),并逐步完成代码以查找有问题的指令集。我的搜索引导我使用此代码(在OpenSSL&#39; s tasn_fre.c文件中):

static void asn1_item_combine_free(ASN1_VALUE **pval, const ASN1_ITEM *it, int combine)
{
    // ... some code, not really relevant.
    tt = it->templates + it->tcount - 1;

    for (i = 0; i < it->tcount; tt--, i++) {
        ASN1_VALUE **pseqval;
        seqtt = asn1_do_adb(pval, tt, 0);
        if (!seqtt) continue;

        pseqval = asn1_get_field_ptr(pval, seqtt);
        ASN1_template_free(pseqval, seqtt);
    }
    if (asn1_cb)
        asn1_cb(ASN1_OP_FREE_POST, pval, it, NULL);
    if (!combine) {
        OPENSSL_free(*pval); // <= CRASH OCCURS ON free()
        *pval = NULL;
    }
    // Some more code...
}

对于那些不熟悉OpenSSL及其ASN.1例程的人来说,基本上这个for - 循环的作用是它通过序列的所有元素(从最后一个元素开始)和&#34;删除&#34;他们(稍后会详述)。

在崩溃发生之前,删除了3个元素的序列(*pval,即0x00000053379575E0)。看看记忆,可以看到以下事情发生:

memory dump #1

序列长度为12个字节,每个元素长度为4个字节(在本例中为2510)。在每次循环迭代中,元素被删除&#34;通过OpenSSL(在此上下文中,不会调用deletefree:它们只是设置为特定值)。以下是一次迭代后内存的显示方式:

memory dump #2

此处的最后一个元素设置为ff ff ff 7f,我假设这是OpenSSL确保在以后未分配内存时没有密钥信息泄露的方法。

在循环之后(以及在调用OPENSSL_free()之前),内存如下:

memory dump #3

所有元素都设置为ff ff ff 7fasn1_cbNULL,因此不会进行任何调用。接下来的事情是调用OPENSSL_free(*pval)

free()号调用似乎是有效的&amp;分配的内存失败并导致执行中止并显示一条消息:&#34; HEAP CORRUPTION DETECTED&#34;

对此感到好奇,我迷上了mallocreallocfree(正如OpenSSL允许的那样),以确保这不是双重免费或永不分配的免费记忆。事实证明,0x00000053379575E0处的内存实际上是一个12字节的块,它确实被分配了(之前从未释放过)。

我很想知道这里发生了什么:从我的研究来看,似乎free()在通常由malloc()返回的指针上失败。除此之外,这个存储器位置在没有任何问题之前被写入几条指令,这证实了存储器被正确分配的假设。

我知道如果不是不可能的话,在没有所有信息的情况下进行远程调试很困难,但我不知道我的下一步应该是什么。

所以我的问题是:这究竟是什么&#34; HEAP CORRUPTION&#34;由Visual Studio的调试器检测?来自free()的呼叫的所有可能原因是什么?

2 个答案:

答案 0 :(得分:6)

一般来说,可能性包括:

  1. 重复免费。
  2. 之前重复免费。
  3. (最有可能)您的代码在开始之前或结束之后写入超出分配的内存块的限制。 malloc()和朋友在这里添加了额外的簿记信息,例如大小,可能还有一个完整性检查,你将通过覆盖而失败。
  4. 释放未经malloc()编辑的内容。
  5. 继续写入已经free() - d。
  6. 的块

答案 1 :(得分:2)

我终于可以找到问题并解决它。

原来有些指令是在分配的堆缓冲区之外写入字节(因此0x00000000而不是预期的0xfdfdfdfd。)

在调试模式下,在使用free()释放内存或使用realloc()重新分配内存之前,内存保护的覆盖仍然未被检测到。这就是我遇到的 HEAP CORRUPTION 消息的原因。

我希望在发布模式下,这可能会产生戏剧性的效果,比如覆盖应用程序中其他地方使用的有效内存块。


为了将来参与面临类似问题的人,以下是我的做法:

OpenSSL提供CRYPTO_set_mem_ex_functions()函数,定义如下:

int CRYPTO_set_mem_ex_functions(void *(*m) (size_t, const char *, int),
                                void *(*r) (void *, size_t, const char *,
                                            int), void (*f) (void *))

此函数允许您在OpenSSL中挂接和替换内存分配/释放功能。好处是添加了const char *, int参数,这些参数基本上由OpenSSL填充,并包含分配的文件名行号

有了这些信息,很容易找到分配内存块的地方。然后,我可以在查看内存检查器等待内存块被破坏时逐步执行代码。

就我而言,发生的事情是:

if (!combine) {
    *pval = OPENSSL_malloc(it->size); // <== The allocation is here.

    if (!*pval) goto memerr;

    memset(*pval, 0, it->size);
    asn1_do_lock(pval, 0, it);
    asn1_enc_init(pval, it);
}
for (i = 0, tt = it->templates; i < it->tcount; tt++, i++) {
    pseqval = asn1_get_field_ptr(pval, tt);

    if (!ASN1_template_new(pseqval, tt))
        goto memerr;
}
在3个序列元素上调用

ASN1_template_new()来初始化它们。

依次结束ASN1_template_new()来电asn1_item_ex_combine_new()来执行此操作:

if (!combine)
    *pval = NULL;

pvalASN1_VALUE**,此指令在Windows x64系统上设置8个字节而不是预期的4个字节,导致列表的最后一个元素的内存损坏。

有关如何在上游解决此问题的完整讨论,请参阅this thread