Live patching QEMU for VENOM mitigation

Vincent Bernat

CVE-2015-3456, also known as VENOM, is a se­cu­rity vul­ner­a­bil­ity in QEMU vir­tual floppy con­troller:

The Floppy Disk Con­troller (FDC) in QEMU, as used in Xen […] and KVM, al­lows lo­cal guest users to cause a de­nial of ser­vice (out-of-bounds write and guest crash) or pos­si­bly ex­e­cute ar­bi­trary code via the FD_​CMD_​READ_ID, FD_​CMD_​DRIVE_​SPECIFICATION_​COMMAND, or other un­spec­i­fied commands.

Even when QEMU has been con­fig­ured with no floppy drive, the floppy con­troller code is still ac­tive. The vul­ner­a­bil­ity is easy to test:1

#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;
}

Once the fix is in­stalled, all pro­cesses still have to be restarted for the up­grade to be ef­fec­tive. It is pos­si­ble to min­i­mize the down­time by lever­ag­ing virsh save.

An­other pos­si­bil­ity would be to patch the run­ning pro­cesses. The Linux ker­nel at­tracted a lot of in­ter­est in this area, with so­lu­tions like Ksplice (mostly killed by Or­a­cle), kGraft (by SUSE) and kpatch (by Red Hat) and the in­clu­sion of a com­mon frame­work in the ker­nel. The userspace has far fewer out-of-the-box so­lutions.2

I present here a sim­ple and self-con­tained way to patch a run­ning QEMU to re­move the vul­ner­a­bil­ity with­out re­quir­ing any no­tice­able down­time. Here is a short demon­stration:

Proof of concept#

First, let’s find a work­around that would be sim­ple to im­ple­ment through live patch­ing: while mod­i­fy­ing run­ning code text is pos­si­ble, it is eas­ier to mod­ify a sin­gle variable.

Concept#

Look­ing at the code of the floppy con­troller and the patch, we can avoid the vul­ner­a­bil­ity by not ac­cept­ing any com­mand on the FIFO port. Each re­quest would be an­swered by “In­valid com­mand” (0x80) and a user will not be able to push more bytes to the FIFO un­til the an­swer is read and the FIFO queue re­set. The floppy con­troller would be ren­dered use­less in this state, but who cares?

The list of com­mands ac­cepted by the con­troller on the FIFO port is con­tained in the handlers[] array:

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 */
};

To avoid brows­ing the ar­ray each time a com­mand is re­ceived, an­other ar­ray is used to map each com­mand to the ap­pro­pri­ate handler:

/* 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;
                }
            }
        }
    }
    /* […] */
}

Our work­around is to mod­ify the command_​to_​handler[] ar­ray to map all com­mands to the fdctrl_​unimplemented() han­dler (the last one in the handlers[] array).

Testing with gdb#

To check if the work­around works as ex­pected, we test it with gdb. Un­less you have com­piled QEMU your­self, you need to in­stall a pack­age with de­bug sym­bols. Un­for­tu­nately, on De­bian, they are not avail­able, yet. On Ubuntu, you can in­stall the qemu-system-x86-dbgsym pack­age af­ter en­abling the ap­pro­pri­ate repos­itories.

Up­date (2018-11)

Au­to­matic de­bug pack­ages are avail­able since De­bian Stretch. Like for Ubuntu, a ded­i­cated repos­i­tory needs to be enabled.

The fol­low­ing func­tion for gdb maps ev­ery com­mand to the unim­ple­mented handler:

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

At­tach to the vul­ner­a­ble process (with attach), call the func­tion (with patch) and de­tach from the process (with detach). You can check that the ex­ploit is not work­ing any­more. This could be eas­ily au­tomated.

Limitations#

Us­ing gdb has two main lim­i­tations:

  1. It needs to be in­stalled on each host to be patched.
  2. The de­bug pack­ages need to be in­stalled as well. More­over, it can be dif­fi­cult to fetch pre­vi­ous ver­sions of these packages.

Writing a custom patcher#

To over­come these lim­i­ta­tions, we can write a cus­tom patcher us­ing the ptrace() sys­tem call with­out re­ly­ing on de­bug sym­bols be­ing present.

Finding the right memory spot#

