Dutch
English
reverse engineering
kernel-modus
ring0
hvci
exploitation
warbird
windows internals

Analyse van Microsoft Warbird: Microsofts kernel-mode dynamic packer

GetRektBoy724
17 October, 2025

Introductie

Microsoft Warbird, ook wel Warbird genoemd, is een obfuscation‑framework of packer ontwikkeld door Microsoft om gevoelige Windows internals te beschermen, waaronder licensing, DRM en kern‑security features zoals Code Integrity (CI). Dit codebeschermingssysteem is in Windows ingebouwd om het reversen van sleutelcomponenten zoals ci.dll, clipsp.sys en peauth.sys veel moeilijker te maken. Het versleutelt en ontsleutelt kernel‑mode code dynamisch tijdens runtime, waardoor statische analyse van delen van de software onmogelijk wordt. Nog interessanter is dat Warbird zelfs werkt op systemen met Hypervisor‑Enforced Code Integrity (HVCI) en Virtualization‑Based Security (VBS), waar uitvoering van dynamic code in de kernel (kernelgeheugen dat kan wisselen tussen writable en executable en omgekeerd) onmogelijk zou moeten zijn. Dit onderzoek bekijkt hoe Warbird kernelcode versleutelt en ontsleutelt en hoe het HVCI en VBS “bypasst”. Aan het einde van dit onderzoek hoop ik ten minste één van deze 2 dingen te bereiken:

  1. Uitvinden hoe Microsoft zijn eigen regels doorbreekt door dynamic code (Warbird) toe te staan in een door HVCI beschermde kernel
  2. Uitvinden hoe ik mijn eigen dynamic code kan uitvoeren op de VTL0‑kernel met HVCI ingeschakeld

In dit onderzoek komen alle binaries die geanalyseerd worden uit Windows 11 versie 24H2 (build 26100.6584).

  1. clipsp.sys (v10.0.26100.5074) SHA1 : e5483ceec03e5baa203c12e0d3749e8f66bb5fba
  2. ntoskrnl.exe (v10.0.26100.6584) SHA1 : 16f05adb58478bcfc5e8773dc6ba30a040c2340d
  3. securekernel.exe (v10.0.26100.5074) SHA1 : 0b6d2ea1c1d996370fc6924ae5cf939a220ad160
  4. skci.dll (v10.0.26100.5074) SHA1 : 6d4ae8bf308a702bdb9723c22c38040bba7088b9

Disclaimer : Dit is mijn eerste diepgaande reverse‑engineering. Verwacht niet direct topkwaliteit. Reken op latere revisies.

clipsp.sys

Onze analyse start bij clipsp.sys, een kernel‑driver die onderdeel is van de Windows Client Licensing Service. Deze driver is beschermd door Warbird. Voordat we de inner workings van Warbird in clipsp.sys gaan reverse‑engineeren, kijken we eerst naar de “buitenkant” van de PE zelf met PE-bear. Als we de sections van de PE‑binary bekijken, zien we al aanwijzingen/signatures voor de aanwezigheid van Warbird. Er zijn enkele opmerkelijke sections die niet gebruikelijk zijn bij normale PE’s, en kijk naar hun naam, PAGEwx? wx betekent writable executable? Erg interessant.

image-8.png

Vervolgens beginnen we met het reverse‑engineeren van de Warbird‑routines in clipsp.sys. Omdat we geen symbols voor deze driver hebben, starten we onze analyse bij de geëxporteerde routine ClipSpInitialize.

image.png

Hier zien we dat eerst wordt gecontroleerd of de WarbirdMutex is geïnitialiseerd; zo niet, dan gebeurt dat via KeInitializeEvent. Daarna probeert het de Warbird‑packed sections PAGEwx1 en PAGEwx3 te ontsleutelen, voordat het code uitvoert die in de PAGEwx1‑section staat. En na het uitvoeren van de functie in PAGEwx1 worden de sections PAGEwx1 en PAGEwx3 opnieuw versleuteld. We focussen ons op de functies WarbirdDecryptSection en WarbirdReencryptSection.

image-1.png

image-2.png

