Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 211 additions & 7 deletions dissect/hypervisor/descriptor/vbox.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,226 @@
from __future__ import annotations

from datetime import datetime
from functools import cached_property
from typing import TYPE_CHECKING, TextIO
from uuid import UUID

from defusedxml import ElementTree

if TYPE_CHECKING:
from collections.abc import Iterator
from xml.etree.ElementTree import Element

NS = "{http://www.virtualbox.org/}"


class VBox:
VBOX_XML_NAMESPACE = "{http://www.virtualbox.org/}"
"""VirtualBox XML descriptor parser.

Args:
fh: A file-like object of the VirtualBox XML descriptor.
"""

def __init__(self, fh: TextIO):
self._xml: Element = ElementTree.fromstring(fh.read())
if self._xml.tag != f"{NS}VirtualBox":
raise ValueError("Invalid VirtualBox XML descriptor: root element is not VirtualBox")

if (machine := self._xml.find(f"./{NS}Machine")) is None:
raise ValueError("Invalid VirtualBox XML descriptor: no Machine element found")

if machine.find(f"./{NS}Hardware") is None:
raise ValueError("Invalid VirtualBox XML descriptor: no Hardware element found")

self.machine = Machine(self, machine)

def __repr__(self) -> str:
return f"<VBox uuid={self.uuid} name={self.name}>"

@property
def uuid(self) -> UUID | None:
"""The VM UUID."""
return self.machine.uuid

@property
def name(self) -> str | None:
"""The VM name."""
return self.machine.name

@property
def media(self) -> dict[UUID, HardDisk]:
"""The media (disks) registry."""
return self.machine.media

@property
def hardware(self) -> Hardware:
"""The current machine hardware state."""
return self.machine.hardware

@property
def snapshots(self) -> dict[UUID, Snapshot]:
"""All snapshots."""
return self.machine.snapshots


class Machine:
def __init__(self, vbox: VBox, element: Element):
self.vbox = vbox
self.element = element

def __repr__(self) -> str:
return f"<Machine uuid={self.uuid} name={self.name}>"

@property
def uuid(self) -> UUID:
"""The machine UUID."""
return UUID(self.element.get("uuid").strip("{}"))

@property
def name(self) -> str:
"""The machine name."""
return self.element.get("name")

@property
def current_snapshot(self) -> UUID | None:
"""The current snapshot UUID."""
if (value := self.element.get("currentSnapshot")) is not None:
return UUID(value.strip("{}"))
return None

@cached_property
def media(self) -> dict[UUID, HardDisk]:
"""The media (disks) registry."""
result = {}

stack = [(None, element) for element in self.element.find(f"./{NS}MediaRegistry/{NS}HardDisks")]
while stack:
parent, element = stack.pop()
hdd = HardDisk(self, element, parent)
result[hdd.uuid] = hdd

stack.extend([(hdd, child) for child in element.findall(f"./{NS}HardDisk")])

return result

@cached_property
def hardware(self) -> Hardware:
"""The machine hardware state."""
return Hardware(self.vbox, self.element.find(f"./{NS}Hardware"))

@cached_property
def snapshots(self) -> dict[UUID, Snapshot]:
"""All snapshots."""
result = {}

if (element := self.element.find(f"./{NS}Snapshot")) is None:
return result

stack = [(None, element)]
while stack:
parent, element = stack.pop()
snapshot = Snapshot(self.vbox, element, parent)
result[snapshot.uuid] = snapshot

if (snapshots := element.find(f"./{NS}Snapshots")) is not None:
stack.extend([(snapshot, child) for child in list(snapshots)])

return result

@property
def parent(self) -> Snapshot | None:
if (uuid := self.current_snapshot) is not None:
return self.vbox.snapshots[uuid]
return None


class HardDisk:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it not be useful to also have the format of the disk available? Not for target-query, but for the package as its own entity

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed that a HardDisk can have an additional <Property name="CRYPT/KeyID"> and <Property name="CRYPT/KeyStore">. These exist when you tell virtualbox that it should encrypt a vm. It creates it for every associated disk in that case.