Be­fore be­ing able to mod­ify the command_​to_​handler[] ar­ray, we need to know its lo­ca­tion. The first clue is given by the sym­bol ta­ble. To query it, use 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

This ta­ble is usu­ally stripped out of the ex­e­cutable to save space, as shown below:

$ 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

If your dis­tri­bu­tion pro­vides a de­bug pack­age, the de­bug sym­bols are in­stalled in /usr/​lib/​debug. Most mod­ern dis­tri­bu­tions are now re­ly­ing on the build ID3 to map an ex­e­cutable to its de­bug­ging sym­bols, like the ex­am­ple above. With­out a de­bug pack­age, you need to re­com­pile the ex­ist­ing pack­age with­out strip­ping de­bug sym­bols in a clean en­vi­ron­ment.4 On De­bian, this can be done by set­ting the DEB_​BUILD_​OPTIONS en­vi­ron­ment vari­able to nostrip.

We have now two pos­si­ble cases:

  • the easy one; and
  • the hard one.

The easy case#

On x86, here is the stan­dard lay­out of a reg­u­lar Linux process in memory:5

Memory layout of a regular process on x86
Memory layout of a regular process on Linux x86 with ASLR.

The ran­dom gaps (ASLR) are here to pre­vent an at­tacker from re­li­ably jump­ing to a par­tic­u­lar ex­ploited func­tion in mem­ory. On x86-64, the lay­out is quite sim­i­lar. The im­por­tant point is that the base ad­dress of the ex­e­cutable is fixed.

The mem­ory map­ping of a process is also avail­able through /proc/​PID/​maps. Here is a short­ened and an­no­tated ex­am­ple on 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]

With a reg­u­lar ex­e­cutable, the value given in the sym­bol ta­ble is an ab­so­lute mem­ory address:

$ 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

So, the ad­dress of command_​to_​handler[], in the above ex­am­ple, is 0x602080.

The hard case#

To en­hance se­cu­rity, it is pos­si­ble to load some ex­e­cuta­bles at a ran­dom base ad­dress, just like a li­brary. Such an ex­e­cutable is called a Po­si­tion In­de­pen­dent Ex­e­cutable (PIE). An at­tacker won’t be able to rely on a fixed ad­dress to find some help­ful func­tion. Here is the new mem­ory layout:

Memory layout of a PIE process on x86
Memory layout of a PIE process on Linux x86 with ASLR.

With a PIE process, the value in the sym­bol ta­ble is now an off­set from the base address.

$ 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

If we look at /proc/​PID/​maps, we can fig­ure out where the ar­ray is lo­cated in memory:

$ 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]

The base ad­dress is 0x7f6c13b31000, the off­set is 0x202080 and there­fore, the lo­ca­tion of the ar­ray is 0x7f6c13d33080. We can check with gdb:

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

Patching a memory spot#

Once we know the lo­ca­tion of the command_​to_​handler[] ar­ray in mem­ory, patch­ing it is quite straight­for­ward. First, we start trac­ing the tar­get process:

/* 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;
}

Then, we re­trieve the command_​to_​handler[] ar­ray, mod­ify it, and put it back in memory.6

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;
}

Since ptrace() only al­lows one to read or write a word at a time, ptrace_​read() and ptrace_​write() are wrap­pers to read or write ar­bi­trary large chunks of mem­ory.7 Here is the code for 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;
}

Putting the pieces together#

The patcher is pro­vided with the fol­low­ing in­formation:

  • the PID of the process to be patched;
  • the command_​to_​handler[] off­set from the sym­bol table; and
  • the build ID of the ex­e­cutable file used to get this off­set (as a safety measure).

The main steps are:

  1. At­tach to the process with ptrace().
  2. Get the ex­e­cutable name from /proc/​PID/exe.
  3. Parse /proc/​PID/​maps to find the ad­dress of the text seg­ment (it’s the first one).
  4. Do some con­sis­tency checks:
    • check there is an ELF header at this lo­ca­tion (4-byte magic number),
    • check the ex­e­cutable type (ET_​EXEC for reg­u­lar ex­e­cuta­bles, ET_​DYN for PIE), and
    • get the build ID and com­pare with the ex­pected one.
  5. From the base ad­dress and the pro­vided off­set, com­pute the lo­ca­tion of the command_​to_​handler[] array.
  6. Patch it.

You can find the com­plete patcher on 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!