Correction sans redémarrage de la faille VENOM de QEMU

Vincent Bernat

La faille CVE-2015-3456, aussi connue sous le nom VENOM, ex­ploite une fai­blesse dans l’im­plé­men­ta­tion du contrô­leur de dis­quettes de QEMU:

Le contrô­leur de dis­quettes (FDC) de QEMU, tel qu’uti­lisé dans Xen […] et dans KVM, per­met aux sys­tèmes in­vi­tés de pro­vo­quer un déni de ser­vice (écri­ture hors li­mite sui­vie du crash du pro­ces­sus in­vité) ou éven­tuel­le­ment d’exé­cu­ter du code ar­bi­traire à tra­vers les com­mandes FD_CMD_READ_ID, FD_​CMD_​DRIVE_​SPECIFICATION_​COMMAND ou d’autres com­mandes non spé­cifiées.

Même lorsque QEMU a été confi­guré pour ne pas ex­po­ser de lec­teur de dis­quettes, le contrô­leur est tou­jours ac­tif. La vul­né­ra­bi­lité est fa­cile à tester1 :

#define FDC_IOPORT 0x3f5
#define FD_CMD_READ_ID 0x0a

int main() {
    ioperm(FDC_IOPORT, 1, 1);
    outb(FD_CMD_READ_ID, FDC_IOPORT);
    for (size_t i = 0;; i++)
        outb(0x42, FDC_IOPORT);
    return 0;
}

Une fois le cor­rec­tif ins­tallé, tous les pro­ces­sus doivent être re­dé­mar­rés pour que la mise à jour prenne ef­fet. Il est pos­sible de mi­ni­mi­ser le temps de cou­pure en uti­li­sant virsh save.

Une al­ter­na­tive se­rait de mo­di­fier le pro­ces­sus en cours d’exé­cu­tion. Le noyau Li­nux a sus­cité beau­coup d’in­té­rêt dans ce do­maine avec des so­lu­tions telles que Ks­plice, kGraft et kpatch, ainsi que par l’in­clu­sion d’une struc­ture com­mune dans le noyau. L’es­pace uti­li­sa­teur ne dis­pose ce­pen­dant pas de so­lu­tions aussi éla­borées2.

Je pré­sente ici une so­lu­tion simple et sans dé­pen­dance pour cor­ri­ger une ins­tance de QEMU en cours d’exé­cu­tion. Voici une courte dé­mons­tration :

Prototype#

Es­sayons d’abord de trou­ver une mo­di­fi­ca­tion simple à im­plé­men­ter : bien qu’il soit pos­sible de mo­di­fier du code en cours d’exé­cu­tion, il est bien plus simple de mo­di­fier une variable.

Concept#

En exa­mi­nant le code du contrô­leur de dis­quettes et le cor­rec­tif, une fa­çon d’évi­ter la vul­né­ra­bi­lité est de n’ac­cep­ter au­cune com­mande sur le port FIFO. Chaque re­quête aura comme ré­ponse « In­va­lid com­mand » (0x80). L’uti­li­sa­teur ne pourra plus pous­ser au­cun oc­tet avant de lire la ré­ponse, ce qui pro­vo­quera une re­mise à zéro de la queue FIFO. Bien sûr, le contrô­leur de dis­quette de­vien­dra alors in­opérant.

La liste des com­mandes ac­cep­tées par le contrô­leur sur le port FIFO se trouve dans le ta­bleau handlers[] :

static const struct {
    uint8_t value;
    uint8_t mask;
    const char* name;
    int parameters;
    void (*handler)(FDCtrl *fdctrl, int direction);
    int direction;
} handlers[] = {
    { FD_CMD_READ, 0x1f, "READ", 8, fdctrl_start_transfer, FD_DIR_READ },
    { FD_CMD_WRITE, 0x3f, "WRITE", 8, fdctrl_start_transfer, FD_DIR_WRITE },
    /* […] */
    { 0, 0, "unknown", 0, fdctrl_unimplemented }, /* default handler */
};

Pour évi­ter de par­cou­rir ce ta­bleau pour chaque com­mande re­çue, un autre ta­bleau as­so­cie une com­mande à la fonc­tion adéquate :

/* Associate command to an index in the 'handlers' array */
static uint8_t command_to_handler[256];

