Assessing Unikernel Security

A lot has been said about the security posture of unikernels. I know I've said a mouthful myself. ;) I originally wasn't going to write this article but I've now had to respond to the same question a number of times so my hand has been forced.

One thing I should quickly point out before we get started - there is now a veritable cornucopia of unikernel implementations out there. Some are focused on security, some are focused on performance, some are focused on size, others on ease of use. Some are actively being worked on - others have languished. Some are master thesis, others are supported by multi-billionaire dollar companies. I think this is a good thing as it leads to diversity of thought and after 50 years of unix hegemony (30 with linux alone) I think it's about damn time too.

There was a recent paper. It was a document spanning more than a hundred pages and it was quite involved. However, I noticed when reading it there were a few things that stood out. One - people are still trying to compare unikernels to Linux - that which they are not. This has become a pyschological hump to many people. Unikernels are not linux just as they are not containers.

Nothing in the paper that I read was necessarily wrong but it also had a lens that wasn't fully colored either. One thing that particularly rubbed me the wrong way was the assertion that you shouldn't be trying to make advances to things like the networking stack. As of if the entire Santa Clara Valley has brought no value to the world and should just disappear. What utter nonsense. The rump project was used as a scapegoat which I find funny considering the maintainer of the rump project has been busy brewing beer for the past 3+ years with no interest in having to be the maintainer anymore. Mmmm beer. This is an unfortunate side affect of open source in general nowadays where old maintainers get attacked for wanting to do other things in their lives. It's free and open source - deal with it.

Now, since this paper has come out I felt the need to address some of these concerns - as mentioned - I don't disagree with many of the points that were brought up in the paper but every single point brought up was fixable. To be clear they even made patches for some of these problems and many of the other problems were already fixed.

This is reminiscent of the "unikernels are un-debuggable" points brought up earlier. This is funny too considering one of the authors started working on an unikernel specific debugger.... For real.

♫ "Cause they all on the same shit Based on cut down placement Uptown stay strong" ♫

It was really only a matter of time until we saw some of this pop up. When some of the worlds leading companies are starting to adopt unikernels haters gonna hate.

Anyways, enough of this intro - let's look at some of the examples and let's run them.

Show Me The Code

First off they talk about the ability to execute on the stack. If you haven't spent time in the security world and if you aren't "of age" ;) you probably have no clue what this is referring to. In the past there used to be a scourge of attacks that would place code on a program's stack, abuse it and attack the host for easy RCE. Buffer overflows are the quintessential attack here. Any linux system that is relatively new has not had to deal with this for a while since various protections were put into place a long time ago to prevent the stack from being executable. Pax/GRSecurity has been out forever and while never quite making it into the kernel similar code did end up in.

Keep in mind most of the examples here are not exploits per-se. They merely show an attack vector that *could* be exploited. It's like stating you *could* have sql injection since we found no evidence of prepared statements.

me@abc:~/ss$ cat x.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char shellcode[] = "\xcd\x03";

typedef int (*fPtr)();

int main(int argc, char *argv[]){
    int (*f)();
    char x[4];

    memcpy(x, shellcode, sizeof(shellcode));

    f = (fPtr)x;
    (*f)();
}

What we do here is declare some shellcode to call int 3 and stuff it into a variable living on said stack. Then we declare a function pointer and cast our variable as a function and try to execute it. Under linux you'll see that this segfaults as it should because linux is telling the attacker that they are doing something naughty and should not be trying to execute raw code on the stack and of course as you can see it also fails when ran with OPS.

me@s1:~/ss$ ops run x
Finding dependent shared libs
booting /home/me/.ops/images/x.img ...
qemu-system-x86_64: warning: TCG doesn't support requested feature:
CPUID.01H:ECX.vmx [bit 5]
assigned: 10.0.2.15

Page protection violation
addr 0x767ffe74, rip 0x767ffe74, error RSI
no vmap found address
dump_ptes 0x767ffe74
  l1: 0x7fe5c027
  l2: 0x7fe5b027
  l3: 0x8000000001a000e7
Unhandled: 000000000000000e
Page fault
interrupt: 000000000000000e
frame: 0000000100201000
error code: 0000000000000011
address: 00000000767ffe74
rax: 0000000000000000
rbx: 0000000000000000
rcx: 0000000005bc2740
rdx: 00000000767ffe74
rsi: 00000000767fff68
rdi: 0000000000000000
rbp: 00000000767ffe80
rsp: 00000000767ffe48
r8: 00000070d93ecd80
r9: 00000070d93ecd80
r10: 00000005002289f0
r11: 0000000000000090
r12: 0000000005bc25a0
r13: 00000000767fff60
r14: 0000000000000000
r15: 0000000000000000
rip: 00000000767ffe74
flags: 0000000000000046
stack trace:
halt
exit status 255
In their examples they were using an infinite loop (jmp -2) but in this example we were just calling out to int 3 - it's easier to tell when something fails vs something spinning.

Part Compiler/Part Operating System

One thing that is worth pointing out here and I don't think they quite pointed out so well in their paper -- it's important to realize that protections are part the responsibility of your compiler *and* part of the operating system in question. In their paper they complain since several implementations were using alt-clibs which do not provide this out of the box.

At the end of the day the operating system needs to enforce the protections that are present in the elf headers (if you are for example loading an elf - there's plenty of different binary formats out there and whose to say we won't see new ones emerge). So it's not an either/or - it's both the os and the compiler working together in tandem - this is a *highly* important point.

To illustrate this let's look at a few examples. How can we tell if a stack is supposed to be executable anyways? Scanelf is probably the most succint:
me@box:~/ss$ scanelf -e x
 TYPE   STK/REL/PTL FILE