I think it might be useful to note down if the hard disk is encrypted. I can give you an example file if needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have an example, sure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<?xml version="1.0"?>
<!--
** DO NOT EDIT THIS FILE.
** If you make changes to this file while any VirtualBox related application
** is running, your changes will be overwritten later, without taking effect.
** Use VBoxManage or the VirtualBox Manager GUI to make changes.
**
** Written by VirtualBox 7.2.4 (r170995)
-->
<VirtualBox xmlns="http://www.virtualbox.org/" version="1.19-linux">
  <Machine uuid="{366f53dd-1710-434e-8021-1f91b0405b65}" name="encrypted test" OSType="Windows11_64" currentSnapshot="{cfbdc706-6561-44e8-8f3a-236df446b808}" snapshotFolder="Snapshots" lastStateChange="2026-01-20T13:01:05Z">
    <MediaRegistry>
      <HardDisks>
        <HardDisk uuid="{24cdb8e9-35d6-42f2-aa17-c2d78bf1e1de}" location="encrypted test.vdi" format="VDI" type="Normal">
          <Property name="CRYPT/KeyId" value="encrypted test"/>
          <Property name="CRYPT/KeyStore" value="U0NORQABQUVTLVhUUzI1Ni1QTEFJTjY0AAAAAAAAAAAAAAAAAABQQktERjItU0hB&#10;MjU2AAAAAAAAAAAAAAAAAAAAAAAAAEAAAACOEfHKuPU9MHJktr3UZAxmuL+epJsk&#10;SRfK3vhbPnb4USAAAABCoUSl+kb0+6OzG8N3wo16bru7UNsjHzIsB+OPBfZUPiBO&#10;AACa0Cbl1wJz09E5i8U10V3xElnY8iAIwEqSwVWpwFPiYsAJDgBAAAAAgcb2cMho&#10;wWkDpje3pQHrn62sH8oTyr4+wXaJoFUEreymEuIXYlnB+jeLjQDWEEIs9DUIXfiP&#10;PWw3Fph9h0a3lA=="/>
          <HardDisk uuid="{742e6d0f-8896-4aa6-97f4-e05d70e73029}" location="Snapshots/{742e6d0f-8896-4aa6-97f4-e05d70e73029}.vdi" format="VDI"/>
        </HardDisk>
        <HardDisk uuid="{1db0b9fe-36c2-44e8-9b7c-61fa5b6d1462}" location="encrypted test_1.vdi" format="VDI" type="Normal"/>
      </HardDisks>
    </MediaRegistry>
    <Snapshot uuid="{cfbdc706-6561-44e8-8f3a-236df446b808}" name="Encryption" timeStamp="2026-01-20T13:01:05Z">
      <Hardware>
        <Memory RAMSize="4096"/>
        <HID Pointing="USBTablet"/>
        <Display controller="VBoxSVGA" VRAMSize="128"/>
        <Firmware type="EFI"/>
        <BIOS>
          <IOAPIC enabled="true"/>
          <NVRAM path="Snapshots/2026-01-20T13-01-05-717390000Z.nvram"/>
          <SmbiosUuidLittleEndian enabled="true"/>
          <AutoSerialNumGen enabled="true"/>
        </BIOS>
        <TrustedPlatformModule type="v2_0" location=""/>
        <USB>
          <Controllers>
            <Controller name="XHCI" type="XHCI"/>
          </Controllers>
        </USB>
        <Network>
          <Adapter slot="0" enabled="true" MACAddress="0800274C9CFB" type="82540EM">
            <NAT localhost-reachable="true"/>
          </Adapter>
        </Network>
        <AudioAdapter controller="HDA" useDefault="true" driver="ALSA" enabled="true" enabledOut="true"/>
        <Clipboard/>
        <StorageControllers>
          <StorageController name="SATA" type="AHCI" PortCount="2" useHostIOCache="false" Bootable="true" IDE0MasterEmulationPort="0" IDE0SlaveEmulationPort="1" IDE1MasterEmulationPort="2" IDE1SlaveEmulationPort="3">
            <AttachedDevice type="HardDisk" hotpluggable="false" port="0" device="0">
              <Image uuid="{24cdb8e9-35d6-42f2-aa17-c2d78bf1e1de}"/>
            </AttachedDevice>
            <AttachedDevice passthrough="false" type="DVD" hotpluggable="false" port="1" device="0"/>
          </StorageController>
        </StorageControllers>
        <CPU count="2">
          <HardwareVirtExLargePages enabled="false"/>
          <PAE enabled="false"/>
          <LongMode enabled="true"/>
        </CPU>
      </Hardware>
    </Snapshot>
    <Hardware>
      <Memory RAMSize="4096"/>
      <HID Pointing="USBTablet"/>
      <Display controller="VBoxSVGA" VRAMSize="128"/>
      <Firmware type="EFI"/>
      <BIOS>
        <IOAPIC enabled="true"/>
        <SmbiosUuidLittleEndian enabled="true"/>
        <AutoSerialNumGen enabled="true"/>
      </BIOS>
      <TrustedPlatformModule type="v2_0" location=""/>
      <USB>
        <Controllers>
          <Controller name="XHCI" type="XHCI"/>
        </Controllers>
      </USB>
      <Network>
        <Adapter slot="0" enabled="true" MACAddress="0800274C9CFB" type="82540EM">
          <NAT localhost-reachable="true"/>
        </Adapter>
      </Network>
      <AudioAdapter controller="HDA" useDefault="true" driver="ALSA" enabled="true" enabledOut="true"/>
      <Clipboard/>
      <StorageControllers>
        <StorageController name="SATA" type="AHCI" PortCount="3" useHostIOCache="false" Bootable="true" IDE0MasterEmulationPort="0" IDE0SlaveEmulationPort="1" IDE1MasterEmulationPort="2" IDE1SlaveEmulationPort="3">
          <AttachedDevice type="HardDisk" hotpluggable="false" port="0" device="0">
            <Image uuid="{742e6d0f-8896-4aa6-97f4-e05d70e73029}"/>
          </AttachedDevice>
          <AttachedDevice passthrough="false" type="DVD" hotpluggable="false" port="1" device="0"/>
          <AttachedDevice type="HardDisk" hotpluggable="false" port="2" device="0">
            <Image uuid="{1db0b9fe-36c2-44e8-9b7c-61fa5b6d1462}"/>
          </AttachedDevice>
        </StorageController>
      </StorageControllers>
      <CPU count="2">
        <HardwareVirtExLargePages enabled="false"/>
        <PAE enabled="false"/>
        <LongMode enabled="true"/>
      </CPU>
    </Hardware>
  </Machine>
