Some Notes About the Xen XSA-122 Bug

tl;dr;

This is a summary of a vulnerability in Xen I found earlier in 2015, and why it’s not very useful in practice. Basically you can leak small amounts of memory from the hypervisor stack, but due to the way the associated hypercall is compiled, it turns out you can’t reliably leak very useful information.

The XSA-122 bug

On March 16, 2015 Xen released XSA-122 [1] to fix a bug I found while working on a hypervisor exploit I blogged about earlier in the year [2]. NCC Group also released a corresponding advisory [3], with some additional technical details. This bug sneaked out under the shadow of XSA-123 [4], which was much more interesting.

However, I thought it would be worthwhile to some to provide a little more information about this bug and why isn’t that useful, despite potentially leaking you some legitimate memory addresses. I feel like more information about most bugs is almost always a good thing.

There were two other sources that talk briefly about XSA-122, one post from Bromium [5] and an advisory from Qubes [6], and both suggest that it is not all that interesting due to the lack of sensitive information stored on the hypervisor stack. However, this bug is also not that useful for other reasons which I’ll explain soon.

But first, just to recap, the vulnerability exists in the HYPERVISOR_xen_version hypercall, which is exposed to both para-virtualised (PV) and hardware-virtualised (HVM) Xen guests. This hypercall contains a number of sub-operations that will return a variety of information about the hypervisor, including:

  • Compiler version, compile date
  • Command-line arguments
  • Supported features
  • Changeset data
  • Major, minor, and extra version

I wanted to use this hypercall to detect a vulnerable hypervisor from within a malicious guest, and while trying to understand the function, I noticed it contained a few simple bugs related to lack of initialisation. As noted in advisories, the XENVER_compile_info, XENVER_extraversion, and XENVER_changeset sub-operations all fail to properly initialise the structures they return to the guest domain. The following code handles the XENVER_compile_info sub-operation: 

case XENVER_compile_info:
{
struct xen_compile_info info;
safe_strcpy(info.compiler, xen_compiler());
safe_strcpy(info.compile_by, xen_compile_by());
safe_strcpy(info.compile_domain, xen_compile_domain());
safe_strcpy(info.compile_date, xen_compile_date());
if ( copy_to_guest(arg, &info, 1) )
return -EFAULT;
return 0;
}

A xen_compile_info structure has a few strings copied into it and the structure is returned back to the guest via copy_to_guest().

/* arg == xen_compile_info_t. */
#define XENVER_compile_info 2
struct xen_compile_info {
    char compiler[64];
    char compile_by[16];
    char compile_domain[32];
    char compile_date[32];
};
typedef struct xen_compile_info xen_compile_info_t;

As shown, there are four character arrays used. As long as the compiler strings copied into this structure don’t exactly match the associated lengths, some amount of data from the stack location used by the hypervisor to store the structure will be leaked. This could be information partially leaked from all four members.

The other two sub-operations shown below have similar problems:

case XENVER_extraversion:
    {
        xen_extraversion_t extraversion;
        safe_strcpy(extraversion, xen_extra_version());
        if ( copy_to_guest(arg, extraversion, ARRAY_SIZE(extraversion)) )
            return -EFAULT;
        return 0;
    }
[…]
    case XENVER_changeset:
    {
        xen_changeset_info_t chgset;
        safe_strcpy(chgset, xen_changeset());
        if ( copy_to_guest(arg, chgset, ARRAY_SIZE(chgset)) )
            return -EFAULT;
        return 0;
    }

With the associated structures being defined as follows:

/* arg == xen_extraversion_t. */
#define XENVER_extraversion 1
typedef char xen_extraversion_t[16];
[…]
#define XENVER_changeset 4
typedef char xen_changeset_info_t[64];

So in practice, even in the best case scenario, we’ll be leaking at most 144 bytes from the stack.

Triggering

This bug can easily be triggered from both a PV and a HVM guest. In order to issue hypercalls you must have kernel privileges, so it requires an attacker-controlled domain or prior privilege escalation on a compromised domain. This isn’t a huge barrier to entry for exploitation, as an attacker would simply need to either purchase time on some virtualised infrastructure using Xen and then install an OS they completely control, or just compromise and root a victim OS running somewhere as a Xen guest.

All you need to do to trigger the bug is issue one of the hypercalls and dump the response. The hypercalls all have convenient prototypes in the Linux kernel development headers. Triggering the bug just requires the following:

