Skip to content

Virtualization & Lab Setup

Every malware detonation, every kernel exploit, and every red-team tool run needs an isolated environment that can be snapshotted, restored, and reset in seconds. The choice between a Type-1 hypervisor and a Docker container is not just a performance decision — it is a security boundary decision. CVE-2019-5736 (runc container escape) and CVE-2022-0847 (Dirty Pipe) work because container “isolation” depends on a kernel that the host and container share. A VM sits on a different kernel. Knowing the difference is the difference between running malware safely and becoming a customer’s incident.

Virtualization has two boundaries:

  1. Hardware virtualization (Type-1 hypervisor, Type-2 hypervisor) — the guest runs on a separate kernel. vmexit/vmentry are CPU-assisted (VMX on Intel, SVM on AMD). The guest cannot reach the host without an explicit shared resource (file, socket, mapped page).
  2. OS-level virtualization (containers) — the guest shares the host kernel. Isolation is enforced by namespaces (CLONE_NEWPID, CLONE_NEWNET, etc.), cgroups, capabilities, seccomp, and an LSM. Every kernel exploit that hits the host kernel can hit the container, and vice versa.
┌──────────────────────────────────────────────────────────┐
│ Type-1 hypervisor (KVM, ESXi, Hyper-V) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ VM1 │ │ VM2 │ │ VM3 │ │
│ │ kernel A │ │ kernel B │ │ kernel C │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ──────────── hypervisor ──────────────────── │
│ hardware (CPU VMX/SVM, IOMMU, NPT/EPT) │
├──────────────────────────────────────────────────────────┤
│ Containers (Docker, containerd, runc) │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ c1 │ │ c2 │ │ c3 │ │
│ │ user │ │ user │ │ user │ │
│ │ space │ │ space │ │ space │ │
│ └────────┘ └────────┘ └────────┘ │
│ ───── shared host kernel ───── │
│ namespaces + cgroups + capabilities + seccomp + LSM │
└──────────────────────────────────────────────────────────┘
  1. KVM/QEMU stack on Linux:

    • kvm kernel module exposes /dev/kvm. A userspace process (QEMU) does ioctl(KVM_CREATE_VM), allocates guest memory, registers vCPUs.
    • Memory: EPT (Extended Page Tables, Intel) or NPT (Nested Page Tables, AMD). Guest physical → host physical translation is hardware-assisted. VMEXIT for I/O is handled by QEMU in userspace.
    • Disk: virtio-blk (paravirtual) is fastest; virtio-scsi for many disks; raw block device for max I/O.
    • Net: virtio-net (paravirtual), e1000 (emulated Intel), rtl8139 (emulated Realtek — slowest).
    • Management: libvirt (virsh, virt-manager). XML configs under /etc/libvirt/qemu/.
  2. Docker architecture:

    • dockerd (daemon) → containerd (runtime manager) → runc (OCI runtime).
    • Image is a tarball of layers (one per Dockerfile instruction), each a tar of filesystem diff. Storage driver (overlay2 default) stacks layers as mounted overlayfs.
    • Container start sequence: runc calls clone() with CLONE_NEWPID|CLONE_NEWNET|CLONE_NEWNS|CLONE_NEWIPC|CLONE_NEWUTS|CLONE_NEWUSER, applies cgroup v2 limits, applies seccomp filter (default profile blocks ~50 syscalls), drops all capabilities except a small allowlist, applies AppArmor or SELinux label, then execves the entrypoint.
  3. Linux namespaces — what each one isolates:

    • CLONE_NEWPID — process ID space (container’s init is PID 1).
    • CLONE_NEWNET — network interfaces, routes, firewall, sockets.
    • CLONE_NEWNS — mount points.
    • CLONE_NEWIPC — System V IPC, POSIX message queues.
    • CLONE_NEWUTS — hostname, domainname.
    • CLONE_NEWUSER — UID/GID mapping; enables unprivileged containers.
    • CLONE_NEWCGROUP — cgroup view (since kernel 4.6).
    • CLONE_NEWTIME — monotonic/boot clock offset (since kernel 5.6).
  4. Docker network modes:

    • bridge (default) — docker0 interface, 172.17.0.0/16, NAT via iptables -t nat POSTROUTING -j MASQUERADE. Each container gets a veth pair.
    • host — no namespace separation; container shares the host’s eth0 etc.
    • none — lo only; fully isolated.
    • container:<id> — share another container’s network namespace.
    • macvlan / ipvlan — container gets a MAC and IP on the physical LAN.
  5. VM network modes (libvirt):

    • NAT — default; libvirt creates a virbr0 with 192.168.122.0/24, dnsmasq for DHCP, iptables MASQUERADE.
    • Bridged — VM gets an IP directly on the physical LAN.
    • Isolated — VM-to-VM only; no external connectivity.
    • routed — VMs are on a routed subnet; libvirt does not NAT.
    • Private — VMs can reach out, but external hosts cannot initiate in.
