CVE et probabilité carrée

Il y a environ un an, en juillet 2019, nous avons commencé à recevoir d'étranges rapports de bogues sur le noyau RHEL7 dans OpenVz. À première vue, les bugs étaient différents: les nœuds se sont plantés à différents endroits et même dans différents sous-systèmes, mais à chaque fois l'enquête a trouvé l'un ou l'autre objet "tordu". Les objets étaient différents, parfois une sorte de poubelle y était trouvée, parfois une référence à la mémoire libérée, parfois l'objet lui-même était libéré, mais dans tous les cas la mémoire de cet objet était allouée à partir du cache kmalloc-192. Sous la coupe - une histoire détaillée sur cette histoire.



image



Le dépannage habituel dans de tels cas consiste à examiner attentivement le cycle de vie de l'objet affecté: voir comment la mémoire lui est allouée, comment elle est libérée, comment correctement les compteurs de référence sont pris et libérés, en accordant une attention particulière aux chemins d'erreur. Cependant, dans notre cas, différents objets ont été taillés et la vérification de leur cycle de vie n'a pas trouvé de bogues.



Le cache kmalloc-192 est assez populaire dans le noyau, il combine plusieurs dizaines d'objets différents. Un bogue dans le cycle de vie de l'un d'eux est la raison la plus probable de ce type de bogues. Le simple fait de lister tous ces objets est assez problématique, et il n'est pas question de tous les vérifier. Les rapports de bogues ont continué d'arriver, mais nous n'avons pas réussi à trouver leur cause par une enquête directe. Un indice était nécessaire.



De notre côté, ces bogues ont été étudiés par Andrey Ryabinin, un spécialiste de la gestion de la mémoire, largement connu dans les cercles étroits des développeurs de noyau en tant que développeur de KASAN - une technologie impressionnante pour détecter les erreurs d'accès à la mémoire. En fait, c'est KASAN qui était le mieux adapté pour découvrir les causes de notre bug. KASAN n'était pas inclus dans le noyau RHEL7 d'origine, mais Andrey nous a porté les correctifs nécessaires dans OpenVz. Nous n'avons pas inclus KASAN dans la version de production de notre noyau, mais il est présent dans la version de débogage du noyau et aide activement notre QA à trouver les bogues.















En plus de KASAN, le noyau de débogage inclut de nombreuses autres fonctionnalités de débogage que nous avons héritées de Red Hat. À la suite du débogage, le noyau s'est avéré plutôt lent. Le contrôle qualité dit que les mêmes tests sur un noyau de débogage prennent 4 fois plus de temps. Pour nous, ce n'est pas fondamental, nous n'y mesurons pas les performances, mais recherchons des bugs. Cependant, un tel ralentissement était inacceptable pour les clients, et nos demandes de mise en production d'un noyau de débogage étaient invariablement rejetées.



Comme alternative à KASAN, les clients ont été invités à activer slub_debug sur les nœuds affectés... Cette technologie permet également la détection de la corruption de la mémoire. En utilisant une zone rouge et un empoisonnement de la mémoire pour chaque objet, l'allocateur de mémoire vérifie si tout est en ordre à chaque fois qu'il alloue et libère de la mémoire. Si quelque chose ne va pas, il émet un message d'erreur, si possible, corrige les dommages détectés et permet au noyau de continuer à fonctionner. De plus, des informations sont enregistrées sur le dernier qui a alloué et libéré l'objet, de sorte qu'en cas de détection post-factum de corruption de mémoire, il est possible de comprendre «qui» cet objet était dans la «vie passée». Slub_debug peut être activé dans la ligne de commande du noyau sur un noyau de production, mais ces vérifications consomment également de la mémoire et des ressources de processeur. Pour le développement et le débogage QA, c'est bien, mais les clients de production l'utilisent sans beaucoup d'enthousiasme.



