Skip to content

Commit

Permalink
Merge pull request #656 from Ana06/vbox-build-flare-vm
Browse files Browse the repository at this point in the history
Add script to install FLARE-VM in a guest VM
  • Loading branch information
Ana06 authored Feb 7, 2025
2 parents 5916533 + 7fc9d16 commit 858c908
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 93 deletions.
142 changes: 76 additions & 66 deletions virtualbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,62 @@

**This folder contains several scripts related to enhance building, exporting, and using FLARE-VM in VirtualBox.**

## Export snapshots

[`vbox-export-snapshots.py`](vbox-export-snapshots.py) export one or more snapshots in the same VirtualBox VM as .ova, changing the network to a single Host-Only interface.
It also generates a file with the SHA256 hash of the exported `.ova`.
This script is useful to export several versions of FLARE-VM after its installation consistently and with the internet disabled by default (desired for malware analysis).
For example, you may want to export a VM with the default FLARE-VM configuration and another installing in addition the packages `visualstudio.vm` and `pdbs.pdbresym.vm`.
These packages are useful for malware analysis but are not included in the default configuration because of the consequent increase in size.
The scripts receives the path of the JSON configuration file as argument.
See configuration example files in the [`configs`](configs/) directory.
## Clean up snapshots

It is not possible to select and delete several snapshots in VirtualBox, making cleaning up your VM manually after having creating a lot snapshots time consuming and tedious (possible errors when deleting several snapshots simultaneously).

[`vbox-clean-snapshots.py`](vbox-clean-snapshots.py) cleans a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name.

### Example

```
$ ./vbox-export-snapshots.py configs/export_win10_flare-vm.json
$ ./vbox-remove-snapshots.py FLARE-VM.20240604
Exporting snapshots from "FLARE-VM.testing" {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d}
Export directory: "/home/anamg/EXPORTED VMS"
Cleaning FLARE-VM.20240604 🫧 Snapshots to delete:
Snapshot 1
wip unpacked
JS downloader deobfuscated
Snapshot 6
C2 decoded
Snapshot 5
wip
Snapshot 4
Snapshot 3
Snapshot 2
complicated chain - all samples ready
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✨ restored snapshot "FLARE-VM"
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: saved. Starting VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ⚙️ network set to single hostonly adapter
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🔄 power cycling before export... (it will take some time, go for an 🍦!)
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: poweroff. Starting VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🚧 exporting "FLARE-VM.20250129.dynamic"... (it will take some time, go for an 🍦!)
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ EXPORTED "/home/anamg/EXPORTED VMS/FLARE-VM.20250129.dynamic.ova"
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ GENERATED "/home/anamg/EXPORTED VMS/FLARE-VM.20250129.dynamic.ova.sha256": 73c3de4175449987ef6047f6e0bea91c1036a8599b43113b3f990104ab294a47
VM state: Paused
⚠️ Snapshot deleting is slower in a running VM and may fail in a changing state
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ❌ ERROR exporting "FLARE-VM.full":Command 'VBoxManage snapshot {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} restore FLARE-VM.full' failed: Could not find a snapshot named 'FLARE-VM.full'
Confirm deletion (press 'y'):y
Deleting... (this may take some time, go for an 🍦!)
🫧 DELETED 'Snapshot 1'
🫧 DELETED 'wip unpacked'
🫧 DELETED 'JS downloader deobfuscated '
🫧 DELETED 'Snapshot 6'
🫧 DELETED 'C2 decoded'
🫧 DELETED 'Snapshot 5'
🫧 DELETED 'wip'
🫧 DELETED 'Snapshot 4'
🫧 DELETED 'Snapshot 3'
🫧 DELETED 'Snapshot 2'
🫧 DELETED 'complicated chain - all samples ready'
See you next time you need to clean up your VMs! ✨
Done! 🙃
```

##### Before

![Before](../Images/vbox-clean-snapshots_before.png)

##### After

![After](../Images/vbox-clean-snapshots_after.png)


## Check internet adapter status