</VirtualBox>

From how I interpreted it, the encryption holds for all parent disks and their children.
In the case above I added another disk after enabling the "Encrypt all disks" tho it seems it doesn't add a key for that one. Whereas it does add the properties if you enable encryption with multiple disks attached to the vm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case above I added another disk after enabling the "Encrypt all disks" tho it seems it doesn't add a key for that one. Whereas it does add the properties if you enable encryption with multiple disks attached to the vm.

But the second disk is encrypted? Presumably using the same key as the other one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the second disk is encrypted? Presumably using the same key as the other one?

The second disk is not encrypted. Had to verify it to be sure. Tho the wording "encrypt all disks" made me assume otherwise at first.

def __init__(self, vbox: VBox, element: Element, parent: HardDisk | None = None):
self.vbox = vbox
self.element = element
self.parent = parent

def __repr__(self) -> str:
return f"<HardDisk uuid={self.uuid} location={self.location}>"

@property
def uuid(self) -> UUID:
"""The disk UUID."""
return UUID(self.element.get("uuid").strip("{}"))

@property
def location(self) -> str:
"""The disk location."""
return self.element.get("location")

@property
def type(self) -> str | None:
"""The disk type."""
return self.element.get("type")

@property
def format(self) -> str:
"""The disk format."""
return self.element.get("format")

@cached_property
def properties(self) -> dict[str, str]:
"""The disk properties."""
return {prop.get("name"): prop.get("value") for prop in self.element.findall(f"./{NS}Property")}

@property
def is_encrypted(self) -> bool:
"""Whether the disk is encrypted."""
disk = self
while disk is not None:
if "CRYPT/KeyId" in disk.properties or "CRYPT/KeyStore" in disk.properties:
return True
disk = disk.parent

return False


class Snapshot:
def __init__(self, vbox: VBox, element: Element, parent: Snapshot | Machine | None = None):
self.vbox = vbox
self.element = element
self.parent = parent

def __repr__(self) -> str:
return f"<Snapshot uuid={self.uuid} name={self.name}>"

@property
def uuid(self) -> UUID:
"""The snapshot UUID."""
return UUID(self.element.get("uuid").strip("{}"))

@property
def name(self) -> str:
"""The snapshot name."""
return self.element.get("name")

@property
def ts(self) -> datetime:
"""The snapshot timestamp."""
return datetime.strptime(self.element.get("timeStamp"), "%Y-%m-%dT%H:%M:%S%z")

@cached_property
def hardware(self) -> Hardware:
"""The snapshot hardware state."""
return Hardware(self.vbox, self.element.find(f"./{NS}Hardware"))


class Hardware:
def __init__(self, vbox: VBox, element: Element):
self.vbox = vbox
self.element = element

def __repr__(self) -> str:
return f"<Hardware disks={len(self.disks)}>"

def disks(self) -> Iterator[str]:
for hdd_elem in self._xml.findall(f".//{self.VBOX_XML_NAMESPACE}HardDisk[@location][@type='Normal']"):
# Allow format specifier to be case-insensitive (i.e. VDI, vdi)
if (format := hdd_elem.get("format")) and format.lower() == "vdi":
yield hdd_elem.attrib["location"]
@property
def disks(self) -> list[HardDisk]:
"""All attached hard disks."""
images = self.element.findall(
f"./{NS}StorageControllers/{NS}StorageController/{NS}AttachedDevice[@type='HardDisk']/{NS}Image"
)
return [self.vbox.media[UUID(image.get("uuid").strip("{}"))] for image in images]
Loading