NtSetTimer2
Arma un objeto Timer2 con un tiempo de vencimiento, periodo opcional y un bloque T2_SET_PARAMETERS con callback y flags.
Prototipo
NTSTATUS NtSetTimer2( HANDLE TimerHandle, PLARGE_INTEGER DueTime, PLARGE_INTEGER Period, PT2_SET_PARAMETERS Parameters );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| TimerHandle | HANDLE | in | Handle a un timer creado previamente con NtCreateTimer2. |
| DueTime | PLARGE_INTEGER | in | Tiempo de vencimiento inicial absoluto (positivo) o relativo (negativo, en unidades de 100ns). |
| Period | PLARGE_INTEGER | in | Intervalo periódico opcional en milisegundos; NULL o 0 para one-shot. |
| Parameters | PT2_SET_PARAMETERS | in | Estructura con TolerableDelay, NoWakeTolerance opcional y el descriptor apc/callback. |
IDs de syscalls por versión de Windows
| Versión 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 |
Módulo del kernel
APIs relacionadas
Stub del 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
Notas no documentadas
Donde el antiguo `NtSetTimer` tomaba la rutina APC y su contexto como parámetros `PTIMER_APC_ROUTINE` / `PVOID` separados, `NtSetTimer2` colapsa todo lo que *modifica* el estado armado en un único bloque `T2_SET_PARAMETERS`. La estructura lleva `Version`, un `TolerableDelay` (usado por el scheduler de coalescencia), un `NoWakeTolerance` opcional (evita sacar la CPU de los C-states), y una unión que elige entre *callback workitem* (usado por `SetThreadpoolTimer`) y *callback APC* (usado cuando el timer alimenta una alertable wait). En el kernel es `ExpSetTimer2` → `KiSetTimer2` y el timer se rearma vía `KiTimerExpiration` en la cola de segunda generación del `PRCB`. La coalescencia es la verdadera razón de Timer2: un flujo de pequeños timers a través de muchos hilos puede fusionarse en la expiración para reducir wakeups en un orden de magnitud.
Uso común por malware
En diseños de sleep-mask que adoptaron Timer2 (Cronos / Zilean y varios kits red-team privados) `NtSetTimer2` es la llamada que *realmente arma* el timer: el campo `T2_SET_PARAMETERS::Callback.Routine` apunta al primer gadget ROP de la cadena de cifrado, y un `DueTime` corto (típicamente 1-5 ms relativo) más un `Period` no cero hace que la cadena se rearme sola. Poner `NoWakeTolerance` a un valor alto reduce wakeups de CPU, haciendo que la postura idle del implante sea indistinguible de un consumidor threadpool normal como Edge o Teams. Algunas variantes de **PoolParty** pasan `Parameters` que apuntan a memoria de *otro proceso* mediante un work-item infiltrado, abusando del worker factory del threadpool para entregar ejecución.
Oportunidades de detección
En telemetría, `NtSetTimer2` es el cuello de botella porque `NtCreateTimer2` es demasiado ruidoso en apps legítimas. ETW-TI (`Microsoft-Windows-Threat-Intelligence`) *no* emite actualmente un evento dedicado al armado de Timer2; ETW `Microsoft-Windows-Kernel-Process` y `Microsoft-Windows-Threading` juntos permiten reconstruirlo. La señal fuerte es: `Parameters->Callback.Routine` en memoria no respaldada / RWX `MEM_PRIVATE`, `DueTime` corto (< 100 ms) combinado con un `Period` que coincide con la cadencia derivada de Ekko de 50-100 ms, y un hilo llamante cuyo propio entry point está en memoria privada. CrowdStrike, Defender for Endpoint y Elastic detectan Ekko *vía la consecuencia* — la alertable wait en memoria RWX — más que vía este syscall.
Ejemplos de syscalls directos
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 */);
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20