Zoals je ziet zijn deze twee functies eigenlijk gewoon wrapper‑functies voor dezelfde functie, WarbirdEncryptDecryptSection. Deze twee functies locken de WarbirdMutex en controleren de decryption count die is opgeslagen in de DecryptionData2‑structuur. Als de decryption count één is, dan her‑versleutelt WarbirdEncryptDecryptSection; en als de decryption count 0 is, dan ontsleutelt WarbirdEncryptDecryptSection. Dit is een manier om Warbird‑encryptie en ‑decryptie te laten werken in een multithreaded situatie (waar meerdere threads de instructies in de Warbird‑packed sections gebruiken en tegelijk proberen te decrypten of opnieuw te encrypten). Laten we nu bekijken hoe WarbirdEncryptDecryptSection werkt.

image-3.png

Eerst initialiseert het een structuur die ik PAGEWX_PREPARATION_INFO noem, met informatie zoals het PAGEwx‑nummer en de index, MDL’s die zijn aangemaakt tijdens het encrypten/decrypten van de section, de huidige operatie (encrypt of decrypt), belangrijke pointers, enzovoort. Daarna roept WarbirdEncryptDecryptSection WarbirdPrepareSectionForModification aan; deze functie maakt de main MDL voor de versleutelde PAGEwx‑section en gebruikt vervolgens MmChangeImageProtection om de versleutelde PAGEwx‑section writable (RW) te maken. MmChangeImageProtection krijgt vier parameters: de eerste parameter is de main MDL van PAGEwx, de tweede parameter is een hash (of in elk geval een gedeeltelijke, later meer), de derde parameter is de totale grootte van de aangeleverde hash, en de vierde is de protection flag. MmChangeImageProtection is een ongedocumenteerde functie die, voor zover ik kan zien, alleen door clipsp.sys wordt geïmporteerd, dus we kunnen afleiden dat Microsoft deze functie specifiek voor kernel‑mode Warbird heeft gemaakt. We gaan later in deze analyse dieper in op MmChangeImageProtection.

image-4.png

Na het succesvol voorbereiden van de PAGEwx‑section voor modificatie gaat WarbirdEncryptDecryptSection verder met een encryptie/decryptie‑loop met een door hen geïmplementeerde Feistel‑cipher. Elke iteratie begint met het aanmaken van een writable MDL‑mapping met een specifieke start‑VA uit Decryption Data 1 van de PAGEwx‑section, waarna de uitvoering wordt doorgegeven aan WarbirdFeistelEncrypt of WarbirdFeistelDecrypt. Ik ga hier niet in op de details van hun Feistel‑cipher‑implementatie.

image-5.png

En tot slot zijn we aan het einde van de Warbird‑encryptie/decryptie‑routine. Eerst worden overige writable MDL‑mappings opgeruimd die zijn gebruikt voor de Feistel‑operatie, en daarna wordt WarbirdFinishSectionModification aangeroepen. Dit is de functie die in de decryptie‑route de section van writable naar executable verandert.

image-6.png

Hier zien we wat WarbirdFinishSectionModification doet. Als het om de encryptie‑route gaat, probeert het de section te locken en direct daarna weer te unlocken. Eerlijk gezegd heb ik geen idee waarom ze dit doen, en het is nog vreemder dat ze één keer locken en twee keer unlocken. Hoe dan ook, laten we ons richten op de decryptie‑route. Daar zien we dat MmChangeImageProtection opnieuw wordt aangeroepen, maar het verschil tussen deze call en de eerdere call in WarbirdPrepareSectionForModification is dat de vierde parameter nu 1 is in plaats van 2. Ik denk dat dit aangeeft dat de bescherming naar executable moet worden gezet. En na de MmChangeImageProtection‑call wordt de PAGEwx main MDL opgeruimd en wordt de PAGEWX_PREPARATION_INFO‑data schoongemaakt.

image-7.png

In het volgende deel van de analyse duiken we dieper in MmChangeImageProtection, hoe het werkt en hoe het samenwerkt met VTL1.

ntoskrnl.exe - MmChangeImageProtection