static void fdctrl_realize_common(FDCtrl *fdctrl, Error **errp)
{
    int i, j;
    static int command_tables_inited = 0;

    /* Fill 'command_to_handler' lookup table */
    if (!command_tables_inited) {
        command_tables_inited = 1;
        for (i = ARRAY_SIZE(handlers) - 1; i >= 0; i--) {
            for (j = 0; j < sizeof(command_to_handler); j++) {
                if ((j & handlers[i].mask) == handlers[i].value) {
                    command_to_handler[j] = i;
                }
            }
        }
    }
    /* […] */
}

Notre mo­di­fi­ca­tion consiste à chan­ger le ta­bleau command_to_handler[] pour as­so­cier toutes les com­mandes à la fonc­tion fdctrl_unimplemented() (celle en der­nière po­si­tion dans le ta­bleau handlers[]).

Test avec gdb#

Pour vé­ri­fier que cette mo­di­fi­ca­tion fonc­tionne cor­rec­te­ment, nous la tes­tons avec gdb. À moins d’avoir com­pilé QEMU ma­nuel­le­ment, il est né­ces­saire d’ins­tal­ler le pa­quet conte­nant les sym­boles de dé­bo­gage. Mal­heu­reu­se­ment, chez De­bian, ils ne sont pas en­core dis­po­nibles. Chez Ubuntu, il suf­fit d’ins­tal­ler le pa­quet qemu-system-x86-dbgsym après avoir ac­tivé les dé­pôts ap­propriés.

Mise à jour (11.2018)

Les pa­quets conte­nant les sym­boles de dé­bo­gage sont pro­duits au­to­ma­ti­que­ment de­puis De­bian Stretch. Comme pour Ubuntu, un dé­pôt dé­dié est né­cessaire.

La fonc­tion sui­vante pour gdb im­plé­mente le correctif :

define patch
  set $handler = sizeof(handlers)/sizeof(*handlers)-1
  set $i = 0
  while ($i < 256)
   set variable command_to_handler[$i++] = $handler
  end
  printf "Done!\n"
end

Il suf­fit alors de s’at­ta­cher au pro­ces­sus vul­né­rable (avec attach), d’ap­pe­ler cette fonc­tion (avec patch) et de se dé­ta­cher (avec detach). Cette pro­cé­dure est simple à au­to­matiser.

Limitations#

L’usage de gdb com­porte prin­ci­pa­le­ment deux li­mi­tations :

  1. gdb doit être ins­tallé sur toutes les ma­chines à corriger.
  2. Les pa­quets de dé­bo­gage doivent éga­le­ment être pré­sents. Il est de plus dif­fi­cile de ré­cu­pé­rer d’an­ciennes ver­sions de ceux-ci.

Industrialisation#

Pour contour­ner ces li­mi­ta­tions, nous al­lons écrire un pro­gramme uti­li­sant l’ap­pel sys­tème ptrace() et qui ne né­ces­site pas les sym­boles de dé­bo­gage pour fonc­tionner.

Trouver l’emplacement mémoire#

La pre­mière étape est de lo­ca­li­ser le ta­bleau command_to_handler[] en mé­moire. Le pre­mier in­dice se trouve dans la table des sym­boles que l’on peut in­ter­ro­ger avec readelf -s :

$ readelf -s /usr/lib/debug/.build-id/09/95121eb46e2a4c13747ac2bad982829365c694.debug | \
>   sed -n -e 1,3p -e /command_to_handler/p

Symbol table '.symtab' contains 27066 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
  8485: 00000000009f9d00   256 OBJECT  LOCAL  DEFAULT   26 command_to_handler

Ha­bi­tuel­le­ment, cette table a été re­ti­rée pour éco­no­mi­ser de l’es­pace disque, comme on peut le voir ci-dessous :

$ file -b /usr/bin/qemu-system-x86_64 | tr , \\n
ELF 64-bit LSB shared object
 x86-64
 version 1 (SYSV)
 dynamically linked
 interpreter /lib64/ld-linux-x86-64.so.2
 for GNU/Linux 2.6.32
 BuildID[sha1]=0995121eb46e2a4c13747ac2bad982829365c694
 stripped

