什么可以解释对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
)。 看看记忆,可以看到以下事情发生:
序列长度为12个字节,每个元素长度为4个字节(在本例中为5
和10
)。 在每次循环迭代中,元素被OpenSSL“删除”(在此上下文中,不会调用delete
或free
:它们只是设置为特定值)。 以下是一次迭代后内存的显示方式:
这里的最后一个元素被设置为ff ff ff 7f
,我假设这是OpenSSL确保在以后未分配内存时没有密钥信息泄漏的方法。
在循环之后(以及在调用OPENSSL_free()
),内存如下:
所有元素都设置为ff ff ff 7f
, asn1_cb
为NULL
因此不进行调用。 接下来的事情是调用OPENSSL_free(*pval)
。
对free()
调用似乎是一个有效的和已分配的内存失败,并导致执行中止消息: “HEAP CORRUPTION DETECTED” 。
对此感到好奇,我迷上了malloc
, realloc
和free
(如OpenSSL允许),以确保这不是一个双重免费或免费的永不分配的内存。 事实certificate, 0x00000053379575E0
处的内存实际上是一个12字节的块,它确实被分配了(之前从未释放过)。
我无法弄清楚这里发生了什么:从我的研究来看,似乎free()
在一个通常由malloc()
返回的指针上失败。 除此之外,此存储器位置在没有任何问题之前被写入几条指令,这证实了存储器被正确分配的假设。
我知道在没有所有信息的情况下远程调试很难,如果不是不可能的,但我不知道我的下一步应该是什么。
所以我的问题是:Visual Studio的调试器是如何检测到这种“HEAP CORRUPTION”的? 来自对free()
的调用产生的所有可能原因是什么?
一般来说,可能性包括:
- 免费重复。
- 预先重复免费。
- (最有可能)您的代码在开始之前或结束之后写入超出分配的内存块的限制。
malloc()
和朋友在这里放置了额外的簿记信息,例如大小,可能是一个完整性检查,你将通过覆盖失败。 - 释放没有
malloc()
ed的东西。 - 继续写入已经
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;
pval
是ASN1_VALUE**
,此指令在Windows x64系统上设置8个字节而不是预期的4个字节,导致列表的最后一个元素的内存损坏。
有关如何在上游解决此问题的完整讨论,请参阅此主题 。