Six mois se sont écoulés, la nouvelle année approchait. Les tests locaux sur le noyau de débogage avec KASAN n'ont pas détecté le problème, nous n'avons reçu aucun rapport de bogue des nœuds avec slub_debug activé, nous n'avons rien trouvé dans les matières premières et nous n'avons pas trouvé le problème. Andrey était chargé d'autres tâches, au contraire, j'ai eu une lacune et j'ai été chargé d'analyser le prochain rapport de bogue.



Après avoir analysé le vidage sur incident, j'ai rapidement découvert l'objet problématique kmalloc-192: sa mémoire était remplie d'une sorte de poubelle, d'informations appartenant à un autre type d'objet. C'était très similaire aux conséquences de l'utilisation après libre, mais après avoir soigneusement examiné le cycle de vie de l'objet endommagé dans les matières premières, je n'ai rien trouvé de suspect non plus.



J'ai parcouru les anciens rapports de bogues, essayé de trouver des indices, mais aussi en vain.



Finalement, je suis retourné à mon bogue et j'ai commencé à regarder l'objet précédent. Il s'est également avéré être en cours d'utilisation, mais de son contenu, il était totalement incompréhensible de ce qu'il était - il n'y avait pas de constantes, de références à des fonctions ou à d'autres objets. Après avoir retracé plusieurs générations de références à cet objet, j'ai finalement compris qu'il s'agissait d'un bitmap plus petit. Cet objet faisait partie de la technique d'optimisation pour libérer la mémoire du conteneur. La technologie a été développée à l'origine pour nos noyaux, plus tard son auteur Kirill Tkhai l'a engagée dans la ligne principale de Linux.



"Les résultats montrent que les performances augmentent au moins 548 fois."



Plusieurs milliers de ces correctifs complètent le noyau RHEL7 d'origine stable au rock, rendant le noyau Virtuozzo aussi pratique que possible pour les hébergeurs. Dans la mesure du possible, nous essayons d'envoyer nos développements à la ligne principale, car cela facilite le maintien du code en bon état.



Après avoir suivi les liens, j'ai trouvé une structure décrivant mon bitmap. Le descripteur pensait que la taille du bitmap devait être de 240 octets, et cela ne pouvait en aucun cas être vrai, car en fait l'objet était alloué à partir du cache kmalloc-192.



Bingo!



Il s'est avéré que les fonctions fonctionnant avec la mémoire bitmap accédaient au-delà de sa limite supérieure et pouvaient changer le contenu de l'objet suivant. Dans mon cas, il y avait un refcount au début de l'objet, et lorsque le bitmap l'a annulé, le put suivant a conduit à la libération soudaine de l'objet. Plus tard, de la mémoire a été à nouveau allouée pour un nouvel objet, dont l'initialisation était perçue comme une poubelle par le code de l'ancien objet, ce qui a inévitablement conduit tôt ou tard au crash du nœud.



image


C'est bien quand on peut consulter l'auteur du code!



En regardant son code avec Kirill, nous avons rapidement trouvé la cause première de l'écart détecté. Au fur et à mesure que le nombre de conteneurs augmentait, le bitmap aurait dû augmenter, mais nous avons omis l'un des cas et, par conséquent, avons parfois ignoré le bitmap de redimensionnement. Dans nos tests locaux, cette situation n'a pas été trouvée, et dans la version du correctif que Kirill a envoyé à la ligne principale, le code a été retravaillé et il n'y avait pas de bogue.



Avec 4 tentatives, Kirill et moi avons travaillé ensemble pour composer un tel patch , pendant un mois nous l'avons exécuté dans des tests locaux et à la fin de février nous avons publié une mise à jour avec un noyau fixe. Nous avons vérifié de manière sélective d'autres vidages sur incident, avons également trouvé le mauvais bitmap dans le quartier, célébré la victoire et annulé les anciens bugs en cachette.



Cependant, les vieilles femmes ne cessaient de tomber et de tomber. Le filet de ces types de rapports de bogues a diminué, mais ne s'est pas complètement asséché.