Si votre dis­tri­bu­tion four­nit un pa­quet de dé­bo­gage, les sym­boles sont alors ins­tal­lés dans le ré­per­toire /usr/lib/debug. La plu­part des dis­tri­bu­tions mo­dernes uti­lisent dé­sor­mais le build ID4 pour lier un exé­cu­table à ses sym­boles de dé­bo­gage, comme c’est le cas dans l’exemple ci-des­sus. Sans pa­quet de dé­bo­gage, il est né­ces­saire de re­com­pi­ler le pa­quet dans un en­vi­ron­ne­ment mi­ni­mal5 sans sup­pri­mer les sym­boles. Sur De­bian, cela peut se faire en af­fec­tant nostrip à la va­riable d’en­vi­ron­ne­ment DEB_BUILD_OPTIONS.

Il y a en­suite deux cas possibles :

  • le cas facile ;
  • le cas difficile.

Le cas facile#

Sur x86, la mé­moire d’un pro­ces­sus Li­nux nor­mal est or­ga­ni­sée comme ceci6 :

Organisation mémoire d'un processus normal sur x86
Organisation mémoire d’un processus normal sur x86 avec ASLR

L’es­pace aléa­toire in­tro­duit entre les dif­fé­rentes zones (ASLR) per­mettent de rendre la tâche d’un at­ta­quant plus dif­fi­cile quand il veut ré­fé­ren­cer une fonc­tion par­ti­cu­lière. Sur x86-64, l’or­ga­ni­sa­tion est si­mi­laire. Le point im­por­tant est que l’adresse de base de l’exé­cu­table est fixe.

L’or­ga­ni­sa­tion mé­moire d’un pro­ces­sus peut être consul­tée à tra­vers le fi­chier /proc/PID/maps. Voici une ver­sion rac­cour­cie et an­no­tée sur x86-64 :

$ cat /proc/3609/maps
00400000-00401000         r-xp 00000000 fd:04 483  not-qemu [text segment]
00601000-00602000         r--p 00001000 fd:04 483  not-qemu [data segment]
00602000-00603000         rw-p 00002000 fd:04 483  not-qemu [BSS segment]
[random gap]
02419000-0293d000         rw-p 00000000 00:00 0    [heap]
[random gap]
7f0835543000-7f08356e2000 r-xp 00000000 fd:01 9319 /lib/x86_64-linux-gnu/libc-2.19.so
7f08356e2000-7f08358e2000 ---p 0019f000 fd:01 9319 /lib/x86_64-linux-gnu/libc-2.19.so
7f08358e2000-7f08358e6000 r--p 0019f000 fd:01 9319 /lib/x86_64-linux-gnu/libc-2.19.so
7f08358e6000-7f08358e8000 rw-p 001a3000 fd:01 9319 /lib/x86_64-linux-gnu/libc-2.19.so
7f08358e8000-7f08358ec000 rw-p 00000000 00:00 0
7f08358ec000-7f083590c000 r-xp 00000000 fd:01 5138 /lib/x86_64-linux-gnu/ld-2.19.so
7f0835aca000-7f0835acd000 rw-p 00000000 00:00 0
7f0835b08000-7f0835b0c000 rw-p 00000000 00:00 0
7f0835b0c000-7f0835b0d000 r--p 00020000 fd:01 5138 /lib/x86_64-linux-gnu/ld-2.19.so
7f0835b0d000-7f0835b0e000 rw-p 00021000 fd:01 5138 /lib/x86_64-linux-gnu/ld-2.19.so
7f0835b0e000-7f0835b0f000 rw-p 00000000 00:00 0
[random gap]
7ffdb0f85000-7ffdb0fa6000 rw-p 00000000 00:00 0    [stack]

Dans le cas d’un exé­cu­table nor­mal, le nombre fourni dans la table des sym­boles est une adresse absolue :

$ readelf -s not-qemu | \
>   sed -n -e 1,3p -e /command_to_handler/p

Symbol table '.dynsym' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    47: 0000000000602080   256 OBJECT  LOCAL  DEFAULT   25 command_to_handler

Ainsi, dans l’exemple ci-des­sus, l’adresse du ta­bleau command_to_​handler[], est sim­ple­ment 0x602080.

Le cas difficile#

Pour amé­lio­rer la sé­cu­rité, il est pos­sible de pla­cer cer­tains exé­cu­tables à un em­pla­ce­ment aléa­toire en mé­moire, comme c’est le cas pour une bi­blio­thèque. Un tel exé­cu­table est ap­pelé un Po­si­tion In­de­pendent Exe­cu­table (PIE). Un at­ta­quant ne pourra pas se ba­ser sur une adresse fixe pour re­bon­dir sur une fonc­tion par­ti­cu­lière. Voici à quoi res­semble l’or­ga­ni­sa­tion mé­moire d’un pro­ces­sus dans ce cas :

