NtGetCachedSigningLevel
Liest das von Code Integrity gecachete Signaturlevel-Ergebnis, das als NTFS-Extended-Attribute auf einer Datei abgelegt ist.
Prototyp
NTSTATUS NtGetCachedSigningLevel( HANDLE File, PULONG Flags, PSE_SIGNING_LEVEL SigningLevel, PUCHAR Thumbprint, PULONG ThumbprintSize, PULONG ThumbprintAlgorithm );
Argumente
| Name | Type | Dir | Description |
|---|---|---|---|
| File | HANDLE | in | Handle auf die Datei, deren Extended-Attribute $Kernel.Purge.ESBCache gelesen wird. |
| Flags | PULONG | out | Empfängt Cache-Status-Flags (z. B. CI_VERIFICATION_RESULT_VALID, CI_VERIFICATION_RESULT_EXPIRED). |
| SigningLevel | PSE_SIGNING_LEVEL | out | Empfängt das gecachete Signaturlevel (0 Unchecked, 4 Authenticode, 6 Store, 8 Antimalware, 12 Microsoft, 14 Windows). |
| Thumbprint | PUCHAR | out | Vom Aufrufer bereitgestellter Puffer, der den Per-Page-Hash-Anker empfängt (typischerweise 32 Bytes für SHA-256). |
| ThumbprintSize | PULONG | in/out | Eingabe: Kapazität von Thumbprint. Ausgabe: tatsächlich geschriebene Bytes. |
| ThumbprintAlgorithm | PULONG | out | Empfängt die BCRYPT-Algorithmus-ID, die für Thumbprint genutzt wurde (z. B. 0x800C = SHA-256). |
Syscall-IDs pro Windows-Version
| Windows-Version | Syscall-ID | 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 |
Kernel-Modul
Verwandte APIs
Syscall-Stub
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
Undokumentierte Hinweise
Die *Reader*-Hälfte des Code-Integrity-Cache-Paars — `NtSetCachedSigningLevel` schreibt das `$Kernel.Purge.ESBCache`-EA, dieser Aufruf liest es zurück. Wird massiv von `ci.dll` selbst aufgerufen, kurz bevor ein Image gemappt wird, um den vollständigen Authenticode-Hash-Walk abzukürzen, von `wldp.dll` bei der Dynamic-Code-Policy-Auswertung sowie von jedem EDR, der *günstig* entscheiden möchte, ob eine DLL auf der Disk Microsoft-vertraut ist, bevor er den eigenen Scan startet. Anders als der Setter ist der Getter **nicht** privilegiert: jeder Token, der die Datei für `FILE_READ_EA` öffnen kann, darf ihn aufrufen. Das ausgegebene `SigningLevel` ist `0`, wenn die Datei nie ausgewertet wurde (EA fehlt) — ein frischer Download liefert also Unchecked bis zum ersten Image-Load oder einem expliziten `NtSetCachedSigningLevel`-Aufruf. Das `Flags`-Feld trägt Cache-Validity-Hinweise — `CI_VERIFICATION_RESULT_EXPIRED` bedeutet, dass ein Upstream-Blocklist-Update den Cache-Stempel verdrängt und das Ergebnis neu bewertet werden soll.
Häufige Malware-Nutzung
Zwei relevante Missbrauchsmuster. Erstens, **Trust-Probe** — Implants rufen `NtGetCachedSigningLevel` auf ihr eigenes Image und auf Kandidaten-Ziel-DLLs, um zu entscheiden, *ob sie sich vor* einem CIG-geschützten Ziel verstecken sollen. Liegt das Ergebnis auf der Implant-eigenen Datei bei `Unchecked` oder `Authenticode` (4), während das Ziel `Microsoft` (12) verlangt, weiß der Loader, dass er die Injection abbrechen oder auf eine Microsoft-signierte Proxy-DLL pivotieren muss. Zweitens, **EDR-DLL-Allowlist-Evasion** — moderne EDRs nutzen `NtGetCachedSigningLevel`, um zu entscheiden, welche DLLs einen Runtime-Scan verdienen und welche übersprungen werden können. Kann ein Angreifer das EA auf einer Low-Trust-DLL so vergiften, dass es als Microsoft zurückgelesen wird (die **CIG-Bypass**-Forschungsfamilie, die `NtSetCachedSigningLevel` ins Visier nimmt), liefert jeder folgende `NtGetCachedSigningLevel`-Aufruf die Lüge und die DLL ist still allowlistet. Der Getter selbst ist zudem ein Standardbaustein von **PPLFault**-artigen Ketten, in denen der Angreifer den Erfolg des Cache-Writes vor dem Weitermachen verifizieren muss.
Erkennungsmöglichkeiten
Für sich genommen sind Aufrufe häufig und schwaches Signal — jede Shell-Extension-Last und jeder EDR-Scan-Pfad gibt sie aus. Das *interessante* Signal ist Divergenz: die Datei aus forensischer Sicht hashen, dann den EA-Thumbprint gegen einen echten SHA-256 der Authenticode-Region-Pages der Datei vergleichen. Ein Mismatch heißt, der Cache lügt — exakt das CIG-Bypass-Artefakt. Microsofts `mssense.exe` (Defender for Endpoint) macht diese Validierung stichprobenartig. Der ETW-Provider `Microsoft-Windows-CodeIntegrity` emittiert Event 3089 für jedes Lookup, inklusive Dateipfad und gecachtem Level — Drift zwischen Baseline-Verteilungen des gecachten Levels pro Dateipfad kann zudem systematische Manipulation flaggen.
Direkte Syscall-Beispiele
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
}MITRE ATT&CK-Mappings
Last verified: 2026-05-20