Je suis en train de déboguer un crash depuis plusieurs jours, qui se produit dans les profondeurs d’OpenSSL (discussion avec les responsables ici ). J’ai pris un peu de temps pour enquêter, alors je vais essayer de rendre cette question intéressante et informative.
Tout d’abord et pour donner un peu de contexte, mon échantillon minimal reproduisant le crash est le suivant:
#include #include #include #include #include #include int main() { ERR_load_crypto_ssortingngs(); 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. }
Fondamentalement, ce code génère une clé à courbe elliptique et tente de la sortir sur stdout
. Un code similaire peut être trouvé dans openssl.exe ecparam
et sur les wikis en ligne. Cela fonctionne très bien sous Linux (valgrind ne rapporte aucune erreur). Il se bloque uniquement sur Windows (Visual Studio 2013 – x64). Je me suis assuré que les temps d’exécution appropriés étaient liés à ( /MD
dans mon cas, pour toutes les dépendances).
Ne craignant aucun mal, j’ai recompilé OpenSSL dans x64-debug (cette fois, tout en liant tout dans /MDd
), puis /MDd
parcouru le code pour trouver le jeu d’instructions incriminé. Ma recherche m’a conduit à ce code (dans le fichier tasn_fre.c
d’OpenSSL):
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... }
Pour ceux qui ne sont pas trop familiers avec OpenSSL et ses routines ASN.1, en gros, ceci for
-loop fait en sorte qu’il passe par tous les éléments d’une séquence (en commençant par le dernier élément) et les “supprime” (plus à ce sujet plus tard) .
Juste avant que le crash ne se produise, une séquence de 3 éléments est supprimée (à *pval
, qui est 0x00000053379575E0
). En regardant la mémoire, on peut voir les choses suivantes se produire:
La séquence est longue de 12 octets, chaque élément étant long de 4 octets (dans ce cas, 2
, 5
et 10
). A chaque itération de boucle, les éléments sont “supprimés” par OpenSSL (dans ce contexte, ni delete
ni free
sont appelés: ils sont simplement définis avec une valeur spécifique). Voici à quoi ressemble la mémoire après une itération:
Le dernier élément ici a été défini sur ff ff ff 7f
ce qui, je suppose, est le moyen utilisé par OpenSSL pour garantir qu’aucune information clé ne fuit lorsque la mémoire est désallouée ultérieurement.
Juste après la boucle (et avant l’appel de OPENSSL_free()
), la mémoire est la suivante:
Tous les éléments ont été réglés sur ff ff ff 7f
, asn1_cb
étant NULL
, aucun appel n’est effectué. La prochaine chose qui se passe est l’appel à OPENSSL_free(*pval)
.
Cet appel à free()
sur ce qui semble être une mémoire valide et allouée échoue et provoque l’interruption de l’exécution avec le message “HEAP CORRUPTION DETECTED” .
Curieux à ce sujet, je me suis accroché à malloc
, realloc
et free
(comme le permet OpenSSL) pour s’assurer que ce n’était pas une mémoire double libre ou libre sur jamais allouée. Il s’avère que la mémoire à 0x00000053379575E0
est en réalité un bloc de 12 octets qui a été effectivement alloué (et n’a jamais été libéré auparavant).
Je ne sais pas trop ce qui se passe ici: d’après mes recherches, il semble que free()
échoue sur un pointeur qui était normalement renvoyé par malloc()
. En plus de cela, cet emplacement de mémoire était en train d’être écrit dans quelques instructions avant sans aucun problème qui confirme l’hypothèse que la mémoire soit allouée correctement.
Je sais qu’il est difficile, voire impossible, de déboguer à distance sans toutes les informations, mais je n’ai aucune idée de ce que devraient être mes prochaines étapes.
Ma question est donc la suivante: comment exactement “HEAP CORRUPTION” est-il détecté par le débogueur de Visual Studio? Quelles en sont toutes les causes possibles lorsqu’il provient d’un appel à free()
?
En général, les possibilités comprennent:
malloc()
et ses amis y ont ajouté des informations de tenue de livres supplémentaires, telles que la taille et probablement une vérification de leur santé mentale, que vous échouerez en écrasant. malloc()
. free()
-d. Je pouvais enfin trouver le problème et le résoudre.
Une instruction s’est avérée être en train d’écrire des octets au-delà du tampon de tas alloué (d’où le 0x00000000
au lieu du 0xfdfdfdfd
attendu).
En mode débogage, cet écrasement des gardes de la mémoire rest non détecté jusqu’à ce que la mémoire soit libérée avec free()
ou réaffectée avec realloc()
. C’est ce qui a provoqué le message HEAP CORRUPTION auquel j’ai fait face.
Je pense qu’en mode de publication, cela aurait pu avoir des effets dramatiques, comme la réécriture d’un bloc de mémoire valide utilisé ailleurs dans l’application.
Pour référence future aux personnes confrontées à des problèmes similaires, voici ce que j’ai fait:
OpenSSL fournit une fonction CRYPTO_set_mem_ex_functions()
, définie comme suit:
int CRYPTO_set_mem_ex_functions(void *(*m) (size_t, const char *, int), void *(*r) (void *, size_t, const char *, int), void (*f) (void *))
Cette fonction vous permet d’accrocher et de remplacer des fonctions d’allocation / libération de mémoire dans OpenSSL. La bonne chose est l’ajout des parameters const char *, int
qui sont essentiellement renseignés pour vous par OpenSSL et contiennent le nom du fichier et le numéro de ligne de l’atsortingbution.
Armé de cette information, il était facile de trouver l’endroit où le bloc de mémoire était alloué. Je pouvais alors parcourir le code tout en regardant l’inspecteur de mémoire en attente de la corruption du bloc de mémoire.
Dans mon cas, ce qui s’est passé était:
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; }
ASN1_template_new()
est appelée sur les 3 éléments de séquence pour les initialiser.
ASN1_template_new()
appels asn1_item_ex_combine_new()
qui effectue ceci:
if (!combine) *pval = NULL;
pval
étant un ASN1_VALUE**
, cette instruction définit 8 octets sur les systèmes Windows x64 au lieu des 4 octets prévus, ce qui entraîne une corruption de la mémoire pour le dernier élément de la liste.
Pour une discussion complète sur la façon dont ce problème a été résolu en amont, voir ce fil .