Organisation mémoire d'un processus PIE sur x86
Organisation mémoire d’un processus PIE sur x86 avec ASLR.

Dans le cas d’un pro­ces­sus PIE, le nombre in­di­qué dans la table des sym­boles est re­la­tif à l’adresse de base du processus.

$ readelf -s not-qemu-pie | sed -n -e 1,3p -e /command_to_handler/p

Symbol table '.dynsym' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    47: 0000000000202080   256 OBJECT  LOCAL  DEFAULT   25 command_to_handler

En re­gar­dant le contenu de /proc/PID/maps, il est pos­sible de cal­cu­ler l’em­pla­ce­ment mé­moire du tableau :

$ cat /proc/12593/maps
7f6c13565000-7f6c13704000 r-xp 00000000 fd:01 9319  /lib/x86_64-linux-gnu/libc-2.19.so
7f6c13704000-7f6c13904000 ---p 0019f000 fd:01 9319  /lib/x86_64-linux-gnu/libc-2.19.so
7f6c13904000-7f6c13908000 r--p 0019f000 fd:01 9319  /lib/x86_64-linux-gnu/libc-2.19.so
7f6c13908000-7f6c1390a000 rw-p 001a3000 fd:01 9319  /lib/x86_64-linux-gnu/libc-2.19.so
7f6c1390a000-7f6c1390e000 rw-p 00000000 00:00 0
7f6c1390e000-7f6c1392e000 r-xp 00000000 fd:01 5138  /lib/x86_64-linux-gnu/ld-2.19.so
7f6c13b2e000-7f6c13b2f000 r--p 00020000 fd:01 5138  /lib/x86_64-linux-gnu/ld-2.19.so
7f6c13b2f000-7f6c13b30000 rw-p 00021000 fd:01 5138  /lib/x86_64-linux-gnu/ld-2.19.so
7f6c13b30000-7f6c13b31000 rw-p 00000000 00:00 0
7f6c13b31000-7f6c13b33000 r-xp 00000000 fd:04 4594  not-qemu-pie [text segment]
7f6c13cf0000-7f6c13cf3000 rw-p 00000000 00:00 0
7f6c13d2e000-7f6c13d32000 rw-p 00000000 00:00 0
7f6c13d32000-7f6c13d33000 r--p 00001000 fd:04 4594  not-qemu-pie [data segment]
7f6c13d33000-7f6c13d34000 rw-p 00002000 fd:04 4594  not-qemu-pie [BSS segment]
[random gap]
7f6c15c46000-7f6c15c67000 rw-p 00000000 00:00 0     [heap]
[random gap]
7ffe823b0000-7ffe823d1000 rw-p 00000000 00:00 0     [stack]

L’adresse de base est 0x7f6c13b31000, le dé­ca­lage re­la­tif est 0x202080 et donc le ta­bleau se trouve à l’adresse mé­moire 0x7f6c13d33080. Il est pos­sible de vé­ri­fier cette va­leur avec gdb : with gdb:

$ print &command_to_handler
$1 = (uint8_t (*)[256]) 0x7f6c13d33080 <command_to_handler>

Modifier un emplacement mémoire#

Une fois l’em­pla­ce­ment du ta­bleau command_to_handler[] connu, le mo­di­fier est re­la­ti­ve­ment simple. Il convient d’abord de s’at­ta­cher au pro­ces­sus cible :

/* Attach to the running process */
static int
patch_attach(pid_t pid)
{
    int status;

    printf("[.] Attaching to PID %d...\n", pid);
    if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
        fprintf(stderr, "[!] Unable to attach to PID %d: %m\n", pid);
        return -1;
    }

    if (waitpid(pid, &status, 0) == -1) {
        fprintf(stderr, "[!] Error while attaching to PID %d: %m\n", pid);
        return -1;
    }
    assert(WIFSTOPPED(status)); /* Tracee may have died */

    if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &si) == -1) {
        fprintf(stderr, "[!] Unable to read siginfo for PID %d: %m\n", pid);
        return -1;
    }
    assert(si.si_signo == SIGSTOP); /* Other signals may have been received */

    printf("[*] Successfully attached to PID %d\n", pid);
    return 0;
}