[`vbox-adapter-check.py`](vbox-adapter-check.py) prints the status of all internet adapters of all VMs in VirtualBox.
Expand Down Expand Up @@ -72,57 +93,46 @@ FLARE-VM.20240808.dynamic 8: Disabled Null
![Notification](../Images/vbox-adapter-check_notification.png)


## Clean up snapshots

It is not possible to select and delete several snapshots in VirtualBox, making cleaning up your VM manually after having creating a lot snapshots time consuming and tedious (possible errors when deleting several snapshots simultaneously).
## Export snapshots

[`vbox-clean-snapshots.py`](vbox-clean-snapshots.py) cleans a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name.
[`vbox-export-snapshots.py`](vbox-export-snapshots.py) export one or more snapshots in the same VirtualBox VM as .ova, changing the network to a single Host-Only interface.
It also generates a file with the SHA256 hash of the exported `.ova`.
This script is useful to export several versions of FLARE-VM after its installation consistently and with the internet disabled by default (desired for malware analysis).
For example, you may want to export a VM with the default FLARE-VM configuration and another installing in addition the packages `visualstudio.vm` and `pdbs.pdbresym.vm`.
These packages are useful for malware analysis but are not included in the default configuration because of the consequent increase in size.
The scripts receives the path of the JSON configuration file as argument.
See configuration example files in the [`configs`](configs/) directory.

### Example

```
$ ./vbox-remove-snapshots.py FLARE-VM.20240604
Cleaning FLARE-VM.20240604 🫧 Snapshots to delete:
Snapshot 1
wip unpacked
JS downloader deobfuscated
Snapshot 6
C2 decoded
Snapshot 5
wip
Snapshot 4
Snapshot 3
Snapshot 2
complicated chain - all samples ready
VM state: Paused
⚠️ Snapshot deleting is slower in a running VM and may fail in a changing state
$ ./vbox-export-snapshots.py configs/export_win10_flare-vm.json
Confirm deletion (press 'y'):y
Exporting snapshots from "FLARE-VM.testing" {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d}
Export directory: "/home/anamg/EXPORTED VMS"
Deleting... (this may take some time, go for an 🍦!)
🫧 DELETED 'Snapshot 1'
🫧 DELETED 'wip unpacked'
🫧 DELETED 'JS downloader deobfuscated '
🫧 DELETED 'Snapshot 6'
🫧 DELETED 'C2 decoded'
🫧 DELETED 'Snapshot 5'
🫧 DELETED 'wip'
🫧 DELETED 'Snapshot 4'
🫧 DELETED 'Snapshot 3'
🫧 DELETED 'Snapshot 2'
🫧 DELETED 'complicated chain - all samples ready'
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✨ restored snapshot "FLARE-VM"
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: saved. Starting VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ⚙️ network set to single hostonly adapter
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🔄 power cycling before export... (it will take some time, go for an 🍦!)
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: poweroff. Starting VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} state: running. Shutting down VM...
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} 🚧 exporting "FLARE-VM.20250129.dynamic"... (it will take some time, go for an 🍦!)
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ EXPORTED "/home/anamg/EXPORTED VMS/FLARE-VM.20250129.dynamic.ova"
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ✅ GENERATED "/home/anamg/EXPORTED VMS/FLARE-VM.20250129.dynamic.ova.sha256": 73c3de4175449987ef6047f6e0bea91c1036a8599b43113b3f990104ab294a47
See you next time you need to clean up your VMs! ✨
VM {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} ❌ ERROR exporting "FLARE-VM.full":Command 'VBoxManage snapshot {2bc66f50-9ecb-4b10-a4dd-0cc329bc383d} restore FLARE-VM.full' failed: Could not find a snapshot named 'FLARE-VM.full'
Done! 🙃
```

##### Before
## Build FLARE-VM


![Before](../Images/vbox-clean-snapshots_before.png)

##### After