Zoals eerder genoemd is MmChangeImageProtection een ongedocumenteerde functie die alleen door de interne Warbird‑routine wordt gebruikt. Het is de meest cruciale functie in de Warbird‑flow en heeft als taak geheugenbescherming te wijzigen, waarmee dynamic code in de kernel mogelijk wordt gemaakt. Zonder MmChangeImageProtection zou het hele idee van kernel‑mode Warbird‑packed binaries niet mogelijk zijn. Zoals we eerder zagen, wordt MmChangeImageProtection twee keer gebruikt: in WarbirdPrepareSectionForModification, waar de geheugenbescherming naar writable wordt gezet zodat de PAGEwx‑section kan worden aangepast, en in WarbirdFinishSectionModification, waar de bescherming naar executable wordt gezet zodat de ontsleutelde instructies in de PAGEwx‑section kunnen worden aangeroepen en uitgevoerd.

MmChangeImageProtection neemt 4 parameters: de eerste is de target MDL, in dit geval de PAGEwx main MDL; de tweede is de pointer naar de ontsleutelde PAGEwx‑partiële (hierover later meer) SHA512‑hash/signatuur (die zich bevindt in de PAGEHrx‑section van clipsp.sys); de derde is de totale lengte van de hash die aan MmChangeImageProtection wordt aangeleverd; en de vierde is de protection flag, die bepaalt of de bescherming wordt gewijzigd naar writable of naar executable.

image-9.png

Aan het begin controleert MmChangeImageProtection de parameters, zorgt het ervoor dat de ontvangen MDL geldig is, en controleert het ook of de aangeleverde hash zich binnen het module‑geheugen bevindt waarvan MmChangeImageProtection de bescherming probeert te wijzigen. Dit betekent dat de hash die het ontvangt afkomstig moet zijn van de doelmodule zelf (in dit geval clipsp.sys). Vervolgens controleert deze functie of de module is ingepaged en in memory staat, en het doet extra controles op de onderliggende PFN’s uit de PAGEwx main MDL. Na dit alles komt het belangrijke deel: eerst wordt de geheugenbescherming van het doelsegment naar writable gezet, vervolgens wordt gekeken of de protection flag op executable staat. Als dat zo is, dan wordt gecontroleerd of HVCI (aangegeven door de StrongCodeGuarantees‑bit in MiFlag) is uitgeschakeld of dat VslValidateDynamicCodePages een NT_SUCCESS status teruggeeft. Er zijn nu 2 mogelijke uitkomsten afhankelijk van de return‑code van VslValidateDynamicCodePages. Als het alleen NT_SUCCESS teruggeeft en niet 0x12c, dan betekent dat dat de validiteitscheck op de dynamic code is mislukt en wordt de geheugenbescherming alleen op VTL0‑niveau gewijzigd. Met andere woorden: de VTL0 PTE wordt als executable gemarkeerd, maar bij uitvoering krijg je nog steeds een access violation, omdat de VTL1 EPTE nog writable staat. Als VslValidateDynamicCodePages 0x12c teruggeeft, betekent dat dat de validiteitscheck is gelukt, de dynamic code als geldig wordt beschouwd, en zowel de VTL0 PTE als de VTL1 EPTE op executable worden gezet.

image-10.png

Je vraagt je misschien af: Wat heeft dit te maken met de "partiële" hashes die je eerder noemde? Nou, met alles. VslValidateDynamicCodePages is de functie die MmChangeImageProtection gebruikt om te communiceren en samen te werken met de VTL1‑kernel, oftewel de Secure Kernel. Via deze weg worden de “partiële” hashes gebruikt om de dynamic code te valideren en te bepalen of de VTL1 EPTE executable wordt gezet of niet. In het volgende deel van de analyse duiken we dieper in VslValidateDynamicCodePages, welke data of parameters het aanneemt en hoe die naar VTL1 worden overgedragen.

ntoskrnl.exe - VslValidateDynamicCodePages

