
Why Build Your Own Lab?
When you start getting serious about malware analysis, your options are usually a cloud sandbox like Any.run or Cuckoo, or a locally spun-up VM with Windows Defender disabled. Both work for basic analysis, but neither gives you the level of control and visibility a dedicated lab provides.
A proper lab lets you:
- See exactly what a sample does at the network level, including decrypted HTTPS traffic
- Correlate behavioral events across process, registry, file system, and network simultaneously
- Protect your real IP from appearing in malware C2 infrastructure logs
- Restore to a clean state in seconds and run the next sample
- Feed everything into a open-source SIEM for long-term pattern analysis
This post walks through building exactly that, from bare metal to a fully operational lab with VPN routing, transparent TLS interception, and Wazuh monitoring across all VMs.
Architecture
Internet
|
└── Mullvad WireGuard VPN
|
Host Machine (Linux Desktop)
|
┌───────┴──────────────────────────────┐
│ libvirt default NAT (192.168.122.x) │
│ | │
│ PFSense VM │
│ WAN: 192.168.122.x (NAT) │
│ LAN: 10.0.1.1/24 │
│ VPN: Mullvad WireGuard │
│ | │
│ pfsense-lan (vmbr0) │
│ 10.0.1.0/24 │
│ | │
│ Remnux VM │
│ enp1s0: 10.0.1.x │
│ enp2s0: 10.0.2.1/24 │
│ DHCP/DNS for analysis LAN │
│ mitmproxy TLS intercept │
│ Wazuh agent │
│ | │
│ analysis-lan (vmbr1) │
│ 10.0.2.0/24 │
│ | │
│ Windows Analysis VM │
│ 10.0.2.10 (DHCP) │
│ FLARE-VM tool suite │
│ Wazuh agent │
└──────────────────────────────────────┘
Wazuh Manager: 192.168.2.137 (separate server)
Key design principles:
- Static analysis never happens on the host — only on Remnux
- Dynamic analysis only happens on the Windows VM
- All lab traffic exits via Mullvad — your real IP never appears in C2 logs
- Wazuh monitors every VM continuously
- TLS is intercepted transparently — you see exactly what malware sends over HTTPS
What You Need
Hardware:
- Linux desktop with Intel VT-x or AMD-V (check with
kvm-ok) - Minimum 32GB RAM, 500GB free disk
- Optional: separate 4TB SSD for VM backups and sample storage
Software:
- virt-manager + QEMU/KVM
- Python 3
ISOs to download:
- PFSense CE — free community edition (downloads as
.gz, decompress withgunzip) - Ubuntu 22.04 Desktop
- Windows 10 Enterprise Evaluation — 180 days free
- VirtIO drivers
Accounts:
- Mullvad VPN — anonymous, no account required, pay with cash/crypto
- Wazuh — free and open source
Host Setup
sudo apt install -y \
virt-manager qemu-kvm qemu-utils \
libvirt-daemon-system libvirt-clients \
build-essential python3 bridge-utils \
tcpdump git python-is-python3
sudo usermod -aG libvirt,kvm $USER
sudo setfacl -m u:libvirt-qemu:rx /home/$USER
sudo systemctl enable --now libvirtd
newgrp libvirt
SeaBIOS Hardening
One of the techniques from c3rb3ru5d3d53c’s lab guide is replacing the default SeaBIOS with a modified version that makes the VM harder to fingerprint. Malware commonly checks BIOS strings to detect virtual environments — this reduces that signal.
git clone https://github.com/c3rb3ru5d3d53c/seabios.git
cd seabios && make
sudo cp /usr/share/seabios/bios.bin /usr/share/seabios/bios.bin.bak
sudo cp out/bios.bin /usr/share/seabios/bios.bin
This only affects what QEMU presents to VMs — your actual motherboard BIOS is a physical chip and is completely untouched.
FLARE-VM’s VM detection check will say “not on a VM” after this change. That’s the point.
Virtual Networks
Create two isolated networks — neither has a direct internet path. PFSense controls all routing.
# PFSense LAN — between PFSense and Remnux
cat > /tmp/pfsense-lan.xml << 'EOF'
<network>
<name>pfsense-lan</name>
<forward mode='none'/>
<bridge name='vmbr0' stp='on' delay='0'/>
</network>
EOF
# Analysis LAN — between Remnux and Windows
cat > /tmp/analysis-lan.xml << 'EOF'
<network>
<name>analysis-lan</name>
<forward mode='none'/>
<bridge name='vmbr1' stp='on' delay='0'/>
</network>
EOF
virsh net-define /tmp/pfsense-lan.xml && virsh net-start pfsense-lan && virsh net-autostart pfsense-lan
virsh net-define /tmp/analysis-lan.xml && virsh net-start analysis-lan && virsh net-autostart analysis-lan
PFSense VM
PFSense sits between the internet and everything else. It manages the Mullvad WireGuard VPN tunnel and the firewall rules that keep your real IP hidden and your home network unreachable from the lab.
Create the disk and VM
qemu-img create -f qcow2 -o cluster_size=2M ~/malware_lab/vms/pfsense.qcow2 16G
In virt-manager: create a new VM from the disk, OS type FreeBSD, 2GB RAM, 1 CPU. Add two NICs:
- NIC 1:
default(libvirt NAT) — this becomes WAN - NIC 2:
pfsense-lan(vmbr0) — this becomes LAN
Boot from the PFSense ISO.
Console configuration
When PFSense boots, assign interfaces:
VLANs? n
WAN: vtnet0
LAN: vtnet1
Then option 2 to set LAN IP:
LAN IP: 10.0.1.1/24
DHCP: yes, range 10.0.1.10 - 10.0.1.240
DNS Resolver
Access the WebGUI at https://10.0.1.1. Under Services → DNS Resolver → Custom options, add upstream forwarders:
forward-zone:
name: "."
forward-addr: 8.8.8.8
forward-addr: 1.1.1.1
Mullvad WireGuard VPN
Install the WireGuard package from System → Package Manager. Then generate a keypair from the PFSense shell (Option 8):
mkdir -p /usr/local/etc/wireguard
cd /usr/local/etc/wireguard
wg genkey | tee privkey | wg pubkey > pubkey
cat pubkey
Download a WireGuard config from https://mullvad.net/en/account/wireguard-config — this generates a keypair and registers it with your account in one step. Use the values from that config to set up the tunnel and peer in PFSense under VPN → WireGuard.
Assign the tunnel as a new interface (MULLVAD), set its IPv4 address to your assigned Mullvad tunnel IP, create a gateway pointing to that IP, and add a LAN firewall rule routing all traffic through that gateway.
Verify from Remnux after setup:
curl -s https://am.i.mullvad.net/json | python3 -m json.tool
# Should show: "mullvad_exit_ip": true
Wazuh bypass
Since Wazuh manager lives on your local network (192.168.2.137) and all lab traffic routes through Mullvad, you need a static route to bypass the VPN for Wazuh specifically:
System → Routing → Static Routes
Destination: 192.168.2.137/32
Gateway: WAN_DHCP
Firewall → Rules → LAN → Add (two rules)
TCP, source LAN net, destination 192.168.2.137, port 1514, gateway WAN_DHCP
TCP, source LAN net, destination 192.168.2.137, port 1515, gateway WAN_DHCP
This allows Wazuh agents to report home without exposing anything else on your local network to the lab.
Remnux VM (Ubuntu 22.04)
Remnux serves two purposes: it’s the static analysis workstation and the network gateway for the Windows VM. Everything the Windows VM sends to the internet passes through Remnux first, which is why TLS interception works.
qemu-img create -f qcow2 -o cluster_size=2M ~/malware_lab/vms/remnux.qcow2 60G
Two NICs:
- NIC 1:
pfsense-lan(uplink to PFSense/internet) - NIC 2:
analysis-lan(gateway for Windows VM)
Post-install configuration
sudo apt install -y \
dnsmasq isc-dhcp-server docker.io \
wireshark tshark tcpdump nmap netcat \
python3-pip git curl wget vim net-tools \
iptables-persistent
pip3 install --user pefile capstone dnfile yara-python requests scapy
sudo usermod -aG docker $USER
docker pull remnux/remnux-distro:focal
echo 'alias remnux="docker run --rm -it -v $(pwd):$(pwd) -w $(pwd) -u remnux remnux/remnux-distro:focal bash"' >> ~/.bashrc
git clone https://github.com/Yara-Rules/rules.git ~/yara-rules/community
Network configuration
# /etc/netplan/01-lab.yaml
network:
version: 2
renderer: networkd
ethernets:
enp1s0:
dhcp4: yes
enp2s0:
addresses:
- 10.0.2.1/24
DHCP for the analysis LAN
# /etc/dhcp/dhcpd.conf
subnet 10.0.2.0 netmask 255.255.255.0 {
option routers 10.0.2.1;
option domain-name-servers 10.0.2.1;
range 10.0.2.10 10.0.2.254;
}
IP forwarding and NAT
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
sudo iptables -A FORWARD -i enp2s0 -o enp1s0 -j ACCEPT
sudo iptables -A FORWARD -i enp1s0 -o enp2s0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo netfilter-persistent save
mitmproxy TLS interception
wget https://downloads.mitmproxy.org/12.2.3/mitmproxy-12.2.3-linux-x86_64.tar.gz
tar -xzvf mitmproxy-12.2.3-linux-x86_64.tar.gz
sudo mv mitmdump mitmproxy mitmweb /usr/bin/
# Redirect HTTP and HTTPS from analysis LAN through mitmproxy
sudo iptables -t nat -A PREROUTING -i enp2s0 -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -i enp2s0 -p tcp --dport 443 -j REDIRECT --to-port 8080
sudo netfilter-persistent save
# Generate CA cert
mitmproxy --mode transparent --listen-host 0.0.0.0 --listen-port 8080
# Ctrl+C once started — cert is at ~/.mitmproxy/mitmproxy-ca-cert.cer
Create a session startup script at ~/start-intercept.sh:
#!/bin/bash
NAME=${1:-capture_$(date +%Y%m%d_%H%M%S)}
mkdir -p ~/pcaps ~/tls-secrets
mitmproxy \
--mode transparent \
--listen-host 0.0.0.0 \
--listen-port 8080 \
--ssl-insecure \
--save-stream-file ~/pcaps/${NAME}.pcap \
--set hardump=~/tls-secrets/${NAME}.txt
Install the CA cert on Windows by serving it from Remnux:
python3 -m http.server 8888 --directory ~/.mitmproxy/
Then on Windows browse to http://10.0.2.1:8888/mitmproxy-ca-cert.cer and run:
certutil -addstore root mitmproxy-ca-cert.cer
Windows Analysis VM (FLARE-VM)
The Windows VM sits only on the analysis LAN — it has no path to your host or home network. All its internet traffic routes through Remnux → PFSense → Mullvad.
qemu-img create -f qcow2 -o cluster_size=2M ~/malware_lab/vms/windows-analysis.qcow2 128G
One NIC: analysis-lan only.
VirtIO disk driver — the common gotcha
The Windows installer won’t see a VirtIO disk. When the disk selection screen shows nothing, click Load driver and browse the VirtIO ISO to viostor\w10\amd64. Select Red Hat VirtIO SCSI pass-through controller and the disk appears. Windows will then have the driver baked in from first boot.
Post-install hardening
# Disable Tamper Protection first via Settings → Windows Security UI
# Then via registry:
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v DisableAntiSpyware /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection" /v DisableRealtimeMonitoring /t REG_DWORD /d 1 /f
# Disable Windows Update
Stop-Service wuauserv -Force
Set-Service wuauserv -StartupType Disabled
# Disable firewall
netsh advfirewall set allprofiles state off
# Static DNS — survives reboots
# Control Panel → Network → Ethernet → IPv4 → Use following DNS
# Preferred: 10.0.2.1
# Leave alternate blank
FLARE-VM
Set-ExecutionPolicy Unrestricted -Force
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1" -OutFile "$env:TEMP\install.ps1"
& "$env:TEMP\install.ps1"
FLARE-VM will say “not on a VM” — that’s the SeaBIOS modification working. Proceed anyway. The install takes 30-60 minutes with multiple automatic reboots.
Recommended packages: x64dbg, Ghidra, Die, CFF Explorer, Procmon, Regshot, API Monitor, Wireshark, HxD, CyberChef, Python3.
Wazuh Integration
With Wazuh agents on both Remnux and Windows, every process creation, registry modification, file system change, and network connection is captured and correlated in your SIEM.
Remnux agent
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | sudo gpg --dearmor -o /usr/share/keyrings/wazuh.gpg
echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" \
| sudo tee /etc/apt/sources.list.d/wazuh.list
sudo apt-get update
sudo WAZUH_MANAGER="192.168.2.137" apt-get install -y wazuh-agent
# Register with a valid name — single letters are rejected
sudo /var/ossec/bin/agent-auth -m 192.168.2.137 -A remnux-analysis
sudo systemctl enable --now wazuh-agent
Windows agent
Invoke-WebRequest -Uri "https://packages.wazuh.com/4.x/windows/wazuh-agent-4.14.5-1.msi" -OutFile "$env:TEMP\wazuh-agent.msi"
Start-Process msiexec.exe -ArgumentList "/i $env:TEMP\wazuh-agent.msi /q WAZUH_MANAGER=192.168.2.137" -Wait
& "C:\Program Files (x86)\ossec-agent\agent-auth.exe" -m 192.168.2.137 -A windows-analysis
Start-Service wazuh
Analysis Workflow
Static analysis (always on Remnux, never the host)
# Quick triage
file sample.exe
strings -n 8 sample.exe | head -100
md5sum sample.exe && sha256sum sample.exe
yara -r ~/yara-rules/community/ sample.exe
# Full REMnux toolset
remnux
# Inside the container — pecheck, floss, capa and much more
Dynamic analysis session
# 1. Start TLS intercept on Remnux
~/start-intercept.sh session-name
# 2. Serve sample to Windows
python3 -m http.server 8000 --directory ~/samples/
# Windows: http://10.0.2.1:8000/sample.exe
On Windows:
- Open Procmon
- Take Regshot first snapshot
- Execute sample
- Observe behavior — watch Procmon, Wazuh dashboard, network
- Take Regshot second snapshot and compare
- Stop Procmon
# 3. Analyse PCAP with TLS decryption
wireshark ~/pcaps/session-name.pcap
# Edit → Preferences → Protocols → TLS
# Pre-Master-Secret log → ~/tls-secrets/session-name.txt
# 4. Restore Windows to clean state
python3 lab.py --restore windows-analysis flare-clean
Backup Strategy
VM disks and configs back up to a 4TB Samsung SSD formatted as BTRFS (for compression and checksumming). The lab.py manager script handles everything:
# Format and mount (one time)
sudo mkfs.btrfs -f -L malware-backup /dev/sdb1
sudo mount -o compress=zstd /dev/sdb1 /mnt/malware-ssd
# Run backup (VMs must be shut down)
python3 lab.py --stop-lab
python3 lab.py --backup
# Restore after OS wipe
sudo mount /mnt/malware-ssd
python3 lab.py --restore-from-backup
Malware samples live permanently on the SSD — never on the NVMe. This means your collection survives an OS reinstall automatically.
Security Posture
| Lab VMs | Host | |
|---|---|---|
| Real IP exposed | Never (Mullvad) | Normal |
| Home network accessible | No | Yes |
| Wazuh monitored | Yes | No |
| Internet access | Via Mullvad only | Direct |
| Samples executed | Windows VM only | Never |
The only home network resource reachable from lab VMs is the Wazuh manager on ports 1514 and 1515 specifically — enforced by PFSense firewall rules. Everything else on your home network is unreachable from the lab.
Lab Manager Script
All of this is managed by a Python script (lab.py) that wraps virsh and qemu-img:
python3 lab.py --start-lab # PFSense → Remnux → Windows in order
python3 lab.py --stop-lab
python3 lab.py --status # full overview of VMs, snapshots, captures
python3 lab.py --restore windows-analysis flare-clean
python3 lab.py --tls-intercept --session-name wannacry-01
python3 lab.py --check-vpn-ip # verify Mullvad is active
python3 lab.py --backup # compress and back up all VM disks
python3 lab.py --restore-from-backup # full restore after OS wipe
References
- c3rb3ru5d3d53c’s KVM Malware Lab — the approach this is based on
- c3rb3ru5d3d53c’s GitHub — modified SeaBIOS and helper scripts
- REMnux — Linux distribution for malware analysis
- FLARE-VM — Windows malware analysis distribution by Mandiant
- Mullvad VPN — anonymous VPN, no logs, no account required
- Wazuh — open source SIEM/XDR
- John Hammond — malware analysis content
- Vx Undergound — malware sample repository
- TCM Security - Practical Malware Analysis & Triage Course
Built on Ubuntu 22.04 LTS with KVM/QEMU. All analysis traffic exits via WireGuard. Tested with WannaCry, NightSky ransomware, and BPFDoor variants.