TechniqueDescriptionDetection Difficulty
runc escape (CVE-2019-5736)Overwrite host runc binary from inside container via /proc/self/exe while runc is briefly inside container’s mount nsEasy — patch runc; use read-only runc binary
Dirty Pipe (CVE-2022-0847) from containerOverwrite /proc/1/root/usr/bin/su to break outEasy — patch kernel ≥ 5.16.11/5.15.25/5.10.102
Cap-only escape (CAP_SYS_ADMIN abuse)mount host fs or open /proc/1/root from container with CAP_SYS_ADMIN retainedMedium — drop CAP_SYS_ADMIN
User namespace bypass (kernel <5.8)unshare -r lets you write privileged files from containerEasy — disable user.max_user_namespaces or patch
CVE-2022-0185 (legacy cgroup v1 heap OOB)Container escape via cgroup v1 fsconfig heap overflowMedium — use cgroup v2; patch kernel ≥ 5.16.5
CVE-2018-12130 (L1TF)Speculative read across VM boundary on Intel SMTHard — disable SMT or apply microcode
vmware-backdoored VM escapeVMware Tools backdoor abuse ( CVE-2023-20871 [verify] )Hard — patch Workstation/ESXi
Host-only + packet capturevirbr0 traffic is captured with tcpdump on the hostEasy — by design; used for forensics
Terminal window
# Verify KVM is available; the kernel module is loaded and the CPU has VT-x/AMD-V
ls -l /dev/kvm
egrep -c '(vmx|svm)' /proc/cpuinfo # >0 = hardware virtualization supported
# Create a throwaway Ubuntu VM via cloud-init + libvirt in 60 seconds
multipass launch --name lab --cpus 2 --disk 10G --mem 2G 22.04
# Inspect a container's namespace setup
docker run --rm --privileged alpine sh -c '
ls -l /proc/self/ns
cat /proc/self/status | grep -E "^Cap|^Uid|^NSsid"
'
# Compare capability set of a default container vs a privileged one
docker run --rm alpine sh -c 'grep CapEff /proc/self/status'
docker run --rm --privileged alpine sh -c 'grep CapEff /proc/self/status'
# Show all running containers and their network namespaces
docker ps
for c in $(docker ps -q); do
echo "=== $c ==="
ls -l /proc/$(docker inspect -f '{{.State.Pid}}' $c)/ns/net
done
# Spin up a deliberately-vulnerable container to test CVE patches
docker run --rm -it --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges:true \
--read-only --tmpfs /tmp \
ubuntu:22.04 bash
# Snapshot a libvirt VM (atomic; can be reverted in <2s)
virsh snapshot-create-as --domain lab-vm clean-install \
--disk-only --atomic
virsh snapshot-list lab-vm
virsh snapshot-revert lab-vm clean-install
# Network capture inside a container's network namespace
pid=$(docker inspect -f '{{.State.Pid}}' container1)
nsenter -t $pid -n tcpdump -nni eth0 -w /tmp/c1.pcap
# Force a rootless container with user namespace mapping; safer against escapes
docker run --rm --userns=host alpine id
# Build a multi-arch image (e.g., for Kali ARM + x86 labs)
docker buildx build --platform linux/amd64,linux/arm64 -t lab:latest .

On the host:

  • auditd -S clone -S unshare -S setns -k container to log every namespace creation.
  • Sysmon for Linux (now ships with container-aware events) tags container starts with the namespace IDs.
  • kubectl get events on K8s logs pod creation including the security context (runAsUser, runAsNonRoot, allowPrivilegeEscalation).

MITRE ATT&CK T1611 — Escape to Host (container escape; this is the technique ID for runc/CVE-2019-5736, Dirty Pipe from container, etc.).

MITRE ATT&CK T1610 — Deploy Container (adversary uses Docker/K8s to deploy malicious containers in the victim’s cluster — seen in cryptomining campaigns).

MITRE ATT&CK T1613 — Container and Resource Discovery (docker ps, kubectl get pods, crictl ps).

MITRE ATT&CK T1612 — Build Image on Host (adversary builds a malicious container image — often the precursor to T1611).

MITRE ATT&CK T1614.001 — System Location Discovery: VM Shell Artifacts (defenders look for VM-emulator artifacts like vmware-tools-installed packages, the QEMU CPU model name, the VBOX shell).

Wazuh rule 920 / 261 — Privileged container start [verify — rule fires when docker run --privileged is detected in auditd].

Wazuh rule 920 / 515 — Container escape via --privileged mode or CAP_SYS_ADMIN retained [verify].