En général, cela était attendu. Nos clients sont des hébergeurs. Ils n'aiment pas du tout redémarrer leurs nœuds, car reboot == downtime == a perdu de l'argent. Nous n'aimons pas non plus publier fréquemment des noyaux. La publication officielle de la mise à jour est une procédure assez laborieuse qui nécessite d'exécuter un tas de tests différents. Par conséquent, de nouveaux noyaux stables sont publiés environ tous les trimestres.



Pour assurer une livraison rapide des corrections de bogues aux nœuds de production clients, nous utilisons les correctifs en direct ReadyKernel. À mon avis, personne d'autre ne fait cela à part nous. Virtuozzo 7 utilise une stratégie inhabituelle pour utiliser des patchs en direct.



Habituellement, lifepatch n'est que sécurité. Dans notre pays, 3/4 des correctifs sont des corrections de bogues. Corrections de bogues sur lesquels nos clients sont déjà tombés par hasard ou sur lesquels ils pourraient facilement tomber à l'avenir. En effet, de telles choses ne peuvent être faites que pour votre kit de distribution: sans le retour des utilisateurs, il est impossible de comprendre ce qui est important pour eux et ce qui ne l'est pas.



Le patch en direct n'est certainement pas une panacée. Il est généralement impossible de tout patcher d'affilée - la technologie ne le permet pas. La nouvelle fonctionnalité n'est pas non plus ajoutée de cette manière. Cependant, une partie importante des bogues est corrigée avec les correctifs en une ligne les plus simples, qui sont excellents pour la correction de la vie. Dans les cas plus complexes, le correctif original doit être "modifié de manière créative avec un fichier", parfois la machinerie de mise à jour en direct est boguée, mais notre assistant Zhenya Shatokhin connaît parfaitement son travail. Récemment, par exemple, il a déterrébug enchanteur dans kpatch , sur lequel, pour de bonnes raisons , il vaut généralement la peine d'écrire un opéra séparé.



Au fur et à mesure que les corrections de bogues appropriées s'accumulent, généralement une fois toutes les une à deux semaines, Zhenya lance une autre série de correctifs en direct ReadyKernel. Après la sortie, ils volent instantanément vers les nœuds clients et empêchent l'attaque du râteau que nous connaissons déjà. Et tout cela sans redémarrer les nœuds clients. Et publiez fréquemment les noyaux inutilement. Des avantages continus.



Cependant, le patch live arrive souvent trop tard aux clients: le problème qu'il ferme est déjà survenu, mais le nœud ne s'est pas encore écrasé.



C'est pourquoi l'émergence de nouveaux rapports de bogues avec le problème que nous avons déjà résolu n'était pas inattendue pour nous. Les analyser à maintes reprises a révélé des symptômes familiers: un vieux noyau, des déchets dans kmalloc-192, une «mauvaise» bitmap devant lui, et un patch live déchargé ou chargé tardivement avec un correctif.



L'OVZ-7188 de FastVPS , arrivé à la toute fin de février, ressemblait à l'un de ces cas . «Merci beaucoup pour le rapport de bogue. Nos condoléances. Immédiatement très similaire au problème connu. C'est dommage qu'il n'y ait pas de patchs en direct dans OpenVZ. Attendez une version stable du noyau, passez à Virtuozzo ou utilisez des noyaux instables avec un correctif. "



Les rapports de bogues sont l'une des choses les plus précieuses qu'OpenVZ nous offre. Les rechercher nous donne une chance de déceler de graves problèmes avant que l'un des gros clients n'intervienne. Par conséquent, malgré le problème connu, j'ai néanmoins demandé à remplir des vidages sur incident pour nous.



L'analyse du premier d'entre eux m'a quelque peu découragée: le "mauvais" bitmap devant l'objet kmalloc-192 "tordu" n'a pas été trouvé.



Un peu plus tard, le problème a été reproduit sur le nouveau noyau. Et puis un autre, un autre et un autre.



Oups!



Comment? Non corrigé? J'ai revérifié les matières premières - tout va bien, le patch est en place, rien n'est perdu.



Encore une fois la corruption? Au même endroit?



J'ai dû le découvrir à nouveau.