int ret;
struct xen_changeset_info *cs;
cs = kmalloc(sizeof(struct xen_changeset_info), GFP_KERNEL);
if (cs) {
    memset(cs, 0, sizeof(struct xen_changeset_info));
    ret = HYPERVISOR_xen_version(XENVER_changeset, cs);
    if (!ret) {
        printk(KERN_INFO "Changeset leak:\n");
        printk(KERN_INFO "==================\n");
        print_hex_dump(KERN_INFO, "", DUMP_PREFIX_ADDRESS, 16, 1, (char *)cs, sizeof(struct xen_changeset_info), true);
        printk(KERN_INFO "==================\n\n");
    }
    kfree(cs);
}

What can we leak?

There are two things for which you could try to use this type of bug. One is leaking some sort of sensitive information from other guests. This could theoretically be some “secret” data passed using a hypercall, but in practice there aren’t really any secrets being passed around using the hypercalls or stored hypervisor stack. You might, however, be able to use this type of bug as an indicator for when a guest has issued certain hypercalls, which might be useful for a race condition or similar attack.

The other use would be to leak pointer data off the stack, which is a little less esoteric and generally more useful. This type of leak could be used to inform an exploit targeting a separate memory corruption bug. It would also help in the event that the Xen hypervisor starts introducing more mitigations that include address randomisation. As it stands though, Xen uses hardcoded address ranges for everything, so leaking precise addresses isn’t currently as useful as it could be in the future.

At any rate, I spent a little bit of time before the bug went public looking into any call paths that might reliably leak pointers to useful structures. My primary thinking was to issue a hypercall that uses a pointer to a structure or something I’d want to overwrite later, and see if I could then use the memory revelation hypercall to leak this address.

Now we’ll learn why this bug is pretty boring and you can’t even really do that. Almost all hypercalls use minimal stack space and don’t really have deeply nested call chains, nor call into functions with overly large stack frames. What this means is that if we want to leak something interesting from another hypercall, it is going to have to be pretty early on the stack frame in order for the right overlap to occur with whatever we want to leak. While running through different hypercalls trying to influence what was leaked, I noticed that the values leaked never actually changed.

After some digging, the reason became obvious. We have to revisit the HYPERVISOR_xen_version hypercall. Basically it is one big switch statement that handles each sub-operation, and each sub-operation has one or more associated stack variables. What this means is that no matter what sub-operation you request, stack storage will have been allocated for at least the largest stack variable associated with a request. The XENVER_capabilities sub-operation uses and returns the following buffer:

#define XENVER_capabilities 3
typedef char xen_capabilities_info_t[1024];
[…]
    case XENVER_capabilities:
    {
        xen_capabilities_info_t info;
 
        memset(info, 0, sizeof(info));
        arch_get_xen_caps(&info);
 
        if ( copy_to_guest(arg, info, ARRAY_SIZE(info)) )
            return -EFAULT;
        return 0;
    }      

So because only one code path can hit per function call, what ends up happening is that the stack pointer is just adjusted by 1024 bytes no matter what sub-operation is actually requested, and that space is used to hold whatever structure is associated with the operation you did request.

This means the only data we can leak is 1024 bytes down the stack, and far away from anything that could be easily correlated to earlier hypercalls I made. The only return addresses I ever seemed to leak were from what appear to be deeply nested exception handling routines. That in itself is potentially useful in that it gives you an idea of a .text offset, assuming you couldn’t already accurately profile the hypervisor from the compiler version. This was definitely not as interesting as I had hoped.

Conclusion

This bug, although fun to find and play around with, isn’t really that useful in practice. If nothing else, I hope that some people found this additional information useful or interesting. I plan to look at Xen again in the future, so hopefully I’ll have something more interesting to write about next time! If you’re just learning about Xen and want to play around with hypercalls, XSA-122 would be an easy place to get started and actually “trigger a bug” at the same time. As always, you can feel free to provide feedback or corrections either via email aaron dot adams at nccgroup dot trust or via Twitter @fidgetingbits.

Resources

  1. http://xenbits.xen.org/xsa/advisory-122.html
  2. NCC Group blog: Adventures-in-xen-exploitation/
  3. NCC Group technical advisory: Xen hypervisor xen version stack memory revelation.pdf
  4. http://xenbits.xen.org/xsa/advisory-123.html
  5. http://labs.bromium.com/2015/03/10/the-five-xen-security-advisories-from-march-2015-and-bromium-vsentry/
  6. https://github.com/QubesOS/qubes-secpack/blob/master/QSBs/qsb-016-2015.txt

Published date:  22 May 2015

Written by:  Aaron Adams