En­suite, ré­cu­pé­rons le ta­bleau command_to_handler[], mo­di­fions le et ré­écri­vons le en mémoire7.

static int
patch_doit(pid_t pid, unsigned char *target)
{
    int ret = -1;
    unsigned char *command_to_handler = NULL;
    size_t i;

    /* Get the table */
    printf("[.] Retrieving command_to_handler table...\n");
    command_to_handler = ptrace_read(pid,
                                     target,
                                     QEMU_COMMAND_TO_HANDLER_SIZE);
    if (command_to_handler == NULL) {
        fprintf(stderr, "[!] Unable to read command_to_handler table: %m\n");
        goto out;
    }

    /* Check if the table has already been patched. */
    /* […] */

    /* Patch it */
    printf("[.] Patching QEMU...\n");
    for (i = 0; i < QEMU_COMMAND_TO_HANDLER_SIZE; i++) {
        command_to_handler[i] = QEMU_NOT_IMPLEMENTED_HANDLER;
    }
    if (ptrace_write(pid, target, command_to_handler,
           QEMU_COMMAND_TO_HANDLER_SIZE) == -1) {
        fprintf(stderr, "[!] Unable to patch command_to_handler table: %m\n");
        goto out;
    }
    printf("[*] QEMU successfully patched!\n");
    ret = 0;

out:
    free(command_to_handler);
    return ret;
}

Comme ptrace() ne per­met de lire et d’écrire qu’un mot à la fois, ptrace_read() et ptrace_write() sont des en­ro­bages pour lire et écrire une quan­tité ar­bi­traire de mé­moire. Voici par exemple le code de ptrace_read() :

/* Read memory of the given process */
static void *
ptrace_read(pid_t pid, void *address, size_t size)
{
    /* Allocate the buffer */
    uword_t *buffer = malloc((size/sizeof(uword_t) + 1)*sizeof(uword_t));
    if (!buffer) return NULL;

    /* Read word by word */
    size_t readsz = 0;
    do {
        errno = 0;
        if ((buffer[readsz/sizeof(uword_t)] =
                ptrace(PTRACE_PEEKTEXT, pid,
                       (unsigned char*)address + readsz,
                       0)) && errno) {
            fprintf(stderr, "[!] Unable to peek one word at address %p: %m\n",
                    (unsigned char *)address + readsz);
            free(buffer);
            return NULL;
        }
        readsz += sizeof(uword_t);
    } while (readsz < size);
    return (unsigned char *)buffer;
}

Assembler les morceaux#

Le pro­gramme prend en pa­ramètre :

  • le PID du pro­ces­sus à modifier,
  • le dé­ca­lage issu de la table des sym­boles pour le ta­bleau command_to_handler[],
  • le build ID de l’exé­cu­table uti­lisé pour ob­te­nir ce dé­ca­lage (à des fins de sé­curité).

Les prin­ci­pales étapes sont alors les suivantes :

  1. S’at­ta­cher au pro­ces­sus avec ptrace().
  2. Ob­te­nir le nom de l’exé­cu­table de­puis /proc/PID/exe.
  3. Lire le fi­chier /proc/PID/maps afin de trou­ver l’adresse de base.
  4. Ef­fec­tuer cer­taines vé­ri­fi­ca­tions sup­plé­men­taires:
    • vé­ri­fier qu’il y a bien un en­tête ELF à l’adresse de base (via quatre oc­tets ma­giques),
    • vé­ri­fier le type de l’exé­cu­table (ET_EXEC pour les exé­cu­tables nor­maux, ET_DYN pour les PIE),
    • ré­cu­pé­rer et com­pa­rer le build ID avec ce­lui attendu.
  5. À par­tir de l’adresse de base et du dé­ca­lage fourni, cal­cu­ler l’em­pla­ce­ment du ta­bleau command_to_handler[].
  6. Mo­di­fier le tableau.

Les sources du pro­gramme sont dis­po­nibles sur GitHub.

$ ./patch --build-id 0995121eb46e2a4c13747ac2bad982829365c694 \
>         --offset 9f9d00 \
>         --pid 16833
[.] Attaching to PID 16833...
[*] Successfully attached to PID 16833
[*] Executable name is /usr/bin/qemu-system-x86_64
[*] Base address is 0x7f7eea912000
[*] Both build IDs match
[.] Retrieving command_to_handler table...
[.] Patching QEMU...
[*] QEMU successfully patched!