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 createhd and storageattach, remember the destroy trigger
  • VMware: Use vmware-vdiskmanager and 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.