image

(Qu'est-ce que c'est? Voir ici )



Dans chacun des nouveaux vidages sur incident, l'enquête a de nouveau rencontré l'objet kmalloc-192. En général, un tel objet avait l'air tout à fait normal, mais au tout début de l'objet, la mauvaise adresse était à chaque fois trouvée. En suivant la relation de l'objet, j'ai trouvé que deux octets internes étaient annulés dans l'adresse.



in all cases corrupted pointer contains nulls in 2 middle bytes: (mask 0xffffffff0000ffff)
0xffff9e2400003d80
0xffff969b00005b40
0xffff919100007000
0xffff90f30000ccc0


Dans le premier des cas répertoriés, au lieu de la "mauvaise" adresse 0xffff9e2400003d80, la "bonne" adresse 0xffff9e24740a3d80 aurait dû l'être. Une situation similaire a été constatée dans d'autres cas.



Il s'est avéré que du code étranger annulait notre objet avec 2 octets. Le scénario le plus probable est l'utilisation après la libération, lorsqu'un objet, après avoir été libéré, remet à zéro un champ dans ses premiers octets. J'ai vérifié les objets les plus fréquemment utilisés, mais rien de suspect n'a été trouvé. Encore une fois une impasse.



FastVPSà notre demande, j'ai exécuté le noyau de débogage avec KASAN pendant une semaine, mais cela n'a pas aidé, le problème ne s'est jamais reproduit. Nous avons demandé à enregistrer slub_debug, mais cela a nécessité un redémarrage, et le processus a pris beaucoup de temps. En mars-avril, les nœuds se sont plantés plusieurs fois, mais slub_debug a été désactivé, et cela ne nous a pas donné de nouvelles informations.



Et puis il y a eu une accalmie, le problème a cessé de se reproduire. Avril terminé, mai passé - il n'y a pas eu de nouvelles chutes.



L'attente s'est terminée le 7 juin - enfin, un problème a frappé le cœur avec slub_debug activé. En vérifiant la zone rouge lors de la libération de l'objet slub_debug, j'ai trouvé deux octets de zéro au-delà de sa limite supérieure. En d'autres termes, il s'est avéré que ce n'était pas une utilisation après utilisation gratuite, l'objet précédent était à nouveau le coupable. Il y avait une structure d'aspect normal nf_ct_ext. Cette structure fait référence au suivi de connexion, une description de la connexion réseau utilisée par le pare-feu.



Cependant, on ne savait toujours pas pourquoi cela se produisait.



J'ai commencé à regarder à conntrack: dans l'un des conteneurs, quelqu'un a frappé sur le port ouvert 1720 en utilisant ipv6. Par port et protocole, j'ai trouvé le nf_conntrack_helper correspondant.



static struct nf_conntrack_helper nf_conntrack_helper_q931[] __read_mostly = {
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .data_len               = sizeof(struct nf_ct_h323_master),
                .tuple.src.l3num        = AF_INET, <<<<<<<< IPv4
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .tuple.src.l3num        = AF_INET6, <<<<<<<< IPv6
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
};


En comparant les structures, j'ai remarqué que l'assistant ipv6 ne définissait pas .data_len. Je me suis mis à comprendre d'où il venait, j'ai découvert un patch de 2012.



commit 1afc56794e03229fa53cfa3c5012704d226e1dec

Auteur: Pablo Neira Ayuso <pablo@netfilter.org>

Date: Thu Jun 7 12:11:50 2012 +0200



netfilter: nf_ct_helper: implémente des données privées d'aide à longueur variable



Ce patch utilise les nouvelles extensions conntrack à longueur variable.



Au lieu d'utiliser l'union nf_conntrack_help qui contient toutes les

informations de données privées d'assistance, nous allouons une

zone de longueur variable pour stocker les données d'assistance privées.



Ce patch inclut la modification de tous les helpers existants.

Il comprend également quelques en-têtes d'inclusion pour éviter la compilation

mises en garde.



