What the encryptor actually does, why some VMDKs survive, and why recovery is not just a header repair.
Research and analysis by Saeed Yavari
BabyK is also commonly referenced as Babuk or Babak. During an incident involving several VMware ESXi hosts, I recovered the shell script used to coordinate the attack, two builds of the encryptor from different servers, a working decryptor issued for a third server, and several encrypted files. I wanted to answer a practical question: what had actually happened to the VMDKs? Were they fully encrypted, partially encrypted, or simply renamed?
The answer mattered. Some recovery methods that worked against older ESXi ransomware incidents rely on rebuilding a descriptor, restoring a GPT header, or removing a malicious suffix. Those methods are useful only when the bytes underneath are still intact. In this sample, the situation was more complicated: the encryptor has failure paths, so some files can remain untouched or only be renamed, while files that were processed successfully have their first 520 MiB transformed with a per-file stream key.
Important
The findings below apply to the exact BabyK samples recovered from this incident. Key values, per-file footers, victim identifiers, and attacker contact details have been intentionally withheld.
Note
This repository is a GitHub-formatted publication of the research report. It contains no ransomware sample, decryptor, recovery script, executable tool or private key.
- 1. The Two Files That Drove the Attack
- 2. How the Encryptor Selected Files
- 3. What Changed Between Two Victim-Specific Encryptors
- 4. Per-File Key Generation
- 5. Exactly How Much Data Was Transformed
- 6. The Final 32 Bytes
- 7. What the Decryptor Confirmed
- 8. Why
.babykDoes Not Always Mean Encrypted - 9. Why GPT and Descriptor Repair Were Not Enough
- 10. What Happened to ext4
- 11. Why Copying 520 MiB from a Similar VM Does Not Restore the Victim
- 12. Recovery Decisions
- 13. One Remaining Research Lead
- Indicators from the Analyzed Samples
The execution chain revolved around two files placed under /var/run:
/var/run/run.sh— orchestration script/var/run/backup— ELF64 encryptor
The shell script handled timing, VM termination, repeated encryption passes, and disruption of the ESXi recovery path. The binary named backup performed the recursive file discovery and cryptographic work.
-
Waited 300 seconds before beginning the destructive sequence.
-
Disabled the ESXi execInstalledOnly control so that an uninstalled binary could run.
-
Changed /var/run/backup to mode 777.
-
Enumerated running VM worlds while excluding names containing vcls, vcenter, or vcsa.
-
Issued force and hard VM process kills, waited, and repeated the process.
-
Executed /var/run/backup against /vmfs/volumes/ and redirected output to files under /tmp.
-
Repeated VM termination and encryption several times, then changed the ESXi welcome message to the ransom notice.
-
After another delay, killed vmx processes with SIGKILL, removed the vmware-fdm VIB, and ran the encryptor again.
esxcli system settings advanced set -o /User/execInstalledOnly -i 0 chmod 777 /var/run/backup /var/run/backup /vmfs/volumes/ > /tmp/script_output_1.txt 2>&1 esxcli software vib remove -n vmware-fdm
The repeated passes are significant. A VMDK can be unavailable during an early pass because a VM is still running, a handle is open, or an allocation or read fails. The script keeps killing processes and running the encryptor again to improve coverage. It also overwrites script_output_2.txt on later passes, which removes part of its own earlier execution history.
Figure 1. Execution chain reconstructed from /var/run/run.sh.
The encryptor is a stripped, 64-bit x86 ELF. It walks directories recursively, creates a ransom note named How To Restore Your Files.txt, and uses a thread pool sized from the host CPU count. In the recovered sample, it looked for file names containing:
-
.log
-
.vmdk
-
.vmem
-
.vswp
-
.vmsn
A name already containing .babyk was skipped. The .vmdk substring catches both the small text descriptor and the large -flat.vmdk extent. That distinction matters: a small descriptor can be transformed from beginning to end, while a large flat extent is only processed through a fixed prefix.
I compared two encryptors recovered from two different ESXi servers. Both were exactly 70,720 bytes long and both carried the same GNU Build ID:
66fc99c8a97d44e0f14f86dfa915776fab535f47
Despite different SHA-256 hashes, only 83 byte positions differed across the files. Fifty-one changed bytes were associated with the victim-specific decrypt ID string and its padding. The other difference was one contiguous 32-byte field at file offsets 0x103C0 through 0x103DF.
Note
The values of both 32-byte fields are redacted. The comparison established their role without publishing either value.
The changed 32-byte field is the victim's master X25519 public key. The code around it is identical. The shared Build ID, combined with victim-specific data changes, strongly suggests that the operator built one executable and patched the decrypt ID and public-key field for each target.
The decrypt ID is therefore best understood as a lookup label, not the encryption secret. I also tested several obvious deterministic candidates based on hashing the decrypt ID with SHA-256, SHA-512, SHA-3, and BLAKE2. None reproduced the observed key material.
Every file gets its own ephemeral X25519 scalar. The encryptor opens /dev/urandom and requests exactly 32 bytes. It then applies the standard X25519 clamping operations:
scalar[0] &= 248;
scalar[31] &= 127;
scalar[31] |= 64;
From that scalar, the program calculates two values. First, it multiplies the scalar by Curve25519's base point 9 to obtain the ephemeral public key. Second, it combines the same scalar with the victim-specific master public key embedded in the encryptor to produce a shared secret.
ephemeral_public = X25519(ephemeral_private, base_point_9)
shared_secret = X25519(ephemeral_private, master_public)
stream_key = SHA256(shared_secret)
The SHA-256 output becomes the key for Sosemanuk with a zero IV. Sosemanuk generates a keystream, which is XORed with the file data. Once the shared secret and public footer have been generated, the encryptor clears the temporary private scalar from its stack buffer.
Figure 2. Per-file X25519 agreement and stream-key derivation. All actual key values are redacted.
The I/O buffer is 0xA00000 bytes, or 10 MiB. The loop checks its running byte count against 0x1FFFFFFF, but it checks after each chunk. After 510 MiB has been processed, one more 10 MiB read occurs. The total reaches 520 MiB and the loop stops.
| Field | Observed value |
|---|---|
| First transformed byte | 0x00000000 |
| Last transformed byte | 0x207FFFFF / 545,259,519 |
| First untouched byte | 0x20800000 / 545,259,520 |
| Total transformed prefix | 520 MiB / 545,259,520 bytes |
| I/O chunk | 0xA00000 / 10 MiB |
For a file smaller than 520 MiB, the final read reaches EOF and the entire file is transformed. This explains why a VMDK descriptor can become unreadable while only the first portion of a large flat extent is affected.
Figure 3. Byte-level map of a large encrypted VMDK.
After processing a file, the encryptor appends the ephemeral public key to the end. This is a 32-byte public value generated specifically for that file. It is not a hash, a marker, or the private key.
original size : N
footer start : N
footer end : N + 31
new file size: N + 32
One 80 GiB flat VMDK from the incident illustrates the layout. Its original size was 85,899,345,920 bytes, or 0x1400000000. The encrypted file was 85,899,345,952 bytes, or 0x1400000020. The footer occupied offsets 0x1400000000 through 0x140000001F.
Caution
Do not discard the footer. The matching decryptor needs the original 32 bytes from that exact file. Renaming a file to .babyk does not recreate this cryptographic input.
The decryptor issued for a third server was a stripped 54,400-byte ELF. It contained a fixed 32-byte master private key in its data section. I am deliberately not publishing the value or its exact extraction location.
The decryptor seeks to 32 bytes before EOF, reads the per-file public footer, and combines it with its embedded master private key:
shared_secret = X25519(master_private, ephemeral_public_footer)
stream_key = SHA256(shared_secret)
X25519 gives both sides the same shared secret. The decryptor then creates the same Sosemanuk keystream and XORs it over the same prefix. It truncates the 32-byte footer and removes the .babyk suffix.
To validate the analysis, I implemented an independent decryptor from the recovered algorithm. On files belonging to the same decrypt ID as the supplied decryptor, my implementation produced byte-for-byte the same output as the attacker's tool. The recovered content was correct.
Important
The decryptor for the third server did not correspond to either of the two compared encryptors. That was expected: each server carried a different master key pair.
The encryptor is not transaction-safe. It does not prove that every requested read or write completed before treating a file as processed. Most notably, the helper that reads /dev/urandom never checks the return value from fread. There are also paths where allocation or I/O failure can leave a file unmodified or only partially processed before later rename logic is reached.
That means the suffix alone is not reliable evidence. On some hosts, some files may still contain their original bytes even though their names end in .babyk. This is consistent with the attacker's decision to run the binary several times.
-
Clone the VM directory. Preserve the original names, sizes, hashes, and the final 32 bytes of every candidate file.
-
Open the small VMDK descriptor as text. A healthy descriptor normally starts with
# Disk DescriptorFileand contains a readable extent line. -
Inspect the flat extent independently. Look for expected disk signatures and compare the size with the descriptor. A healthy descriptor alone does not prove the flat file is healthy.
-
On the copy, remove only the
.babyksuffix and runvmkfstools -eagainst the descriptor. -
If the descriptor and flat extent both pass validation, start the VM in an isolated network first. Do not reconnect it directly to production.
When both files are still intact and were only renamed, removing the suffixes can be enough to bring the VM back. I have seen this class of failure in incomplete ransomware passes. But when the descriptor is unreadable or the first 520 MiB of the flat extent is demonstrably transformed, renaming alone will not recover the machine.
Figure 4. A conservative decision path for distinguishing rename-only cases from encrypted files.
A normal GPT disk keeps the protective MBR at LBA 0, the primary GPT header at LBA 1, and the primary partition-entry array beginning at LBA 2. The first 34 sectors occupy 17,408 bytes, offsets 0x0000 through 0x43FF. All of those bytes are inside the transformed 520 MiB prefix.
The backup GPT is stored at the opposite end of the disk. On the large VMDKs I examined, that region was outside the encrypted prefix and remained usable. I rebuilt the primary GPT from the backup copy, restored the original flat-file size by removing the saved footer, rebuilt the text descriptor from the VMX and extent size, and checked the chain with:
vmkfstools -e VMNAME.vmdk
Disk chain is consistent.
That result only means VMware can follow the descriptor and extent chain. It does not say that the guest filesystem is intact. The partition map was repaired, but the bytes inside the first 520 MiB were still ciphertext.
The primary ext4 superblock was inside the affected area. A backup superblock at filesystem block 163840 was still readable. With a 4,096-byte block size, that copy sits approximately 640 MiB from the start of the filesystem and around 642 MiB from the beginning of this particular disk image.
Figure 5. debugfs reading the surviving backup superblock at block 163840.
The surviving superblock reported 32,768 blocks per group. At 4,096 bytes per block, each ext4 block group spans 128 MiB. The ransomware prefix therefore covered roughly the first four groups, including critical early metadata such as the root inode, root directory, journal, initial bitmaps, and portions of the first inode tables.
Figure 6. The root inode could not be read because its checksum no longer matched.
e2fsck could use later group metadata and surviving inodes to make the filesystem internally consistent. It rebuilt the journal and corrected group counts, but it could not recreate directory entries that had been replaced by ciphertext.
Figure 7. e2fsck regenerated the journal and modified the filesystem.
A later check completed without structural errors and reported many surviving files and blocks. After mounting, however, the root contained only lost+found. The file contents in later block groups could still exist, but the original path and filename relationships had been lost with the early directory metadata.
Figure 8. The final consistency check completed, but a clean fsck result did not restore the original directory tree.
A newly installed VM with the same operating system and partition sizes does not contain the victim's original metadata. Its UUID, journal, root inode, inode allocation maps, directory entries, and file extents belong to a different filesystem. Copying that prefix would replace ciphertext with valid but unrelated metadata.
It may produce a filesystem that looks more recognizable, but it cannot reconnect the victim's surviving blocks to their original names and paths. That approach only becomes meaningful when the donor prefix is a byte-for-byte backup of the same VM from before the incident.
The most important distinction is whether a file was actually transformed.
-
Rename-only case: preserve a clone, verify the descriptor and flat extent independently, remove the suffixes on the copy, validate the chain, and boot in isolation.
-
Successfully encrypted case: preserve the per-file footer and use the matching victim-specific decryptor or master private key.
-
Missing footer: the decryptor will treat the last 32 bytes of real disk data as a public key, derive the wrong stream, and damage the copy further.
-
No matching key: header reconstruction can make the disk visible, and file carving may recover later content, but it cannot reverse the transformed 520 MiB prefix.
-
Known-good backup or snapshot: restore from it instead of experimenting on the only copy.
Important
Older header-reset techniques still have value when an encryptor fails and leaves the bytes intact. Once this sample successfully transforms the first 520 MiB, there is no equivalent shortcut: recovery requires the correct cryptographic material or a genuine backup.
Because the urandom read count is not checked, I would extract and hash the final 32 bytes from every untouched .babyk file before running any decryptor. Duplicate footers on the same server would indicate repeated ephemeral public keys. With the same master public key, that would also mean repeated shared secrets and keystream reuse.
A duplicated footer does not reveal the master private key. It can, however, create a known-plaintext recovery opportunity if one file has a clean copy or a predictable format. This is one of the few realistic avenues worth testing before concluding that a keyless recovery is impossible.
Encryptor A SHA-256
f73a3193c8046018488501124af5c1a840c9a24e30f4b40869d8aa553bfc1f43
Encryptor B SHA-256
2e391087a213287b36f5fb46eea5c7794957553a2c77b4d28631b75142b1a01b
Decryptor SHA-256
ce19a7470ee2024d91fb1cf75f4d19fc3a395cc06510f2e50b8619a60bdaa8be
Key values, per-file public footers, victim decrypt IDs, IP addresses, and attacker contact details are intentionally not included in this report.
The easiest evidence to destroy in this incident is also the smallest: the 32-byte footer. Before renaming, truncating, rebuilding a descriptor, restoring GPT structures, or running fsck, preserve the original file and save that footer separately. Work only on a clone. A repair tool can make a damaged filesystem consistent, but it cannot decrypt bytes or recreate directory relationships that no longer exist.
If this research helped your incident response, forensic investigation, publication, presentation, or recovery work, please cite the version you used:
Yavari, S. (2026). BabyK (Babuk) VMware ESXi Ransomware Incident Analysis (Version 1.0.0). GitHub. https://github.com/isaeedyavari/babyk-esxi-ransomware-recovery
GitHub also provides APA and BibTeX formats through Cite this repository.
This report focuses on evidence observed directly in the analyzed BabyK incident samples. The following publications provide broader context about ransomware targeting VMware ESXi:
- SentinelLabs — Hypervisor Ransomware: Multiple Threat Actor Groups Hop on Leaked Babuk Code to Build ESXi Lockers
- VMware Threat Analysis Unit — ESXi-Targeting Ransomware: Tactics and Techniques, Part 2
- CISA and FBI — ESXiArgs Ransomware Virtual Machine Recovery Guidance
This repository contributes incident-specific observations, including the 520 MiB transformed prefix, the appended 32-byte per-file X25519 public footer, differences between two victim-specific encryptor builds, and failure paths that may leave files intact or only renamed.
This report does not attribute the initial intrusion to a specific CVE without supporting forensic evidence.
Except where otherwise noted, the original text, diagrams, and images in this repository are available under the Creative Commons Attribution 4.0 International License.
You may share and adapt this material for any purpose. Please credit Saeed Yavari, link to this repository and the license, and indicate whether changes were made.