ET_DYN RW- R-- RW- x
We can see by default (from that second column STK) that the stack is not executable. This is a good thing. Let's disable that though just to see what happens.
me@box:~/ss$ cc x.c -z execstack -o x
me@box:~/ss$ scanelf -e x
 TYPE   STK/REL/PTL FILE
ET_DYN RWX R-- RW- x
As you can see GCC is a loaded revolver but you still have to pull the trigger yourself. There are obviously usecases for this despite what the security community thinks (otherwise it wouldn't exist). But then you also have readelf. For example if we wanted to see what the text section flags were on our earlier example we could run this:
me@box:~/ss$ readelf -SW x | grep text
  [14] .text             PROGBITS        00000000000005a0 0005a0 000212 00  AX  0   0 16
This tells us that the program *expects* to have execute but no write which makes sense. However, if we do the same thing on rodata we see this:
me@box:~/ss$ readelf -SW x | grep rodata
  [16] .rodata           PROGBITS        00000000000007c0 0007c0 000004 04  AM  0   0  4
This says we don't want execute which makes sense. You also have objdump of which most reversers are probably the most familiar with. For example - want to see any constants in the rodata section of an elf? Try this:
objdump -s -j .rodata hi

As you can see with readelf we are primarily interested in the WAX permisions. There has long been a W^X policy in modern systems that states that you should never have a page both writeable and executable at the same time.

ASLR

Next up is ASLR - ASLR is the concept of address space randomization. What this means is that we don't want the attacker to be able to *easily* predict the location of a program's stack, heap, library load location, or program binary location. This gives the attacker information to map out the rest of the binary. There is an assumption that an attacker is usually remote cause if they are local it's already game over. Again this is one of those things that Linux was long afflicted with and in the past if you had an exploit that worked on one system it could work on other systems because you already had the memory layout. It used to be a thing where you'd have the same shellcode but with different offsets for different kernel versions and those would be traded back and forth on irc for things like printer shells where people would store their d0x and 0days.

Looking again at a simple example:
#include <stdio.h>
#include <stdlib.h>

void foo(){}

int main(int argc, char *argv[]){
    int y;
    char *x = (char *) malloc(128);

    printf("Library functions: %016lx, Heap: %016lx, Stack: %016lx, Binary: %016lx\n",
           &malloc, x, &y, &foo);
}
Now let's run it with OPS.
me@box:~/ss$ ops run aslr
Finding dependent shared libs
booting /home/me/.ops/images/aslr.img ...
qemu-system-x86_64: warning: TCG doesn't support requested feature:
CPUID.01H:ECX.vmx [bit 5]
assigned: 10.0.2.15
Library functions: 0000007011c97070, Heap: 000000000642c260, Stack: 0000000076bffe6c, Binary: 000000000616f750
exit status 1
me@box:~/ss$ ops run aslr
Finding dependent shared libs
booting /home/me/.ops/images/aslr.img ...
qemu-system-x86_64: warning: TCG doesn't support requested feature:
CPUID.01H:ECX.vmx [bit 5]
assigned: 10.0.2.15
Library functions: 00000070f6897070, Heap: 0000000002323260, Stack: 000000007bbffe6c, Binary: 0000000002049750
exit status 1
me@box:~/ss$ ops run aslr
Finding dependent shared libs
booting /home/me/.ops/images/aslr.img ...
qemu-system-x86_64: warning: TCG doesn't support requested feature:
CPUID.01H:ECX.vmx [bit 5]
assigned: 10.0.2.15
Library functions: 0000007012c97070, Heap: 0000000004311260, Stack: 00000000709ffe6c, Binary: 0000000003eb7750
exit status 1

Ok - so we ran through a few examples but what's the point? There is no point. All we have to do is look at the recent headline attacks to verify that. Marriot, the docker runc vulnerability, Equifax and practically everything else that was high profile in the past year or past 5 years was exploited by popping a damn shell - sometimes through one curl request for crying out loud!

Cyber is so insanely bad that you don't need crazy rop gadgetry. You just have to fail at the most basic of things - open s3 buckets, k8s exposed to the world, etc. Much of the world is still at the equivalent of post-it notes with passwords written on them. Want another frightening statistic? I was recently emailed attendee information for this year's past RSA. There were 42,490 attendees. 9,752 were exhibitors! If that doesn't tell you what the state of things are I don't know what does.

Sure, there are things that are lacking in some implementations today but that doesn't mean they might not be there tomorrow. Also, it's really really hard to argue that a full blown linux system or god forbid a container is anywhere close to the same level of sane security when the same level of protections are addressed. The fact that there is an entire cottage industry of companies that actively scan containers for malicious crap like cryptominers in them combined with things like Docker DoomsDay should tell you that containers aren't just insecure - they are beyond the pale - they are malware delivery tools.

Listen, unikernels are not a silver bullet - nothing in security is cause there are a thousand different ways to own a box and there's a thousand little kids out there wanting to crap on anything they can. Security bugs are bugs at the end of the day and all one has to do is look at any random popular github to understand how many bugs we as humans create. I also agree with the recent conclusion from the team at uncle G that software alone can not fix some of our security issues and that our architecture as it stands today is fundamentally flawed and we need to be exploring new architecture so there's that.

All in all I'm happy this paper was released because it gives the unikernel community what they really needed - a 3rd party analysis by a respected firm so now all the different unikernel implementations out there can position against it when it's brought up. Thanks!

Deploy Your First Open Source Unikernel In Seconds

Get Started Now.