Le correctif a ajouté un nouveau champ .data_len à l'assistant, qui indiquait la quantité de mémoire nécessaire au gestionnaire de connexion réseau correspondant. Le patch était censé définir .data_len pour tous les nf_conntrack_helpers disponibles à ce moment-là, mais il a manqué la structure que j'ai trouvée.



En conséquence, il s'est avéré que la connexion via ipv6 au port ouvert 1720 a lancé la fonction q931_help (), elle a écrit dans une structure pour laquelle personne n'a alloué de mémoire. Un simple scan de port annulait quelques octets, la transmission d'un message de protocole normal remplissait la structure d'informations plus significatives, mais dans tous les cas, la mémoire de quelqu'un d'autre était effilochée et tôt ou tard, cela conduisit au crash du nœud.



Florian Westphal a de nouveau repensé le code en 2017et supprimé .data_len, et le problème que j'ai découvert est passé inaperçu.



Malgré le fait que le bogue ne se trouve plus dans la ligne principale actuelle du noyau Linux, le problème a été hérité par les noyaux d'un tas de distributions Linux, y compris les toujours actuels RHEL7 / CentOS7, SLES 11 et 12, Oracle Unbreakable Enterprise Kernel 3 et 4, Debian 8 et 9 Ubuntu 14.04 et 16.04 LTS.



Le bogue a été reproduit de manière triviale sur le nœud de test, à la fois sur notre cœur et sur le RHEL7 d'origine. Sécurité explicite: corruption de la mémoire gérée à distance. Où le port 1720 ipv6 est ouvert - pratiquement ping de la mort.



Le 9 juin, j'ai créé un patch d'une ligne avec une description vague et je l'ai envoyé à la ligne principale. J'ai envoyé une description détaillée à Red Hat Bugzilla et l'ai écrite séparément à Red Hat Security.



D'autres événements se sont développés sans ma participation.

Le 15 juin, Zhenya Shatokhin a publié le patch live ReadyKernel pour nos anciens noyaux.

https://readykernel.com/patch/Virtuozzo-7/readykernel-patch-131.10-108.0-1.vl7/



Le 18 juin, nous avons publié un nouveau noyau stable dans Virtuozzo et OpenVz.

https://virtuozzosupport.force.com/s/article/VZA-2020-043



Le 24 juin, Red Hat Security a attribué un identifiant CVE au bogue

https://access.redhat.com/security/cve/CVE-2020-14305



Problème a reçu un impact modéré avec un CVSS v3 Score 8.1 inhabituellement élevé et au cours des prochains jours, d'autres distributions

SUSE ont répondu au bogue du chapeau public https://bugzilla.suse.com/show_bug.cgi?id=CVE-2020-14305

Debian https: / /security-tracker.debian.org/tracker/CVE-2020-14305

Ubuntuhttps://people.canonical.com/~ubuntu-security/cve/2020/CVE-2020-14305.html



Le 6 juillet, KernelCare a publié un livepatch pour les distributifs concernés.

https://blog.kernelcare.com/new-kernel-vulnerability-found-by-virtuozzo-live-patched-by-kernelcare



Le 9 juillet, le problème a été résolu dans les noyaux Linux stables 4.9.230 et 4.4.230.

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-4.9.y&id=396ba2fc4f27ef6c44bbc0098bfddf4da76dc4c9 Les



distributions, cependant, n'ont toujours pas fermé le trou ...



«Regarde, Kostya», dis-je à mon partenaire Kostya Khorenko, «notre obus a touché le même cratère deux fois! Moi et un accès au-delà de la fin de l'objet la dernière fois que j'ai rencontré nepoymi, et ici, il nous a rendu visite deux fois de suite. Dites-moi, est-ce comme une probabilité carrée? Ou pas carré?

- La probabilité est carrée, oui. Mais ici, vous devez regarder - quel événement est la probabilité? La probabilité carrée de l'événement que des bogues inhabituels aient été rencontrés exactement 2 fois de suite. C'est dans une rangée.



Eh bien, Kostya est intelligent, il sait mieux.



All Articles