NtGetCachedSigningLevel
Lee el resultado de nivel de firma cacheado por Code Integrity, almacenado como atributo extendido NTFS en un archivo.
Prototipo
NTSTATUS NtGetCachedSigningLevel( HANDLE File, PULONG Flags, PSE_SIGNING_LEVEL SigningLevel, PUCHAR Thumbprint, PULONG ThumbprintSize, PULONG ThumbprintAlgorithm );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| File | HANDLE | in | Handle al archivo cuyo atributo extendido $Kernel.Purge.ESBCache se leerá. |
| Flags | PULONG | out | Recibe banderas de estado del caché (p. ej. CI_VERIFICATION_RESULT_VALID, CI_VERIFICATION_RESULT_EXPIRED). |
| SigningLevel | PSE_SIGNING_LEVEL | out | Recibe el nivel de firma cacheado (0 Unchecked, 4 Authenticode, 6 Store, 8 Antimalware, 12 Microsoft, 14 Windows). |
| Thumbprint | PUCHAR | out | Búfer proporcionado por el llamante que recibe el ancla de hash por-página (típicamente 32 bytes para SHA-256). |
| ThumbprintSize | PULONG | in/out | En entrada: capacidad de Thumbprint. En salida: bytes realmente escritos. |
| ThumbprintAlgorithm | PULONG | out | Recibe el ID de algoritmo BCRYPT usado para Thumbprint (p. ej. 0x800C = SHA-256). |
IDs de syscalls por versión de Windows
| Versión 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 |
Módulo del kernel
APIs relacionadas
Stub del 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
Notas no documentadas
La mitad *lectora* del par de caché Code Integrity — `NtSetCachedSigningLevel` escribe el EA `$Kernel.Purge.ESBCache`, esta llamada lo lee de vuelta. Llamado masivamente por `ci.dll` mismo cuando una imagen está a punto de mapearse para cortocircuitar el recorrido completo del hash Authenticode, por `wldp.dll` al evaluar la policy de dynamic-code, y por cada EDR que quiere decidir *barato* si una DLL en disco es de confianza Microsoft antes de hacer su propio scan. A diferencia del setter, el getter **no** es privilegiado: cualquier token que pueda abrir el archivo para `FILE_READ_EA` puede llamarlo. El `SigningLevel` de salida es `0` cuando el archivo nunca ha sido evaluado (el EA está ausente), así que una descarga fresca devolverá Unchecked hasta la primera carga de imagen o llamada explícita a `NtSetCachedSigningLevel`. El campo `Flags` lleva pistas de validez de caché — `CI_VERIFICATION_RESULT_EXPIRED` significa que una actualización de blocklist aguas arriba post-fecha el stamp del caché y el resultado debe re-evaluarse.
Uso común por malware
Dos patrones de abuso relevantes. Primero, **sonda de confianza** — los implantes llaman `NtGetCachedSigningLevel` contra su propia imagen y contra DLL objetivo candidatas para decidir *si deben esconderse* de un objetivo protegido por CIG. Si el resultado sobre el propio archivo del implante es `Unchecked` o `Authenticode` (4) mientras el objetivo requiere `Microsoft` (12), el loader sabe que debe abortar la inyección o pivotar a una DLL proxy firmada por Microsoft. Segundo, **evasión de allowlist DLL EDR** — los EDR modernos usan `NtGetCachedSigningLevel` para decidir qué DLL merecen escaneo runtime vs. cuáles pueden saltarse. Si un atacante puede envenenar el EA en una DLL de baja confianza para que se relea como Microsoft (la familia de investigación **CIG-bypass** que ataca `NtSetCachedSigningLevel`), cada llamada subsiguiente a `NtGetCachedSigningLevel` devuelve la mentira y la DLL queda silenciosamente allowlistada. El getter mismo también es un bloque común de cadenas estilo **PPLFault** donde el atacante necesita verificar que la escritura de caché tuvo éxito antes de continuar.
Oportunidades de detección
Por sí solas las llamadas son comunes y de baja señal — cada carga de shell-extension y camino de scan EDR las emite. La señal *interesante* es la divergencia: hashear el archivo desde una perspectiva forense, luego comparar el thumbprint en EA contra un verdadero SHA-256 de las páginas de la región authenticode del archivo. Un mismatch significa que el caché miente, que es exactamente el artefacto CIG-bypass. El `mssense.exe` de Microsoft (Defender for Endpoint) hace esta validación de forma muestreada. El provider ETW `Microsoft-Windows-CodeIntegrity` emite el evento 3089 para cada lookup, incluyendo la ruta del archivo y el nivel cacheado — la deriva entre distribuciones baseline de nivel-cacheado por ruta de archivo también puede señalar manipulación sistemática.
Ejemplos de syscalls directos
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
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20