Я отлаживал крах в течение нескольких дней, что происходит в глубинах OpenSSL (обсуждение с сопровождающими здесь). Я потратил некоторое время на исследование, поэтому постараюсь сделать этот вопрос интересным и информативным.
Во-первых, и чтобы дать некоторый контекст, моя минимальная выборка, которая воспроизводит краш, следующая:
#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
и в Wikis онлайн. Он отлично работает на Linux (valgrind вообще не сообщает об ошибке). Он только сбой в Windows (Visual Studio 2013 - x64). Я убедился, что правильное время работы связано с (/MD
в моем случае, для всех зависимостей).
Не боясь зла, я перекомпилировал OpenSSL в x64-debug (на этот раз, связав все в /MDd
), и /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 < 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
-loop, заключается в том, что он проходит через все элементы последовательности (начиная с последнего элемента) и "удаляет" их (подробнее об этом позже).
Прямо перед сбоем происходит удаление из трех элементов (при *pval
, который равен 0x00000053379575E0
). Глядя на память, можно увидеть следующее:
Последовательность имеет длину 12 байт, каждый элемент имеет длину 4 байта (в данном случае 2
, 5
и 10
). На каждой итерации цикла элементы "удаляются" OpenSSL (в этом контексте не delete
ни 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), чтобы гарантировать, что это не было двойной или свободной на никогда не выделенной памяти. Оказывается, память на 0x00000053379575E0
действительно представляет собой 12-байтовый блок, который действительно был выделен (и никогда не был освобожден раньше).
Я нахожусь в убытке, выясняя, что происходит здесь: из моих исследований кажется, что free()
терпит неудачу в указателе, который обычно возвращался malloc()
. В дополнение к этому, это место памяти записывалось в пару инструкций, прежде чем без каких-либо проблем, подтверждающих гипотезу о том, что память будет правильно распределена.
Я знаю, что трудно, если не невозможно, отлаживать удаленно без всякой информации, но я понятия не имею, какие должны быть мои следующие шаги.
Итак, мой вопрос: как именно это "HEAP CORRUPTION" обнаружено отладчиком Visual Studio? Каковы все возможные причины для этого, исходя из вызова free()
?