Automating Kali VM Setup with QEMU - Hack the planet

Automating Kali VM Setup with QEMU

As a productivity junkie, I’ve always been drawn to automation. Recently, I decided to streamline my process of setting up Kali Linux virtual machines using QEMU. In this article, I’ll walk you through how I’ve automated the creation and configuration of Kali VMs, complete with my preferred dotfiles and tools.

The Need for Automation

If you’re like me, you probably find yourself creating new Kali VMs frequently for various projects or testing environments. The process of setting up a new VM, updating the system, installing your favorite tools, and configuring your environment can be time-consuming and repetitive. That’s where automation comes in handy.

The Automation Process

My automation setup consists of two main components:

  1. A script to launch and set up the initial VM
  2. A script to configure the VM with my preferred settings and tools

Let’s dive into each of these components.

1. Launching the VM with QEMU

The first step in our automation process is creating and launching the VM. I’ve created a Bash script called launch_kali_vm.sh that handles this. Here’s a breakdown of what it does:

  1. Sets up variables for the VM name, image location, and resource allocation
  2. Creates a new directory for the VM
  3. Copies a base Kali image to the new VM directory
  4. Launches the VM using QEMU with specified parameters

Here’s the script:

#!/bin/bash

# Get the directory of the script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"

# Variables
TODAY_DATE=$(date +"%Y-%m-%d")
VM_NAME="Kali-$TODAY_DATE"
BASE_QCOW_IMAGE="/run/media/martin/2TB/VMS/Templates/KaliVMTemplate/kali-linux-2024.3.qcow2"
PRODUCTION_DIR="/run/media/martin/2TB/VMS/ProductionMachines"
NEW_VM_DIR="$PRODUCTION_DIR/$VM_NAME"
NEW_QCOW_IMAGE="$NEW_VM_DIR/kali-linux-2024.3.qcow2"
RAM="8192"
CORES=6
SETUP_SCRIPT="$SCRIPT_DIR/kali_setup.sh"
LOG_FILE="$SCRIPT_DIR/$VM_NAME-log.txt"
SHARED_FOLDER="$NEW_VM_DIR/shared"
DROPBOX_FOLDER="/home/martin/Dropbox"
TEST_MODE=false

# Function for logging
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# Function to check file permissions
check_permissions() {
    if [ ! -r "$1" ]; then
        log "Error: No read permission for $1"
        return 1
    fi
    if [ ! -w "$1" ]; then
        log "Error: No write permission for $1"
        return 1
    fi
    return 0
}

# Parse command line arguments
while [[ "$#" -gt 0 ]]; do
    case $1 in
        --test) TEST_MODE=true ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done

# Start logging
log "Starting Kali VM launch script"

# Check permissions for BASE_QCOW_IMAGE
if ! check_permissions "$BASE_QCOW_IMAGE"; then
    log "Exiting due to permission error"
    exit 1
fi

# Check if the setup script exists
if [ ! -f "$SETUP_SCRIPT" ]; then
    log "Error: Setup script not found at $SETUP_SCRIPT"
    exit 1
fi

# Create the new directory and shared folder
if $TEST_MODE; then
    log "[TEST] Would create directories: $NEW_VM_DIR and $SHARED_FOLDER"
else
    mkdir -p "$NEW_VM_DIR" "$SHARED_FOLDER"
    log "Created directories: $NEW_VM_DIR and $SHARED_FOLDER"
fi

# Copy the base image to the new directory with progress display
log "Copying base image to new directory..."
if $TEST_MODE; then
    log "[TEST] Would run: rsync -ah --progress $BASE_QCOW_IMAGE $NEW_QCOW_IMAGE"
else
    rsync -ah --progress "$BASE_QCOW_IMAGE" "$NEW_QCOW_IMAGE"
    log "Finished copying base image"
fi

# Copy the setup script to the shared folder
cp "$SETUP_SCRIPT" "$SHARED_FOLDER/"
log "Copied setup script to shared folder: $SHARED_FOLDER"

# Launch the VM using virt-install
if $TEST_MODE; then
    log "[TEST] Would launch VM with name: $VM_NAME"
    log "[TEST] VM configuration:"
    log "[TEST]   RAM: $RAM"
    log "[TEST]   CPUs: $CORES"
    log "[TEST]   Disk: $NEW_QCOW_IMAGE"
    log "[TEST]   Shared Folder: $SHARED_FOLDER"
    log "[TEST]   Dropbox Folder: $DROPBOX_FOLDER"