VslValidateDynamicCodePages is een andere ongedocumenteerde functie die door MmChangeImageProtection wordt gebruikt om de dynamic code te valideren en te interacteren met de VTL1‑kernel. De taak van deze functie is om onze data voor te bereiden voor overdracht naar de VTL1‑kernel en de vmcall (hypercall) instructie te emitten met het bijbehorende Secure Service Call Number, in dit geval 0x20, om de daadwerkelijke Secure Kernel‑functie SkmiValidateDynamicCodePages aan te roepen. Deze functie neemt 3 parameters. De eerste parameter is de target MDL, in deze context onze PAGEwx main MDL. De tweede parameter is een pointer naar onze partiële hashes, en de derde parameter is de totale aangeleverde hash‑grootte. Het eerste wat deze functie doet is een extra MDL maken voor onze target MDL. Voor zover ik heb gezien vereist elke data‑transfer naar VTL1 zo’n “second level” MDL, waarbij een tweede MDL wordt aangemaakt die de eerste MDL mapt, best cool toch? Nadat er een tweede MDL is gemaakt die onze target MDL mapt, wordt ook voor de hash‑buffer een MDL en nog een MDL voor de eerste MDL gemaakt via de functie VslpLockPagesForTransfer. Daarna verstuurt het een pointer naar de oorspronkelijke (first level) target MDL, de PFN van de oorspronkelijke target MDL die is afgeleid uit de second level target MDL, een pointer naar de first level hash MDL, de PFN van de first level MDL die is afgeleid uit de second level hash MDL, en de SSCN (secure service call number) die we willen aanroepen. Dit alles wordt verpakt in één datastructuur SKCALL. Vervolgens wordt VslpEnterIumSecureMode aangeroepen, de functie die de vmcall instructie uitzendt en onze doelfunctie aanroept waar de SSCN naar wijst.

image-11.png

In het volgende deel van onze analyse duiken we dieper in SkmiValidateDynamicCodePages, de Secure Kernel‑functie in VTL1 waar onze eerdere SSCN naartoe wijst.

securekernel.exe - SkmiValidateDynamicCodePages

Voordat we bij SkmiValidateDynamicCodePages aankomen, komen alle Secure Kernel‑aanroepen binnen bij IumInvokeSecureService. In deze functie staat een grote switch die elke SSCN afhandelt. Als we kijken naar de 0x20‑case, zien we onderstaande code. Hier zien we dat de parameters uit de SKCALL‑structuur worden opgehaald en dat vervolgens een eigen MDL‑mapping van de hash‑data wordt gemaakt op basis van de meegegeven hash MDL en de PFN. Daarna worden alle voorgaande parameters gekopieerd, verpakt in een eigen structuur en via SkmiOperateOnLockedNar doorgegeven aan SkmiValidateDynamicCodePages. Ik ga niet diep in op hoe SkmiOperateOnLockedNar werkt, omdat we nog te weinig informatie hebben over de NAR‑structuur.

image-12.png

Vanuit SkmiOperateOnLockedNar gaat de uitvoering door naar SkmiValidateDynamicCodePages met twee dingen: een pointer naar een NAR tree entry en een structuur met de eerdere parameters zoals de gekopieerde hash MDL (gebruikt om een eigen MDL‑mapping van de hash‑data te maken), de start‑VA van de hash‑data, de target MDL en de PFN van de target MDL. In SkmiValidateDynamicCodePages begint het met het maken van een eigen MDL‑mapping van het doelgeheugen op basis van de gegeven target MDL en zijn PFN, want tot nu toe was alleen de hash‑data gemapt. Vervolgens worden de driver‑pagina’s gelockt en claimt het de fysieke pagina’s van het doelgeheugen.

image-13.png

Daarna inspecteert de code de PTE’s voor de hash‑pagina’s, verifieert present/non‑large PDE, controleert dat PTE’s via MDL/transfer zijn gemapt, en vergelijkt de PTE‑PFN’s met de PFN‑lijst die is ontstaan bij het voorbereiden van de hash MDL. Ook roept het SkmiLockImagePage en SkmiIncrementImagePageReferenceCount aan op per‑image structuren in de NAR‑entry. Elke PFN‑mismatch, verkeerde PTE‑flags of ontbreken van image‑page ownership triggert SKMI_SECURITY en een failure. Daarnaast is er nog een check met betrekking tot de hash‑data die plaatsvindt voordat de VTL1‑mapping van het doelgeheugen gebeurt; die zorgt ervoor dat de hash‑VA op een specifiek offset/regio ligt van een geheugenbereik dat wordt beschreven door de NAR tree entry, namelijk waar PAGEHrx zich bevindt.

