Thousands of Viruses on My Laptop
I know not everyone gets to attend ethical hacker training. Especially not malware analysis workshops.
The interesting part? Having thousands of viruses and malware samples on your computer. Don’t try this at home. Unless you know exactly what you’re doing.
First thing you need - a proper virtualization solution. Full virtualization. Don’t even think about Docker for this. We used VirtualBox back then. And we got access to a 6-10GB data disk. That we had to attach to the workshop VM, which was Kali Linux.
When we were learning different topics, just detach the malware disk. If we screwed something up, simply recreate the workshop VM. (I was already wondering back then why we weren’t using Vagrant for this.)
But I learned something important - you can attach and detach external disk files in VirtualBox. You can create VMs with Vagrant. And you can even call VBoxManage to control things more precisely.
Good old days. But a lot of time has passed since then. Currently I work as a Go backend developer. On a completely different project.
The Official Development Environment Nobody Uses
We work on Windows with VirtualBox VMs. There’s an official Vagrantfile in the development environment… but somehow everyone just runs it once to import the VM into VirtualBox. Which is a shame, because Vagrant can do so much more than that.
When my colleague’s virtual machine died - and this tends to happen - I saw the panic on his face. All the source code and uncommitted changes were on the VM. That’s when I thought about bringing back the Vagrant and data disks concept.
Since then his VM has died 3 more times. But if he wants, he can continue his work from the data disk.
Why Everything Lives Inside the VM
You might wonder - why not just use shared folders? Why keep everything inside the VM?
Linux filesystem compatibility. Case-sensitive paths. Symlinks that actually work. File permissions that make sense. Performance that doesn’t crawl to a halt when node_modules has 50,000 files.
So yeah, everything lives inside the VM. Which means when the VM dies, everything dies with it.
Unless you have a data disk.
A New Colleague and VMware
Not long ago, we got a new colleague. But he prefers VMware. So I thought about him too. I wasn’t that familiar with using Vagrant with VMware, since until now VMware was paid software. I was very surprised and happy when I realized you can now use VMware for free. So I experimented with data disks there too.
Turns out both VirtualBox and VMware support this pattern. Create a persistent disk, mount it to the VM, store your code there. Destroy the VM whenever you want. The data survives.
How It Works - VirtualBox
The VirtualBox approach uses VBoxManage commands through Vagrant’s customization API:
# Create the disk file
data_disk_path = File.expand_path("../data-disk.vdi", __FILE__)
unless File.exist?(data_disk_path)
vb.customize ['createhd',
'--filename', data_disk_path,
'--size', 100 * 1024] # 100GB in MB
end
# Attach it as a second disk
vb.customize ['storageattach', :id,
'--storagectl', 'SATA Controller',
'--port', 1, # Port 1 = second disk
'--device', 0,
'--type', 'hdd',
'--medium', data_disk_path]
The important part is the destroy trigger. Before destroying the VM, we need to safely detach the data disk:
config.trigger.before :destroy do |trigger|
trigger.ruby do |env, machine|
# Power off first
machine.action(:halt) if machine.state.id != :poweroff
# Then detach
machine.provider.driver.execute("storageattach", machine.id,
"--storagectl", "SATA Controller",
"--port", "1",
"--device", "0",
"--medium", "none")
end
end
Without this, destroying the VM might corrupt or delete the data disk. With this trigger, the disk survives and can be reused.
How It Works - VMware
VMware uses a different approach. Instead of runtime commands, you configure the disk through VMX settings:
config.trigger.before :up do |trigger|
trigger.ruby do |env, machine|
data_disk_path = File.expand_path("../data-disk.vmdk", __FILE__)
unless File.exist?(data_disk_path)
# Create disk using vmware-vdiskmanager
cmd = "vmware-vdiskmanager -c -s 100GB -a scsi -t 0 \"#{data_disk_path}\""
system(cmd)
end
end
end
Then attach it via VMX configuration:
vmware.vmx["scsi0.present"] = "TRUE"
vmware.vmx["scsi0.virtualDev"] = "lsisas1068"
# Attach to SCSI slot 0:2
vmware.vmx["scsi0:2.present"] = "TRUE"
vmware.vmx["scsi0:2.fileName"] = data_disk_path
vmware.vmx["scsi0:2.deviceType"] = "scsi-hardDisk"
vmware.vmx["scsi0:2.mode"] = "persistent"
The beauty of VMware - no destroy trigger needed. The disk configuration is external to the VM, so it automatically survives.
The Common Part - Mounting
Both providers need the same provisioning script to partition, format, and mount the disk:
# Create GPT partition table
sudo parted -s /dev/sdb mklabel gpt
# Create partition using full disk
sudo parted -s /dev/sdb mkpart primary ext4 0% 100%
# Format with ext4
sudo mkfs.ext4 -F /dev/sdb1
# Mount it
sudo mkdir -p /mnt/data
sudo mount /dev/sdb1 /mnt/data
# Add to fstab for automatic mounting
echo '/dev/sdb1 /mnt/data ext4 defaults 0 2' | sudo tee -a /etc/fstab
# Set ownership
sudo chown vagrant:vagrant /mnt/data
# Create symlink for easy access
ln -sf /mnt/data/projects /home/vagrant/projects
Now /home/vagrant/projects points to the persistent disk. Clone your repos there. When you destroy and recreate the VM, everything is still there.
The Real Benefit
VM got corrupted? vagrant destroy && vagrant up. Code is still there.
Want to try a different Linux distro? Switch VMs, mount the same data disk. Code is still there.
Accidentally broke the system packages? Doesn’t matter. The stuff that matters is on a disk that survives.
This is the pattern I learned in that malware analysis workshop years ago. Keep the dangerous stuff isolated on a data disk. If something goes wrong, just recreate the VM.
Turns out it’s just as useful for regular development work.
What About WSL2?
Here’s the problem. When you enable Hyper-V or WSL2 (which uses Hyper-V underneath), you’re running a Type 1 hypervisor. This means all Type 2 hypervisors - VirtualBox, VMware, Parallels, QEMU - can’t access hardware acceleration properly anymore. They either don’t work at all, or run painfully slow.
And Microsoft is forcing Hyper-V on us. Windows 11 comes with Virtualization-Based Security, Device Guard, and a bunch of other “features” that require Hyper-V to be enabled. Whether you like it or not.
So here’s my take - don’t use WSL2 unless there’s a proper provisioning solution for it. That’s partly why I started building the WSL2 provider.
If Microsoft is going to force Hyper-V on us with all these Windows-slowing features, then at least there should be a solution for creating ephemeral and idempotent VMs. Just like we can with VirtualBox and VMware.
WSL2 has potential. But without proper VM lifecycle management, it’s just another way to lose your uncommitted work when things go wrong.
That’s why data disk support became the first priority. Because if we’re stuck with Hyper-V, we might as well make it work properly.
But that implementation story deserves its own post.
Try It Yourself
Here are the complete working examples for both providers.
VirtualBox Example
DATA_DISK_SIZE = (ENV['DATA_DISK_SIZE'] || 100).to_i # Size in GB
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-24.04"
config.vm.provider "virtualbox" do |vb|
data_disk_path = File.expand_path("../data-disk.vdi", __FILE__)
# Create data disk if it doesn't exist
unless File.exist?(data_disk_path)
vb.customize ['createhd',
'--filename', data_disk_path,
'--size', DATA_DISK_SIZE * 1024] # Size in MB
end
# Attach data disk to the VM
vb.customize ['storageattach', :id,
'--storagectl', 'SATA Controller',
'--port', 1,
'--device', 0,
'--type', 'hdd',
'--medium', data_disk_path]
end
# Detach data disk before VM destruction
config.trigger.before :destroy do |trigger|
trigger.ruby do |env, machine|
data_disk_path = File.expand_path("../data-disk.vdi", __FILE__)
if File.exist?(data_disk_path)
# Ensure VM is powered off
machine.action(:halt) if machine.state.id != :poweroff
# Detach the data disk
machine.provider.driver.execute("storageattach", machine.id,
"--storagectl", "SATA Controller",
"--port", "1",
"--device", "0",
"--medium", "none")
end
end
end
# Provision: Format and mount the data disk
config.vm.provision "shell", inline: <<-SHELL
if ! mount | grep -q "/dev/sdb1"; then
if ! sudo blkid /dev/sdb1 > /dev/null 2>&1; then
sudo parted -s /dev/sdb mklabel gpt
sudo parted -s /dev/sdb mkpart primary ext4 0% 100%
sudo mkfs.ext4 -F /dev/sdb1
fi
sudo mkdir -p /mnt/data
sudo mount /dev/sdb1 /mnt/data
if ! grep -q "/dev/sdb1" /etc/fstab; then
echo '/dev/sdb1 /mnt/data ext4 defaults 0 2' | sudo tee -a /etc/fstab
fi
sudo chown vagrant:vagrant /mnt/data
sudo mkdir -p /mnt/data/projects
ln -sf /mnt/data/projects /home/vagrant/projects
fi
SHELL
end
VMware Example
DATA_DISK_SIZE = (ENV['DATA_DISK_SIZE'] || 100).to_i # Size in GB
Vagrant.configure("2") do |config|
config.vm.box = "bento/ubuntu-24.04"
# Create data disk before VM starts
config.trigger.before :up do |trigger|
trigger.ruby do |env, machine|
data_disk_path = File.expand_path("../data-disk.vmdk", __FILE__)
# Convert to native path format for Windows
native_path = Vagrant::Util::Platform.windows? ?
data_disk_path.gsub('/', '\\') : data_disk_path
unless File.exist?(data_disk_path)
# Try different vmware-vdiskmanager paths
vmware_paths = [
"vmware-vdiskmanager",
"\"C:\\Program Files (x86)\\VMware\\VMware Workstation\\vmware-vdiskmanager.exe\"",
"\"C:\\Program Files\\VMware\\VMware Workstation\\vmware-vdiskmanager.exe\""
]
vmware_paths.each do |vmware_cmd|
cmd = "#{vmware_cmd} -c -s #{DATA_DISK_SIZE}GB -a scsi -t 0 \"#{native_path}\""
break if system(cmd)
end
end
end
end
config.vm.provider "vmware_desktop" do |vmware|
data_disk_path = File.expand_path("../data-disk.vmdk", __FILE__)
data_disk_path = data_disk_path.gsub('/', '\\') if Vagrant::Util::Platform.windows?
# Configure SCSI controller
vmware.vmx["scsi0.present"] = "TRUE"
vmware.vmx["scsi0.virtualDev"] = "lsisas1068"
# Attach data disk to SCSI slot 0:2
vmware.vmx["scsi0:2.present"] = "TRUE"
vmware.vmx["scsi0:2.fileName"] = data_disk_path
vmware.vmx["scsi0:2.deviceType"] = "scsi-hardDisk"
vmware.vmx["scsi0:2.mode"] = "persistent"
end
# Same provisioning script as VirtualBox
config.vm.provision "shell", inline: <<-SHELL
# Find the data disk device
DATA_DEVICE=""
for device in /dev/sd[b-z]; do
if [ -b "$device" ] && ! mount | grep -q "$device"; then
DATA_DEVICE="$device"
break
fi
done
if [ -n "$DATA_DEVICE" ]; then
PART_DEVICE="${DATA_DEVICE}1"
if ! mount | grep -q "$PART_DEVICE"; then
if ! sudo blkid "$PART_DEVICE" > /dev/null 2>&1; then
sudo parted -s "$DATA_DEVICE" mklabel gpt
sudo parted -s "$DATA_DEVICE" mkpart primary ext4 0% 100%
sudo mkfs.ext4 -F "$PART_DEVICE"
fi
sudo mkdir -p /mnt/data
sudo mount "$PART_DEVICE" /mnt/data
if ! grep -q "$PART_DEVICE" /etc/fstab; then
echo "$PART_DEVICE /mnt/data ext4 defaults 0 2" | sudo tee -a /etc/fstab
fi
sudo chown vagrant:vagrant /mnt/data
sudo mkdir -p /mnt/data/projects
ln -sf /mnt/data/projects /home/vagrant/projects
fi
fi
SHELL
end
Key Takeaways
- VirtualBox: Use
createhdandstorageattach, remember the destroy trigger - VMware: Use
vmware-vdiskmanagerand VMX configuration, no trigger needed - Both: Partition, format, mount in provisioning script
- Result: Your code survives VM destruction
Simple concept. Learned it handling malware. Applied it to daily development. Works every time.