vdiread: Reading VirtualBox VDI Images Without VirtualBox
I got to do some data recovery work with an old HDD that used to be in the office machine of a small nonprofit. The PC was decommissioned, but the files needed going through. Turned out the HDD had a few huge (30 GB+) VirtualBox .vdi disk images as its main data — an Inception-style data recovery indeed, disks within disks!
Unfortunately, macOS on Apple Silicon proved very resistant to any easy attempt to get the drives read. After some hours of installing VirtualBox and QEMU and trying to get the things running, I decided to abort that route. Maybe modern AI could write a standalone tool that reads the image file and the filesystem inside it, and provides simple file listing and extraction?
Turns out it very much can — I was able to basically single-prompt the vdiread tool into existence. It is a small read-only CLI that opens a .vdi file, finds the NTFS partition inside it and lets you list,
stat and copy out files. No VirtualBox, no mounting, no root. And, compared to the earlier options, I did not have to use some VDI tool to extract a 30 GB compressed file into a 70 GB uncompressed one and then try to mount it. I could work straight within the original image.
Standing on the Shoulders of Dissect
The reason this is a small project rather than a large one is Dissect, the pure-Python forensics toolkit from Fox-IT. It already has the three pieces needed here, each as a separate package:
dissect.hypervisor— reads the VDI container, including sparse dynamic imagesdissect.volume— finds the MBR/GPT partitionsdissect.ntfs— walks the NTFS filesystem
All three are pure Python, so uv sync is the whole installation story —
no kernel extensions, no ntfs-3g, nothing to compile. vdiread stacks them
into a CLI and adds the bits Dissect deliberately leaves to the caller:
partition auto-selection, path handling and copy semantics.
Usage
$ vdiread --help
usage: vdiread [-h] {inspect,partitions,ls,stat,cp,walk} ...
Read and extract files from VirtualBox VDI images
positional arguments:
{inspect,partitions,ls,stat,cp,walk}
inspect Inspect the VDI container and detected filesystems
partitions List partitions inside the image
ls List files from the guest filesystem
stat Show metadata for one guest path
cp Copy a file or directory out of the guest filesystem
walk Recursively list a guest path
inspect is the "what am I even looking at" command — it prints the VDI
format and type, virtual size, block size, how many blocks are actually
allocated, and the partition table with detected filesystems and volume names.
The rest work like their Unix namesakes, with the image as the first argument and guest paths after it:
vdiread ls XPmachine2.vdi '/Documents and Settings/User'
vdiread stat XPmachine2.vdi /boot.ini
vdiread cp XPmachine2.vdi /boot.ini ./boot.ini
vdiread cp XPmachine2.vdi '/Documents and Settings/User/My Documents' ./MyDocs -r
Guest paths accept either / or \, which saves a lot of shell quoting pain
when you are copy-pasting Windows paths.
Taking an Inventory First
The command I ended up using most is walk, especially with --tsv:
vdiread walk XPmachine2.vdi '/Documents and Settings/User' --tsv user_contents.tsv
That writes a tab-separated file with path, resolved_path, type, size,
resident, segment and name columns. Rummaging through a decades-old user
profile is much nicer in a spreadsheet than through repeated ls calls —
sort by size, filter by extension, then go back and cp only what you actually
want.
Design Notes
A few decisions worth mentioning:
- Read-only, always. The image is never opened for writing. The worst thing a bug can do is give you wrong data, not eat the only copy of the data you are trying to recover.
- Fail loudly. Unsupported NTFS cases raise a specific error rather than guessing. There is a small hierarchy of error types — container, partition, filesystem, unsupported feature — so the CLI can say which layer gave up.
- Auto-select the obvious. If there is exactly one NTFS partition it is
picked automatically; with several you pass
--partition <n>.
It was built and tested against a VDI v1.1 dynamic image with an MBR partition table and a single NTFS partition — the classic Windows XP-era layout. Other combinations may well work, since Dissect handles them, but that is the one I actually verified.
Requires Python 3.13+ and uv:
git clone https://github.com/jokkebk/vdiread.git
cd vdiread
uv sync
uv run vdiread inspect yourimage.vdi
Source: github.com/jokkebk/vdiread