image-14.png

image-21.png

Vervolgens roept het de functie aan die onze dynamic code‑hash valideert, SkciValidateDynamicCodePages, waar we in het volgende deel dieper op ingaan. Als die NT_SUCCESS teruggeeft, wordt SkmiProtectSinglePage aangeroepen, die vervolgens de EPTE van het doelgeheugen wijzigt zodat het executable wordt. Dit is het belangrijke deel: SkciValidateDynamicCodePages moet een NT_SUCCESS teruggeven zodat ons doelgeheugen, in deze context de PAGEwx‑sections, in de EPTE als executable wordt gemarkeerd. In SkmiProtectSinglePage wordt een functie gebruikt, ShvlpInitiateVariableHypercall of ShvlpInitiateFastHypercall. Beide emitteten een vmcall die de VTL‑protectiemasker wijzigt. Je kunt hier kijken welke operaties vmcall kan doen.

image-15.png

Daarna doet de functie wat cleanup zoals het verlagen van de page reference count, vrijgeven van fysieke pagina’s, vrijgeven van andere locks en het unmap‑en van het gemapte doelgeheugen dat uit VTL0 kwam. En wanneer de functie terugkeert naar IumInvokeSecureService, wordt ook de gemapte hash‑data uit VTL0 weer unmapt. In het volgende deel van onze analyse bespreken we SkciValidateDynamicCodePages uitgebreider: welke parameters het aanneemt, hoe het de hash genereert en wat de “partiële” hash uit clipsp.sys precies inhoudt.

skci.dll - SkciValidateDynamicCodePages

SkciValidateDynamicCodePages is de functie die onze dynamic code valideert door een hash over de dynamic code te genereren en die te vergelijken met de aangeleverde hash. De functie neemt 4 parameters: de eerste is het doelgeheugen (in deze context onze dynamic code of de PAGEwx‑section), de tweede is de lengte van het doelgeheugen, de derde is de hash en de laatste is de hash‑lengte. Deze functie is in feite een wrapper om CiValidateFullImagePages, die de hash daadwerkelijk genereert en vergelijkt. De return‑waarde van CiValidateFullImagePages wordt vertaald naar 0x12c als die NT_SUCCESS teruggeeft. We analyseren CiValidateFullImagePages in dit deel van de analyse.

image-16.png

image-17.png

CiValidateFullImagePages berekent eerst of de aangeleverde hash voldoende is om al het doelgeheugen te valideren. Vervolgens wordt gecontroleerd of HashType 0x800c is, wat overeenkomt met het SHA256‑type. Als dat zo is, wordt een parallel hashing context verkregen, die wordt gebruikt om tot 8 pagina’s tegelijk te hashen via SymCryptParallelSha256Process. Omdat het hashtype uit SkciValidateDynamicCodePages 0x800E is, kunnen we dit negeren. In plaats daarvan wordt een hash gegenereerd voor één pagina van het doelgeheugen met de functie HashKComputeMemoryHash. Intern wordt HashpInitHash aangeroepen, en daaruit weten we dat het hashtype 0x800E overeenkomt met SHA512.

image-19.png

image-18.png

Nadat voor één pagina van het doelgeheugen een hash is gegenereerd, wordt deze vergeleken met de aangeleverde hash. Hier komt het “partiële” aspect om de hoek. Zoals je ziet wordt alleen de laatste 32 bytes van de gegenereerde SHA512‑hash vergeleken. Dit betekent dat de hashes die in clipsp.sys zijn opgeslagen “partiële” hashes zijn, want er wordt alleen de laatste 32 bytes van een SHA512‑hash opgeslagen.

image-20.png

Als in SkciValidateDynamicCodePages alle dynamic code‑pagina’s zijn gevalideerd, wordt de return‑waarde 0x12c teruggegeven aan MmChangeImageProtection.

