> Windows Syscalls
ntoskrnl.exeT1553T1518.001T1106

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

NameTypeDirDescription
FileHANDLEinHandle vers le fichier dont l'attribut étendu $Kernel.Purge.ESBCache sera lu.
FlagsPULONGoutReçoit les drapeaux d'état du cache (ex. CI_VERIFICATION_RESULT_VALID, CI_VERIFICATION_RESULT_EXPIRED).
SigningLevelPSE_SIGNING_LEVELoutReçoit le niveau de signature en cache (0 Unchecked, 4 Authenticode, 6 Store, 8 Antimalware, 12 Microsoft, 14 Windows).
ThumbprintPUCHARoutTampon fourni par l'appelant qui reçoit l'ancre de hash par-page (typiquement 32 octets pour SHA-256).
ThumbprintSizePULONGin/outEn entrée : capacité de Thumbprint. En sortie : octets effectivement écrits.
ThumbprintAlgorithmPULONGoutReçoit l'ID d'algorithme BCRYPT utilisé pour Thumbprint (ex. 0x800C = SHA-256).

IDs de syscalls par version de Windows

Version de WindowsID de syscallBuild
Win10 15070xE1win10-1507
Win10 16070xE4win10-1607
Win10 17030xE7win10-1703
Win10 17090xE8win10-1709
Win10 18030xE9win10-1803
Win10 18090xEAwin10-1809
Win10 19030xEBwin10-1903
Win10 19090xEBwin10-1909
Win10 20040xF0win10-2004
Win10 20H20xF0win10-20h2
Win10 21H10xF0win10-21h1
Win10 21H20xF1win10-21h2
Win10 22H20xF1win10-22h2
Win11 21H20xF6win11-21h2
Win11 22H20xF7win11-22h2
Win11 23H20xF7win11-23h2
Win11 24H20xF9win11-24h2
Server 20160xE4winserver-2016
Server 20190xEAwinserver-2019
Server 20220xF5winserver-2022
Server 20250xF9winserver-2025

Module noyau

ntoskrnl.exeNtGetCachedSigningLevel

APIs liées

NtSetCachedSigningLevelWldpQueryDynamicCodeTrustGetProcessMitigationPolicy (ProcessSignaturePolicy)CiValidateFileObject

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 ENDP

rustRead 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