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

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

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

#include  #include  #include  #include  #include  #include  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和Wikis online上找到。 它在Linux上运行正常(valgrind报告根本没有错误)。 它只在Windows上崩溃(Visual Studio 2013 – x64)。 我确保正确的运行时是链接到( /MD在我的情况下,对于所有依赖项)。

/MDd恶,我在x64-debug中重新编译了OpenSSL(这次链接/MDd所有内容),并逐步完成代码以查找有问题的指令集。 我的搜索引导我到这个代码(在OpenSSL的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 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 -loop的作用是它通过序列的所有元素(从最后一个元素开始)并“删除”它们(稍后更多) 。

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

内存转储#1

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

内存转储#2

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

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

内存转储#3

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

free()调用似乎是一个有效的和已分配的内存失败,并导致执行中止消息: “HEAP CORRUPTION DETECTED”

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

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

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

所以我的问题是:Visual Studio的调试器是如何检测到这种“HEAP CORRUPTION”的? 来自对free()的调用产生的所有可能原因是什么?

一般来说,可能性包括:

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

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

原来,一些指令是在分配的堆缓冲区之后写入字节(因此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中挂接和替换内存分配/释放function。 好处是添加了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个字节,导致列表的最后一个元素的内存损坏。

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