For container image forensics: dive (https://github.com/wagoodman/dive) shows each layer of an image with the file diff — useful for incident response on a malicious image.

Container escape via --privileged. The “privileged” flag turns off all namespace separation. This is the most common escape vector in real environments because developers copy-paste docker run --privileged from Stack Overflow. Defense: enforce Pod Security Admission (replaces PodSecurityPolicy in K8s 1.25+) with restricted policy; use AppArmor or SELinux profile; default-deny --privileged via Docker daemon configuration (--userns-remap=default).

runc overwrite (CVE-2019-5736). The attack: container opens /proc/self/exe, which is the host’s runc binary while it is briefly execve’d inside the container during docker exec. Then the container writes a malicious ELF over /proc/self/exe, which is the host’s runc. The next docker exec runs the malicious runc → host code execution. Defense: patch runc to use a clone of itself, or set runc binary to read-only (immutable chattr +i).

Dirty Pipe from container (CVE-2022-0847). The container’s /proc/1/root points to the host’s PID 1 mount namespace, so a container that can reach the pipe-buffer code path can overwrite host files. Defense: patch kernel; do not grant containers CAP_SYS_ADMIN.

Kernel namespace bypass via CAP_SYS_PTRACE. A container with CAP_SYS_PTRACE can ptrace host processes through /proc/1/.... Defense: drop CAP_SYS_PTRACE in the container’s runtime spec.

VM escape via VMware backdoor (CVE-2023-20871 [verify]). The VMware tools backdoor is a port-I/O interface (0x5658/0x5659) the guest uses to talk to the hypervisor. A vulnerable VMX could be tricked into writing to host memory. Defense: patch VMware; on hardened deployments, disable the backdoor.

EvasionBoundary crossedHow to detect / prevent
--privilegedAll namespacesBlock via Pod Security Admission restricted
CVE-2019-5736runc binaryPatch runc; chattr +i
CVE-2022-0185Cgroup v1 heapUse cgroup v2; patch kernel
Dirty Pipe/proc/1/rootPatch kernel; drop CAP_SYS_ADMIN
CVE-2018-12130L1 cache via SMTDisable SMT; microcode update
Privileged helper abuseKernel via setuidDrop setuid_root from container images

⚠️ What caught me: I once ran a malware sample in a “VM” that was actually a Docker container with --privileged. The sample was a Linux ELF that did ioctl(FD, 0xc0045878, ...) to a vulnerable driver, except the host kernel didn’t have that driver — and the sample’s fallback path was CVE-2022-0847 against /proc/1/root/usr/bin/su. The host kernel was 5.15.0-25 (unpatched). Within 4 seconds the sample had overwritten su, the next sudo invocation gave me a root shell on the host. The “VM” was supposed to be snapshotted but it was actually a container — different snapshot semantics, same kernel. Lesson: “isolated environment” is a technical term, not a marketing word. Verify the boundary by inspecting /proc/1/root from inside; if you can see host paths, you are in a container.

Host: Ubuntu 22.04 LTS with KVM enabled. Install libvirt, virt-manager, docker.io, podman, vagrant, multipass, dive.

Objective:

  1. Boot a Windows 10 Enterprise VM under KVM; take a libvirt snapshot; install Mimikatz; revert.
  2. Start a Docker container with --cap-drop=ALL --security-opt=no-new-privileges --read-only and verify cat /proc/self/status | grep Cap shows 0000000000000000.
  3. Start a second container with default capabilities; demonstrate that /proc/1/root is accessible and the host paths are visible.
  4. Configure a host-only network in libvirt between two Linux VMs; verify no external connectivity (ping 8.8.8.8 fails) but inter-VM connectivity works.

Expected flag format: flag{libvirt_snapshot_done_caps_dropped_proc_visible_no_ext_net}.

Detection that should fire: auditd -S clone -S unshare -k container shows the clone flags; virsh snapshot-list shows the snapshot; iptables -L FORWARD shows libvirt’s REJECT rule on the host-only bridge.

Conceptual: explain the difference between a Type-1 hypervisor and a container in terms of (a) kernel boundary, (b) syscall surface, (c) escape surface, and (d) snapshot semantics.

Technical: walk through what happens when docker run --rm alpine echo hi is executed. Name every kernel object created and every namespace that is entered.

Scenario: a customer asks whether they should run untrusted malware in a Docker container “for cost.” List three kernel CVEs that have allowed container-to-host escape, and explain why a VM is the safer choice for this workload.

What is the difference between --cap-drop=ALL and --security-opt=no-new-privileges? Which one prevents the setuid bit on /usr/bin/su from working?

Technical: explain CVE-2019-5736 in terms of the window during which runc is briefly present inside the container’s mount namespace, and the three-line defense (patch, read-only binary, AppArmor).

→ F1 (Networking Fundamentals) · F2 (Linux Internals) · M14 (Malware Lab Setup)