![After](../Images/vbox-clean-snapshots_after.png)
[`vbox-build-flare-vm.py`](vbox-build-flare-vm.py) restores a `BUILD-READY` snapshot, copies files required for the installation (like the IDA Pro installer and the FLARE-VM configuration file) and starts the FLARE-VM installation.
The `BUILD-READY` snapshot is expected to be an empty Windows installation that satisfies the FLARE-VM installation requirements and has UAC disabled
To disable UAC execute in a cmd console with admin rights and restart the VM for the change to take effect:
```
%windir%\System32\reg.exe ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
```
74 changes: 74 additions & 0 deletions virtualbox/vbox-build-vm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/python3
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Restore a `BUILD-READY` snapshot, copy files required for the installation (like the IDA Pro installer and
the FLARE-VM configuration file) and start the FLARE-VM installation.
"""

import os

from vboxcommon import ensure_vm_running, get_vm_uuid, restore_snapshot, run_vboxmanage

VM_NAME = "FLARE-VM.testing"
# The base snapshot is expected to be an empty Windows installation that satisfies the FLARE-VM installation requirements and has UAC disabled
# To disable UAC execute in a cmd console with admin rights and restart the VM for the change to take effect:
# %windir%\System32\reg.exe ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
BASE_SNAPSHOT = "BUILD-READY"
GUEST_USERNAME = "flare"
GUEST_PASSWORD = "password"
script_directory = os.path.dirname(os.path.realpath(__file__))
REQUIRED_FILES_DIR = os.path.expanduser("~/REQUIRED FILES")
REQUIRED_FILES_DEST = f"C:\\Users\\{GUEST_USERNAME}\\Desktop"
INSTALLATION_COMMAND = r"""
$desktop=[Environment]::GetFolderPath("Desktop")
cd $desktop
Set-ExecutionPolicy Unrestricted -Force
$url="https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1"
$file = "$desktop/install.ps1"
(New-Object net.webclient).DownloadFile($url,$file)
Unblock-File .\install.ps1
start powershell "$file -password password -noWait -noGui -noChecks"
"""


def control_guest(vm_uuid, args):
"""Run a 'VBoxManage guestcontrol' command providing the username and password.
Args:
vm_uuid: VM UUID
args: list of arguments starting with the guestcontrol sub-command
"""
run_vboxmanage(["guestcontrol", vm_uuid, f"--username={GUEST_USERNAME}", f"--password={GUEST_PASSWORD}"] + args)


vm_uuid = get_vm_uuid(VM_NAME)
if not vm_uuid:
print(f'❌ ERROR: "{VM_NAME}" not found')
exit()

print(f'\nGetting the installation VM "{VM_NAME}" {vm_uuid} ready...\n')

restore_snapshot(vm_uuid, BASE_SNAPSHOT)
ensure_vm_running(vm_uuid)

control_guest(vm_uuid, ["copyto", "--recursive", f"--target-directory={REQUIRED_FILES_DEST}", REQUIRED_FILES_DIR])
print(f"VM {vm_uuid} 📁 Copied required files in: {REQUIRED_FILES_DIR}")


control_guest(vm_uuid, ["run", "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", INSTALLATION_COMMAND])

print(f"\nVM {vm_uuid} ✅ FLARE-VM is being installed... it will take some time,")
print(" Go for an 🍦 and enjoy FLARE-VM when you are back!")
33 changes: 9 additions & 24 deletions virtualbox/vbox-export-snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@
from datetime import datetime

import jsonschema
from vboxcommon import ensure_hostonlyif_exists, ensure_vm_running, ensure_vm_shutdown, run_vboxmanage
from vboxcommon import (
ensure_hostonlyif_exists,
ensure_vm_running,
ensure_vm_shutdown,
get_vm_uuid,
restore_snapshot,
run_vboxmanage,
)

DESCRIPTION = """Export one or more snapshots in the same VirtualBox VM as .ova, changing the network to a single Host-Only interface.
Generate a file with the SHA256 of the exported OVA(s)."""
Expand Down Expand Up @@ -67,19 +74,6 @@ def sha256_file(filename):
return hashlib.file_digest(f, "sha256").hexdigest()


def get_vm_uuid(vm_name):
"""Get the machine UUID for a given VM name using 'VBoxManage list vms'. Return None if not found."""
# regex VM name and extract the GUID
# Example of `VBoxManage list vms` output:
# "FLARE-VM.testing" {b76d628b-737f-40a3-9a16-c5f66ad2cfcc}
# "FLARE-VM" {a23c0c37-2062-4cf0-882b-9e9747dd33b6}
vms_info = run_vboxmanage(["list", "vms"])

match = re.search(rf'^"{vm_name}" (?P<uuid>\{{.*?\}})', vms_info, flags=re.M)
if match:
return match.group("uuid")


def set_network_to_hostonly(vm_uuid):
"""Set the NIC 1 to hostonly and disable the rest."""
# VM must be shutdown before changing the adapters
Expand Down Expand Up @@ -121,21 +115,12 @@ def set_network_to_hostonly(vm_uuid):
print(f"VM {vm_uuid} ⚙️ network set to single hostonly adapter")


def restore_snapshot(vm_uuid, snapshot_name):
"""Restore a given snapshot in the given VM."""
# VM must be shutdown before restoring snapshot
ensure_vm_shutdown(vm_uuid)

run_vboxmanage(["snapshot", vm_uuid, "restore", snapshot_name])
print(f'VM {vm_uuid} ✨ restored snapshot "{snapshot_name}"')


def export_snapshots(vm_name, exported_vm_name, snapshots, export_dir_name):
date = datetime.today().strftime("%Y%m%d")

vm_uuid = get_vm_uuid(vm_name)
if not vm_uuid:
print(f'ERROR: "{vm_name}" not found')
print(f'ERROR: "{vm_name}" not found')
exit()

print(f'\nExporting snapshots from "{vm_name}" {vm_uuid}')
Expand Down
31 changes: 28 additions & 3 deletions virtualbox/vboxcommon.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@


def format_arg(arg):
"""Add quotes to the string arg if it contains spaces."""
if " " in arg:
return f"'{arg}'"
"""Add quotes to the string arg if it contains special characters like spaces."""
if any(c in arg for c in (" ", "\\", "/")):
if "'" not in arg:
return f"'{arg}'"
if '"' not in arg:
return f'"{arg}"'
return arg


Expand Down Expand Up @@ -78,6 +81,19 @@ def ensure_hostonlyif_exists():
return hostonlyif_name


def get_vm_uuid(vm_name):
"""Get the machine UUID for a given VM name using 'VBoxManage list vms'. Return None if not found."""
# regex VM name and extract the GUID
# Example of `VBoxManage list vms` output:
# "FLARE-VM.testing" {b76d628b-737f-40a3-9a16-c5f66ad2cfcc}
# "FLARE-VM" {a23c0c37-2062-4cf0-882b-9e9747dd33b6}
vms_info = run_vboxmanage(["list", "vms"])

match = re.search(rf'^"{vm_name}" (?P<uuid>\{{.*?\}})', vms_info, flags=re.M)
if match:
return match.group("uuid")


def get_vm_state(vm_uuid):
"""Get the VM state using 'VBoxManage showvminfo'."""
# Example of `VBoxManage showvminfo <VM_UUID> --machinereadable` relevant output:
Expand Down Expand Up @@ -137,3 +153,12 @@ def ensure_vm_shutdown(vm_uuid):

if not wait_until_vm_state(vm_uuid, "poweroff"):
raise RuntimeError(f"Unable to shutdown VM {vm_uuid}.")


def restore_snapshot(vm_uuid, snapshot_name):
"""Restore a given snapshot in the given VM."""
# VM must be shutdown before restoring snapshot
ensure_vm_shutdown(vm_uuid)

run_vboxmanage(["snapshot", vm_uuid, "restore", snapshot_name])
print(f'VM {vm_uuid} ✨ restored snapshot "{snapshot_name}"')

0 comments on commit 858c908

Please sign in to comment.