NtGetCachedSigningLevel
Lit le résultat de niveau de signature mis en cache par Code Integrity, stocké comme attribut étendu NTFS sur un fichier.
Prototype
NTSTATUS NtGetCachedSigningLevel( HANDLE File, PULONG Flags, PSE_SIGNING_LEVEL SigningLevel, PUCHAR Thumbprint, PULONG ThumbprintSize, PULONG ThumbprintAlgorithm );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| File | HANDLE | in | Handle vers le fichier dont l'attribut étendu $Kernel.Purge.ESBCache sera lu. |
| Flags | PULONG | out | Reçoit les drapeaux d'état du cache (ex. CI_VERIFICATION_RESULT_VALID, CI_VERIFICATION_RESULT_EXPIRED). |
| SigningLevel | PSE_SIGNING_LEVEL | out | Reçoit le niveau de signature en cache (0 Unchecked, 4 Authenticode, 6 Store, 8 Antimalware, 12 Microsoft, 14 Windows). |
| Thumbprint | PUCHAR | out | Tampon fourni par l'appelant qui reçoit l'ancre de hash par-page (typiquement 32 octets pour SHA-256). |
| ThumbprintSize | PULONG | in/out | En entrée : capacité de Thumbprint. En sortie : octets effectivement écrits. |
| ThumbprintAlgorithm | PULONG | out | Reçoit l'ID d'algorithme BCRYPT utilisé pour Thumbprint (ex. 0x800C = SHA-256). |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0xE1 | win10-1507 |
| Win10 1607 | 0xE4 | win10-1607 |
| Win10 1703 | 0xE7 | win10-1703 |
| Win10 1709 | 0xE8 | win10-1709 |
| Win10 1803 | 0xE9 | win10-1803 |
| Win10 1809 | 0xEA | win10-1809 |
| Win10 1903 | 0xEB | win10-1903 |
| Win10 1909 | 0xEB | win10-1909 |
| Win10 2004 | 0xF0 | win10-2004 |
| Win10 20H2 | 0xF0 | win10-20h2 |
| Win10 21H1 | 0xF0 | win10-21h1 |
| Win10 21H2 | 0xF1 | win10-21h2 |
| Win10 22H2 | 0xF1 | win10-22h2 |
| Win11 21H2 | 0xF6 | win11-21h2 |
| Win11 22H2 | 0xF7 | win11-22h2 |
| Win11 23H2 | 0xF7 | win11-23h2 |
| Win11 24H2 | 0xF9 | win11-24h2 |
| Server 2016 | 0xE4 | winserver-2016 |
| Server 2019 | 0xEA | winserver-2019 |
| Server 2022 | 0xF5 | winserver-2022 |
| Server 2025 | 0xF9 | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 F9 00 00 00 mov eax, 0xF9 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Notes non documentées
La moitié *lecteur* du couple de cache Code Integrity — `NtSetCachedSigningLevel` écrit l'EA `$Kernel.Purge.ESBCache`, cet appel le relit. Appelé massivement par `ci.dll` lui-même quand une image est sur le point d'être mappée pour court-circuiter le parcours complet de hash Authenticode, par `wldp.dll` lors de l'évaluation de policy dynamic-code, et par chaque EDR qui veut décider *à bas coût* si une DLL sur disque est de confiance Microsoft avant de faire son propre scan. Contrairement au setter, le getter n'est **pas** privilégié : tout token pouvant ouvrir le fichier en `FILE_READ_EA` peut l'appeler. Le `SigningLevel` de sortie est `0` quand le fichier n'a jamais été évalué (l'EA est absent), donc un téléchargement frais renverra Unchecked jusqu'au premier image load ou appel explicite à `NtSetCachedSigningLevel`. Le champ `Flags` porte des indices de validité de cache — `CI_VERIFICATION_RESULT_EXPIRED` signifie qu'une mise à jour de blocklist amont post-date le stamp du cache et que le résultat doit être réévalué.
Usage courant par les malwares
Deux patterns d'abus pertinents. Premièrement, **sonde de confiance** — les implants appellent `NtGetCachedSigningLevel` contre leur propre image et contre des DLL cibles candidates pour décider *s'ils doivent se cacher* d'une cible protégée par CIG. Si le résultat sur le fichier propre de l'implant est `Unchecked` ou `Authenticode` (4) tandis que la cible requiert `Microsoft` (12), le loader sait qu'il faut abandonner l'injection ou pivoter vers une DLL proxy signée Microsoft. Deuxièmement, **évasion de l'allowlist DLL EDR** — les EDR modernes utilisent `NtGetCachedSigningLevel` pour décider quelles DLL méritent un scan runtime vs. lesquelles peuvent être skippées. Si un attaquant peut empoisonner l'EA sur une DLL à faible confiance pour qu'elle soit relue comme Microsoft (la famille de recherche **CIG-bypass** qui vise `NtSetCachedSigningLevel`), chaque appel ultérieur de `NtGetCachedSigningLevel` renvoie le mensonge et la DLL est silencieusement allowlistée. Le getter lui-même est aussi un bloc commun des chaînes de style **PPLFault** où l'attaquant doit vérifier que l'écriture cache a réussi avant de continuer.
Opportunités de détection
En soi, les appels sont courants et à faible signal — chaque chargement de shell-extension et chemin de scan EDR en émet. Le signal *intéressant* est la divergence : hasher le fichier d'un point de vue forensique, puis comparer le thumbprint en EA contre un véritable SHA-256 des pages de région authenticode du fichier. Un mismatch signifie que le cache ment, ce qui est exactement l'artefact CIG-bypass. Le `mssense.exe` de Microsoft (Defender for Endpoint) fait cette validation sur base échantillonnée. Le provider ETW `Microsoft-Windows-CodeIntegrity` émet l'évènement 3089 pour chaque lookup, incluant le chemin du fichier et le niveau en cache — la dérive entre les distributions baseline de niveau-en-cache par chemin de fichier peut aussi flagger une falsification systématique.
Exemples de syscalls directs
cTrust probe before staging into a CIG-protected target
// Implant decides whether to inject based on the cached signing level
// of its own DLL versus the requirement implied by the target's
// ProcessSignaturePolicy.
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI* pNtGetCachedSigningLevel)(
HANDLE, PULONG, PUCHAR /*SigningLevel*/, PUCHAR, PULONG, PULONG);
int own_signing_level(LPCWSTR ownDllPath) {
HANDLE h = CreateFileW(ownDllPath, FILE_READ_EA | SYNCHRONIZE,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
ULONG flags = 0, thumbSize = 32, thumbAlg = 0;
UCHAR thumb[32] = { 0 };
UCHAR level = 0;
pNtGetCachedSigningLevel f = (pNtGetCachedSigningLevel)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtGetCachedSigningLevel");
f(h, &flags, &level, thumb, &thumbSize, &thumbAlg);
CloseHandle(h);
return level; // 0 Unchecked, 4 Authenticode, 8 Antimalware, 12 Microsoft, 14 Windows
}asmx64 direct stub (Win11 24H2 / Server 2025, SSN 0xF9)
NtGetCachedSigningLevel PROC
mov r10, rcx
mov eax, 0F9h
syscall
ret
NtGetCachedSigningLevel ENDPrustRead cached level on every newly downloaded payload
// Used by post-exploitation tooling to decide if a downloaded module is
// already trusted (often it is not — the EA only populates on first load).
use std::ffi::c_void;
use windows_sys::Win32::Foundation::HANDLE;
extern "system" {
fn NtGetCachedSigningLevel(
file: HANDLE, flags: *mut u32, level: *mut u8,
thumb: *mut u8, thumb_size: *mut u32, thumb_alg: *mut u32) -> i32;
}
unsafe fn cached_level(file: HANDLE) -> u8 {
let mut f = 0u32; let mut l = 0u8;
let mut t = [0u8; 32]; let mut ts = 32u32; let mut ta = 0u32;
NtGetCachedSigningLevel(file, &mut f, &mut l, t.as_mut_ptr(), &mut ts, &mut ta);
l
}Mappings MITRE ATT&CK
Last verified: 2026-05-20