NtSetTimer2
Arme un objet Timer2 avec un délai, une période optionnelle et un bloc T2_SET_PARAMETERS décrivant callback et flags.
Prototype
NTSTATUS NtSetTimer2( HANDLE TimerHandle, PLARGE_INTEGER DueTime, PLARGE_INTEGER Period, PT2_SET_PARAMETERS Parameters );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| TimerHandle | HANDLE | in | Handle vers un timer précédemment créé avec NtCreateTimer2. |
| DueTime | PLARGE_INTEGER | in | Échéance initiale absolue (positive) ou relative (négative, en unités de 100ns). |
| Period | PLARGE_INTEGER | in | Intervalle périodique optionnel en millisecondes ; NULL ou 0 pour un one-shot. |
| Parameters | PT2_SET_PARAMETERS | in | Structure avec TolerableDelay, NoWakeTolerance optionnel et le descripteur apc/callback. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0x192 | win10-1507 |
| Win10 1607 | 0x19B | win10-1607 |
| Win10 1703 | 0x1A1 | win10-1703 |
| Win10 1709 | 0x1A4 | win10-1709 |
| Win10 1803 | 0x1A6 | win10-1803 |
| Win10 1809 | 0x1A7 | win10-1809 |
| Win10 1903 | 0x1A8 | win10-1903 |
| Win10 1909 | 0x1A8 | win10-1909 |
| Win10 2004 | 0x1AE | win10-2004 |
| Win10 20H2 | 0x1AE | win10-20h2 |
| Win10 21H1 | 0x1AE | win10-21h1 |
| Win10 21H2 | 0x1B0 | win10-21h2 |
| Win10 22H2 | 0x1B0 | win10-22h2 |
| Win11 21H2 | 0x1B9 | win11-21h2 |
| Win11 22H2 | 0x1BD | win11-22h2 |
| Win11 23H2 | 0x1BD | win11-23h2 |
| Win11 24H2 | 0x1C0 | win11-24h2 |
| Server 2016 | 0x19B | winserver-2016 |
| Server 2019 | 0x1A7 | winserver-2019 |
| Server 2022 | 0x1B6 | winserver-2022 |
| Server 2025 | 0x1C0 | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 C0 01 00 00 mov eax, 0x1C0 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
Là où l'ancien `NtSetTimer` prenait la routine APC et son contexte comme paramètres `PTIMER_APC_ROUTINE` / `PVOID` séparés, `NtSetTimer2` regroupe tout ce qui *modifie* l'état armé dans un unique bloc `T2_SET_PARAMETERS`. La structure porte `Version`, un `TolerableDelay` (utilisé par le scheduler de coalescence), un `NoWakeTolerance` optionnel (évite de sortir le CPU des C-states), et une union choisissant entre *callback workitem* (utilisé par `SetThreadpoolTimer`) et *callback APC* (utilisé quand le timer pilote un wait alertable). Côté noyau c'est `ExpSetTimer2` → `KiSetTimer2` et le timer est réarmé via `KiTimerExpiration` exécuté sur la file de seconde génération du `PRCB`. La coalescence est la vraie raison d'être de Timer2 : un flux de petits timers à travers de nombreux threads peut être fusionné à l'expiration pour réduire les wakeups d'un ordre de grandeur.
Usage courant par les malwares
Dans les sleep masks qui ont adopté Timer2 (Cronos / Zilean et plusieurs kits red-team privés), `NtSetTimer2` est l'appel qui *arme effectivement* le timer : le champ `T2_SET_PARAMETERS::Callback.Routine` pointe vers le premier gadget ROP de la chaîne de chiffrement, et un `DueTime` court (typiquement 1-5 ms relatif) plus une `Period` non nulle font que la chaîne se réarme elle-même. Mettre `NoWakeTolerance` à une valeur élevée réduit les wakeups CPU, rendant la posture idle de l'implant indiscernable d'un consommateur threadpool normal comme Edge ou Teams. Certaines variantes de **PoolParty** passent `Parameters` pointant vers la mémoire d'un *autre processus* via un work-item smuggled, abusant du worker factory du threadpool pour délivrer l'exécution.
Opportunités de détection
Côté télémétrie, `NtSetTimer2` est le point de passage critique car `NtCreateTimer2` est trop bruyant en apps légitimes. ETW-TI (`Microsoft-Windows-Threat-Intelligence`) n'émet *pas* actuellement d'événement dédié à l'armement de Timer2 ; ETW `Microsoft-Windows-Kernel-Process` et `Microsoft-Windows-Threading` ensemble permettent de reconstruire l'événement. Le signal fort est : `Parameters->Callback.Routine` en mémoire non-backed / RWX `MEM_PRIVATE`, `DueTime` court (< 100 ms) combiné à une `Period` qui matche la cadence Ekko-dérivée de 50-100 ms, et un thread appelant dont l'entry point lui-même est en mémoire privée. CrowdStrike, Defender for Endpoint et Elastic détectent tous Ekko *via la conséquence* — l'alertable wait en mémoire RWX — plutôt que via ce syscall lui-même.
Exemples de syscalls directs
asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtSetTimer2 (SSN 0x1C0 on Win11 24H2 / Server 2025)
NtSetTimer2 PROC
mov r10, rcx ; syscall convention
mov eax, 1C0h ; SSN — drifts; resolve dynamically for portability
syscall
ret
NtSetTimer2 ENDPcModern sleep-mask arming (Cronos-style)
// Arm the Timer2 created earlier; APC delivery will run the ROP chain that
// flips implant memory between encrypted RW and executable RX.
typedef struct _T2_SET_PARAMETERS_V0 {
ULONG Version;
ULONG Reserved;
LONG64 NoWakeTolerance;
} T2_SET_PARAMETERS, *PT2_SET_PARAMETERS;
LARGE_INTEGER due = { .QuadPart = -50000 }; // 5 ms relative
LARGE_INTEGER period = { .QuadPart = 50 }; // 50 ms
T2_SET_PARAMETERS p = { .Version = 0, .NoWakeTolerance = 50 * 10000 };
NTSTATUS st = NtSetTimer2(hTimer, &due, &period, &p);rustSetThreadpoolTimer wrapper
// Cargo: windows-sys = "0.59" (Win32_System_Threading, Win32_Foundation)
use windows_sys::Win32::System::Threading::{SetThreadpoolTimer, PTP_TIMER};
use windows_sys::Win32::Foundation::FILETIME;
unsafe fn arm(t: PTP_TIMER) {
let mut due: FILETIME = std::mem::zeroed();
// Negative => relative 100ns; -50_000 == 5 ms.
let rel: i64 = -50_000;
due.dwLowDateTime = (rel as u64) as u32;
due.dwHighDateTime = ((rel as u64) >> 32) as u32;
SetThreadpoolTimer(t, &due, 50 /* period ms */, 100 /* tolerance ms */);
}Mappings MITRE ATT&CK
Last verified: 2026-05-20