else
    log "Launching VM with name: $VM_NAME"
    sudo virt-install \
        --name "$VM_NAME" \
        --memory $RAM \
        --vcpus $CORES \
        --disk path="$NEW_QCOW_IMAGE",format=qcow2,bus=virtio \
        --import \
        --os-variant debian12 \
        --network network=default \
        --graphics spice \
        --noautoconsole \
        --filesystem source="$SHARED_FOLDER",target=host_share,mode=mapped

    log "VM launched successfully"
fi

log "Kali VM launch process completed"
echo "Kali VM launched with name $VM_NAME."
echo "The setup script has been copied to the shared folder: $SHARED_FOLDER"
echo "To access the shared folder in the VM:"
echo "1. Connect to the VM using virt-manager"
echo "2. Open a terminal in the VM"
echo "3. Run the following commands:"
echo "   sudo mkdir /mnt/host_share"
echo "   sudo mount -t 9p -o trans=virtio host_share /mnt/host_share"
echo "4. The setup script will be available at /mnt/host_share/kali_setup.sh"
echo "5. To run the setup script:"
echo "   sudo /mnt/host_share/kali_setup.sh"
echo "6. To access your Dropbox folder, use SSHFS or another file sharing method after the VM is running."
echo "7. Remember to mount the 100GB disk to /mnt/100gb"
echo "Use virt-manager to connect to the VM."

This script automates the process of creating a new VM instance, ensuring that each new VM has a unique name based on the current date.

The script creates a log file in the same directory, named Kali-YYYY-MM-DD-log.txt, which contains details about the VM creation process.

  • +Test Mode+: I have created a test mode where you can just print out to the screen so if you are customizing this script use it.
    • For test mode (no actual VM creation):
      • ./launch_kali_vm.sh --test

2. Configuring the VM

Once the VM is launched, we need to configure it with our preferred settings and tools. For this, I use another Bash script called kali_setup.sh. This script is copied to a shared folder that’s accessible from within the VM. Here’s what it does:

  1. Updates the system
  2. Installs necessary packages
  3. Sets up my preferred shell environment (Zsh with Oh My Zsh)
  4. Installs and configures Doom Emacs
  5. Clones and sets up my dotfiles
  6. Configures Docker
  7. Sets up mount points for shared folders

Here’s the script:

#!/bin/bash

# kali_setup.sh

# Set up logging
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="$HOME/Desktop/kali_setup_log.txt"
DRY_RUN=false

# Colors for terminal output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# Function for logging
log() {
    local level="$1"
    local message="$2"
    local color=""
    case $level in
    "INFO") color="$GREEN" ;;
    "WARN") color="$YELLOW" ;;
    "ERROR") color="$RED" ;;
    esac
    echo -e "${color}[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message${NC}" | tee -a "$LOG_FILE"
}

# Array to store errors
errors=()

# Function to execute or simulate command
execute() {
    if [ "$DRY_RUN" = true ]; then
        log "INFO" "[DRY RUN] Would execute: $*"
    else
        log "INFO" "Executing: $*"
        eval "$@"
        exit_status=$?
        if [ $exit_status -ne 0 ]; then
            error_msg="Command failed with exit status $exit_status: $*"
            log "ERROR" "$error_msg"
            errors+=("$error_msg")
        fi
    fi
}

# Function to execute or simulate command with automatic yes
execute_auto_yes() {
    if [ "$DRY_RUN" = true ]; then
        log "INFO" "[DRY RUN] Would execute: yes | $*"
    else
        log "INFO" "Executing with auto yes: $*"
        yes | eval "$@"
        exit_status=$?
        if [ $exit_status -ne 0 ]; then
            error_msg="Command failed with exit status $exit_status: yes | $*"
            log "ERROR" "$error_msg"
            errors+=("$error_msg")
        fi
    fi
}

# Parse command line arguments
while [[ "$#" -gt 0 ]]; do
    case $1 in
    --dry-run) DRY_RUN=true ;;
    *)
        echo "Unknown parameter passed: $1"
        exit 1
        ;;
    esac
    shift
done

log "INFO" "Starting Kali setup script"

