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.
The Core Concept
Section titled “The Core Concept”Virtualization has two boundaries:
- Hardware virtualization (Type-1 hypervisor, Type-2 hypervisor) — the guest runs on a separate kernel.
vmexit/vmentryare CPU-assisted (VMXon Intel,SVMon AMD). The guest cannot reach the host without an explicit shared resource (file, socket, mapped page). - 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 │└──────────────────────────────────────────────────────────┘How It Works
Section titled “How It Works”-
KVM/QEMU stack on Linux:
kvmkernel module exposes/dev/kvm. A userspace process (QEMU) doesioctl(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.
VMEXITfor I/O is handled by QEMU in userspace. - Disk:
virtio-blk(paravirtual) is fastest;virtio-scsifor 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/.
-
Docker architecture:
dockerd(daemon) →containerd(runtime manager) →runc(OCI runtime).- Image is a tarball of layers (one per
Dockerfileinstruction), each a tar of filesystem diff. Storage driver (overlay2default) stacks layers as mountedoverlayfs. - Container start sequence:
runccallsclone()withCLONE_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, thenexecves the entrypoint.
-
Linux namespaces — what each one isolates:
CLONE_NEWPID— process ID space (container’sinitis 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).
-
Docker network modes:
bridge(default) —docker0interface,172.17.0.0/16, NAT viaiptables -t nat POSTROUTING -j MASQUERADE. Each container gets a veth pair.host— no namespace separation; container shares the host’seth0etc.none—loonly; fully isolated.container:<id>— share another container’s network namespace.macvlan/ipvlan— container gets a MAC and IP on the physical LAN.
-
VM network modes (libvirt):
NAT— default; libvirt creates avirbr0with192.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.
Common Techniques
Section titled “Common Techniques”| Technique | Description | Detection 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 ns | Easy — patch runc; use read-only runc binary |
| Dirty Pipe (CVE-2022-0847) from container | Overwrite /proc/1/root/usr/bin/su to break out | Easy — 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 retained | Medium — drop CAP_SYS_ADMIN |
| User namespace bypass (kernel <5.8) | unshare -r lets you write privileged files from container | Easy — disable user.max_user_namespaces or patch |
| CVE-2022-0185 (legacy cgroup v1 heap OOB) | Container escape via cgroup v1 fsconfig heap overflow | Medium — use cgroup v2; patch kernel ≥ 5.16.5 |
| CVE-2018-12130 (L1TF) | Speculative read across VM boundary on Intel SMT | Hard — disable SMT or apply microcode |
| vmware-backdoored VM escape | VMware Tools backdoor abuse ( CVE-2023-20871 [verify] ) | Hard — patch Workstation/ESXi |
| Host-only + packet capture | virbr0 traffic is captured with tcpdump on the host | Easy — by design; used for forensics |
Hands-On Commands
Section titled “Hands-On Commands”# Verify KVM is available; the kernel module is loaded and the CPU has VT-x/AMD-Vls -l /dev/kvmegrep -c '(vmx|svm)' /proc/cpuinfo # >0 = hardware virtualization supported
# Create a throwaway Ubuntu VM via cloud-init + libvirt in 60 secondsmultipass launch --name lab --cpus 2 --disk 10G --mem 2G 22.04
# Inspect a container's namespace setupdocker 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 onedocker 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 namespacesdocker psfor c in $(docker ps -q); do echo "=== $c ===" ls -l /proc/$(docker inspect -f '{{.State.Pid}}' $c)/ns/netdone
# Spin up a deliberately-vulnerable container to test CVE patchesdocker 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 --atomicvirsh snapshot-list lab-vmvirsh snapshot-revert lab-vm clean-install
# Network capture inside a container's network namespacepid=$(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 escapesdocker 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 .Detection — Defender View
Section titled “Detection — Defender View”On the host:
- auditd
-S clone -S unshare -S setns -k containerto log every namespace creation. - Sysmon for Linux (now ships with container-aware events) tags container starts with the namespace IDs.
kubectl get eventson 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.
Evasion Techniques
Section titled “Evasion Techniques”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.
| Evasion | Boundary crossed | How to detect / prevent |
|---|---|---|
--privileged | All namespaces | Block via Pod Security Admission restricted |
| CVE-2019-5736 | runc binary | Patch runc; chattr +i |
| CVE-2022-0185 | Cgroup v1 heap | Use cgroup v2; patch kernel |
| Dirty Pipe | /proc/1/root | Patch kernel; drop CAP_SYS_ADMIN |
| CVE-2018-12130 | L1 cache via SMT | Disable SMT; microcode update |
| Privileged helper abuse | Kernel via setuid | Drop 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 didioctl(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 overwrittensu, the nextsudoinvocation 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/rootfrom inside; if you can see host paths, you are in a container.
Lab Practice
Section titled “Lab Practice”Host: Ubuntu 22.04 LTS with KVM enabled. Install libvirt, virt-manager, docker.io, podman, vagrant, multipass, dive.
Objective:
- Boot a Windows 10 Enterprise VM under KVM; take a libvirt snapshot; install Mimikatz; revert.
- Start a Docker container with
--cap-drop=ALL --security-opt=no-new-privileges --read-onlyand verifycat /proc/self/status | grep Capshows0000000000000000. - Start a second container with default capabilities; demonstrate that
/proc/1/rootis accessible and the host paths are visible. - Configure a
host-onlynetwork in libvirt between two Linux VMs; verify no external connectivity (ping 8.8.8.8fails) 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.
Interview Questions
Section titled “Interview Questions”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).
Related Modules
Section titled “Related Modules”→ F1 (Networking Fundamentals) · F2 (Linux Internals) · M14 (Malware Lab Setup)