Samenvatting

  1. Deze analyse laat zien dat Warbird een bewust Windows‑kernelmechanisme is dat gecontroleerde dynamic kernel code mogelijk maakt door gevoelige routines in PAGEwx‑sections te packen, ze on‑demand te ontsleutelen, uit te voeren en daarna weer te versleutelen.
  2. Warbird gebruikt MDL’s en writable mappings plus een custom Feistel‑cipher om section‑inhoud te beschermen en te wijzigen, en coördineert gelijktijdige toegang met een mutex en decryption counters.
  3. De cruciale enabler is MmChangeImageProtection: die bemiddelt VTL0/VTL1‑gedrag door VslValidateDynamicCodePages aan te roepen, dat MDL’s en partiële hashes verpakt in een vmcall (SSCN) naar de Secure Kernel (SkmiValidateDynamicCodePages).
  4. De Secure Kernel (Skmi) en skci.dll voeren validatie uit per pagina (CiValidateFullImagePages). Warbird levert per pagina een “partiële” SHA‑512 (laatste 32 bytes). Bij succesvolle validatie (vertaald naar 0x12c) wordt de VTL1 EPTE net als de VTL0 PTE executable gezet. Zo niet, dan alleen VTL0, en faalt uitvoering toch.
  5. Dit ontwerp is een bewuste interoperabiliteit tussen ntoskrnl, de Secure Kernel en skci: geen simpele bypass van HVCI/VBS maar een gecontroleerd uitzonderingspad waarin VTL1 uiteindelijk dynamic executable pages autoriseert na cryptografische validatie.

Kwetsbaarheid?

We zouden de Warbird‑encryptie en ‑decryptieroutine kunnen re‑implementeren en onze eigen instructies of shellcode injecteren in de ontsleutelde PAGEwx‑section vlak voordat WarbirdFinishSectionModification wordt aangeroepen. En in WarbirdFinishSectionModification zouden we het hash‑adres kunnen omleiden naar onze eigen custom hash die zich in een van de writable sections van clipsp.sys bevindt, of we zouden een page remap‑aanval kunnen doen op de bestaande hash in de PAGEHrx‑section en die daar aanpassen.

Maar nee, hier zijn een paar redenen waarom de bovenstaande exploit niet zou werken:

  • VA containment (NarBase + PageCount) zorgt ervoor dat de hash‑VA in het door NAR beheerde bereik ligt, wat geen andere writable sections in clipsp.sys omvat.
  • Het remappen van de PAGEHrx‑VA omzeilt wel de NAR‑VA‑containmentcheck, maar de PFN/PTE‑ en image‑page‑validaties van Skmi zijn bedoeld om remaps te detecteren, dus je kunt de NAR‑checks niet omzeilen door de PAGEHrx‑VA in VTL0 simpelweg naar een andere fysieke pagina te laten wijzen.

Bronnen

Hier zijn een paar bronnen die ik kan aanbevelen als je geïnteresseerd bent in meer over VBS, HVCI en/of VTL1/Securekernel :

  1. Connor McGarr’s HVCI‑blogpost
  2. Connor McGarr’s Secure Images
  3. Connor McGarr’s KM Shadow Stacks
  4. Intel VT‑rp Deel 1 door tandasat
  5. Debugging the Windows Hypervisor: Inspecting SK Calls
  6. Rayanfam’s Hypervisor From Scratch‑blogpost

Frequently Asked Questions

Authored By
GetRektBoy724

A Team Member at Websec - Interested in malware development, Windows internals and low levels, and reverse engineering

Deel met de wereld!

Beveiligingsbehoeften?

Bent u er echt zeker van dat uw organisatie veilig is?

Bij WebSec helpen we u deze vraag te beantwoorden door geavanceerde beveiligingsbeoordelingen uit te voeren.

Wil je meer weten? Plan een gesprek in met een van onze experts.

Afspraak Inplannen
Authored By
GetRektBoy724

A Team Member at Websec - Interested in malware development, Windows internals and low levels, and reverse engineering