# Update the system
log "INFO" "=== System Update ==="
execute sudo apt update

# Install packages
log "INFO" "=== Package Installation ==="
execute sudo apt install -y \
    emacs \
    eza \
    alacritty \
    git \
    bat \
    seclists

#Install docker
log "INFO" "=== Docker-ce Installation ==="
execute echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" |
    sudo tee /etc/apt/sources.list.d/docker.list
#import gpg
execute curl -fsSL https://download.docker.com/linux/debian/gpg |
    sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
execute sudo apt update
execute sudo apt install -y docker-ce docker-ce-cli containerd.io

# Install Starship
log "INFO" "=== Installing Starship ==="
execute_auto_yes curl -sS https://starship.rs/install.sh | sh

# Install oh-my-zsh
log "INFO" "=== Installing oh-my-zsh ==="
execute_auto_yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

# Install zsh plugins
log "INFO" "=== Installing zsh plugins ==="
execute git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ~/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting
execute git clone https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
execute git clone https://github.com/zdharma-continuum/fast-syntax-highlighting.git ~/.oh-my-zsh/custom/plugins/fast-syntax-highlighting
execute git clone --depth 1 -- https://github.com/marlonrichert/zsh-autocomplete.git ~/.oh-my-zsh/plugins/zsh-autocomplete

# Install tmuxinator
log "INFO" "=== Installing tmuxinator ==="
execute sudo gem install tmuxinator

# Install doom emacs
log "INFO" "=== Installing Doom Emacs ==="
execute git clone --depth 1 https://github.com/doomemacs/doomemacs ~/.emacs.d
execute_auto_yes ~/.emacs.d/bin/doom install

# Clone dotfiles repo
log "INFO" "=== Cloning dotfiles repository ==="
execute git clone https://github.com/bloodstiller/kaliconfigs.git ~/.dotfiles

# Setup dotfiles
log "INFO" "=== Setting up dotfiles ==="
execute mkdir -p ~/.doom.d
execute rm -rf ~/.zshrc ~/.doom.d/* ~/.config/starship.toml ~/.config/alacritty.yml
execute ln -s ~/.dotfiles/Zsh/.zshrc ~/.zshrc
execute ln -s ~/.dotfiles/Doom/config.el ~/.doom.d/config.el
execute ln -s ~/.dotfiles/Doom/init.el ~/.doom.d/init.el
execute ln -s ~/.dotfiles/Doom/packages.el ~/.doom.d/packages.el
execute ln -s ~/.dotfiles/Doom/README.org ~/.doom.d/README.org
execute ln -s ~/.dotfiles/Starship/starship.toml ~/.config/starship.toml
execute ln -s ~/.dotfiles/Alacritty ~/.config/Alacritty
execute ln -s ~/.dotfiles/Tmux/.tmux.conf ~/.tmux.conf

# Installing Docker Enable docker
log "INFO" "=== Enabling Docker ==="
execute sudo systemctl enable docker --now
execute sudo usermod -aG docker $USER

# Build doom packages
log "INFO" "=== Building Doom Emacs packages ==="
execute ~/.emacs.d/bin/doom sync

# Setup mount points and directories
log "INFO" "=== Setting up mount points and directories ==="
execute sudo mkdir -p /mnt/100gb
execute mkdir -p ~/Dropbox

# Mount shared folders & drive
log "INFO" "=== Mounting shared folders and drives ==="
execute sudo mount -t ext4 UUID=89edac1a-7171-4421-87a6-696050f30325 /mnt/100gb

# Update fstab
log "INFO" "=== Updating fstab ==="
execute echo "host_share /home/kali/host_share 9p trans=virtio,_netdev 0 0" | sudo tee -a /etc/fstab
execute echo "UUID=89edac1a-7171-4421-87a6-696050f30325	/mnt/100gb	ext4	defaults	0	2" | sudo tee -a /etc/fstab

# Install Dropbox
log "INFO" "=== Installing Dropbox ==="
execute wget https://www.dropbox.com/download?dl=packages/ubuntu/dropbox_2024.04.17_amd64.deb -O $HOME/Desktop/dropbox_2024.04.17_amd64.deb
execute sudo dpkg -i $HOME/Desktop/dropbox_2024.04.17_amd64.deb
execute sudo apt --fix-broken install -y

# Symlink Dropbox and Wordlists
log "INFO" "=== Setting up Dropbox and Wordlists symlinks ==="
execute sudo ln -s /mnt/100gb/Dropbox/Dropbox $HOME/Dropbox
execute sudo ln -s /usr/share/wordlists ~/wordlists

# Clean up
log "INFO" "=== Cleaning up ==="
execute sudo apt autoremove -y
execute sudo apt clean

log "INFO" "=== Setup Finished ==="
log "INFO" "Setup complete!"
log "INFO" "Finish setup of Dropbox in GUI"
log "INFO" "Remember to logout & back in for docker user to be enabled"

if [ ${#errors[@]} -ne 0 ]; then
    log "WARN" "The following errors occurred during setup:"
    for error in "${errors[@]}"; do
        log "WARN" "  - $error"
    done
else
    log "INFO" "Setup completed successfully with no errors."
fi

This script ensures that the VM is configured with my preferred settings/dotfiles and tools, making it ready for use in no time.

  • +Test Mode+: I have created a dry-run mode where you can just print out to the screen so if you are customizing this script use it.

    • For dry run mode:
      • ./kali_setup.sh --dry-run
  • +Note+: There a execute sudo apt --fix-broken install -y line as for some reason debian will not install dropbox straight away so it’s easier to run the .deb and then fix.

Benefits and Challenges

This automation setup has several benefits:

  1. Saves a significant amount of time on VM setup
  2. Ensures consistency across all my Kali VMs
  3. Makes it easy to update and modify my setup process

However, it wasn’t without its challenges. Some issues I encountered and solved include:

  • Ensuring proper permissions for shared folders
  • Handling network configuration differences between host systems
  • Dealing with package installation errors due to repository changes

But ansible exists?

“But bloodstiller why did you not use Ansible?” Well I wanted to be in a position where if I did not have access to Ansible, I could still automate the process. I will port this process to ansible soon. However I like being able to pull a kali qmemu image from online, my script, update a few variables and then I have my setup the way I like it.

Customizing the Scripts for Your Own Use

While these scripts are tailored to my specific setup, they can easily be modified to suit your own needs. Here are some key areas you might want to customize:

Modifying launch_kali_vm.sh

  1. VM Specifications: Adjust the RAM, CPU cores, and disk size to match your system’s capabilities:

       RAM="8192"  # Change this to your preferred RAM size in MB
       CORES=6     # Adjust the number of CPU cores
  2. File Paths: Update the paths to match your system’s directory structure:

       # Change this to the path to your base Kali image
       BASE_QCOW_IMAGE="/path/to/your/base/kali-image.qcow2"
       # Change this to the path to your VM directory
       PRODUCTION_DIR="/path/to/your/vm/directory"
  3. Shared Folders: Modify the shared folder locations as needed:

       SHARED_FOLDER="$NEW_VM_DIR/shared"

Customizing kali_setup.sh

  1. Package Installation: Add or remove packages based on your needs:

       execute sudo apt install -y \
           emacs \
           eza \
           alacritty \
           git \
           bat \
           seclists \
           # Add your preferred packages here
  2. Dotfiles: Replace my dotfiles repository with your own:

       execute git clone https://github.com/yourusername/your-dotfiles.git ~/.dotfiles
  3. Additional Tools: Add installation commands for any other tools you use regularly:

       # Example: Installing a custom tool
       execute wget https://example.com/custom-tool.tar.gz
       execute tar -xzvf custom-tool.tar.gz
       execute cd custom-tool && ./install.sh
  4. Mount Points: Adjust the mount points and shared directories to match your setup:

       execute sudo mkdir -p /mnt/your-custom-mount
       execute sudo mount -t ext4 UUID=your-uuid /mnt/your-custom-mount

Remember to test the modifications thoroughly to ensure they work as expected in your environment (+the scripts have a testing mode+)

By customizing these scripts, you can create a personalized, automated VM setup process that perfectly fits your workflow and toolset.

Conclusion

Automating the setup of Kali VMs has been a game-changer for my workflow. It allows me to quickly spin up consistent, fully-configured environments. It’s a system that’s efficient,easily maintainable and adaptable.

You can easily fork the VMLaunchTemplates repository, modify the scripts to suit your needs, and iterate on the process. Before you know it, you’ll have a customized setup that saves you time and headaches.

Happy hacking!