H.Modifier le loader pour qu'il utilise des syscalls direct ou indirects
1. Contexte : L'architecture Windows
Le système d'exploitation Windows est divisé en deux modes d'exécution principaux :
User Mode (Ring 3) : Là où s'exécutent les applications classiques. Elles n'ont pas d'accès direct au matériel ou à la mémoire critique.
Kernel Mode (Ring 0) : Le noyau (Kernel) du système. Il possède des privilèges complets.
Pour qu'une application en User Mode réalise une action privilégiée (ex: lire un fichier, créer un processus), elle doit demander au Kernel de le faire via une System Call (Syscall).
2. La chaîne d'appel standard (The Journey)
Traditionnellement, un programme suit ce chemin :
Win32 API (
kernel32.dll/advapi32.dll) : Fonctions documentées et faciles à utiliser (ex:CreateProcess).Native API (
ntdll.dll) : La couche la plus basse du User Mode. Les fonctions commencent souvent parNtouZw. C'est ici que se prépare le passage vers le Kernel.Syscall Instruction : L'instruction assembleur (
syscallen x64) bascule l'exécution vers le Kernel.
3. Le problème : Le Hooking par les EDR
Les solutions de sécurité (EDR/AV) surveillent les activités malveillantes en plaçant des hooks (hameçons) dans la mémoire des processus, principalement au niveau de ntdll.dll.
Mécanisme : L'EDR remplace les premières instructions d'une fonction (ex:
NtWriteProcessMemory) par un saut (JMP) vers son propre moteur d'analyse.Résultat : Si l'appel est jugé suspect, l'EDR bloque l'exécution avant même qu'elle n'atteigne le Kernel.
4. La solution : Les Direct Syscalls
Le but est de court-circuiter ntdll.dll pour éviter les hooks.
Principe technique
Au lieu d'appeler la fonction dans ntdll.dll, le développeur reproduit manuellement l'étape finale en assembleur dans son propre code :
Placer le SSN (System Service Number) — l'identifiant unique de la fonction système — dans le registre
EAX.Préparer les arguments dans les registres adéquats.
Exécuter directement l'instruction
syscall.
Avantage : Comme l'instruction syscall est exécutée depuis le code de l'attaquant et non depuis la ntdll.dll patchée, l'EDR ne voit jamais passer l'appel en User Mode.
5. Défis et limitations
Variabilité des SSN : Les numéros de syscall changent à chaque version (parfois chaque mise à jour) de Windows.
Solution : Utiliser des techniques comme Hell's Gate ou Halo's Gate pour récupérer dynamiquement les SSN en lisant la
ntdll.dllsur le disque ou en mémoire.
Indicateurs de compromission (IoC) :
La présence d'instructions
syscalldans un exécutable suspect est une anomalie.Call Stack Analysis : Les EDR modernes vérifient d'où vient l'appel. Si un syscall arrive au Kernel sans passer par
ntdll.dll, c'est une détection immédiate.
6. Évolution : Les Indirect Syscalls
Pour contrer l'analyse de la pile d'appel (Call Stack), les attaquants utilisent désormais les Indirect Syscalls :
On prépare le SSN manuellement.
Mais au lieu d'exécuter
syscalldans notre code, on saute (JMP) vers l'instructionsyscallsituée légitimement à l'intérieur dentdll.dll.Bénéfice : Pour l'EDR, l'appel semble provenir d'un emplacement mémoire légitime.
B. Pratique :
On va d'abord voir avec un debuggeur l'architecture utilisé, c'est à dire le fait de passer de kernel32.dll à ntdll puis à l'instruction syscall en identifiant le SSN via windbg :
On va utiliser le programme réaliser sur la page A.Développer un loader de shellcode qui utilise l'API Windows
Désormais on le lance et on met un breakpoint à VirtualAlloc par exemple :

Ensuite comme on peut le voir NtAllocateVirtualMemory est appellé :

Desormais on jump vers le code exécuté par NtAllocateVirtualMemory pour voir le syscall réalisé et récupérer le SSN donc ça veut dire quand on fera des syscalls direct qu'il faudra aller de nouveau consulter ntdll.dll pour récupérer le SSN :

Bingo : On identifie le syscall et le SSN qui est 18h

Désormais passons au code du syscall direct mais avec un premier exemple non complexe avec peu d'argument :
Il va falloir télécharger nasm :
Donc on commence par définir la définition de la fonction :
On lance windbg pour récupérer le SSN :

SSN = 0x34
Désormais on va ecrire un stub assembleur qu'on va exporter et utiliser dans notre code C. On pourrait écrire directement de l'assembleur en C, ça se nomme de l'assembly inline mais ça engendre des problèmes qui ne seront pas abordé puisque c'est un exemple haut level juste pour comprendre comment faire des syscalls direct :
Voici le code assembleur :
Explication registre :
Pourquoi R10 au lieu de RCX ?
R10 au lieu de RCX ?C'est une spécificité qui a lieu lorsqu'on réalise un syscall.
La Convention d'Appel C (Shadow Space) : En x64, le compilateur C utilise toujours
RCXpour le premier argument.La contrainte de l'instruction
syscall: Lorsqu'on exécutes l'instructionsyscall, le processeur écrase automatiquement la valeur du registreRCXpour y sauvegarder l'adresse de retour (leRIPactuel) afin de savoir où revenir une fois le travail fini en Kernel Mode.La solution de Microsoft : Comme
RCXva être "détruit" par l'instructionsyscall, Microsoft a décidé que pour les appels système, le premier argument serait lu dansR10par le noyau.
C'est pour cela que dans chaque stub de ntdll.dll, la première ligne est systématiquement mov r10, rcx. On sauve l'argument dans R10 avant que RCX ne soit écrasé.
2. Rappel des Registres (Convention x64 Windows)
Voici comment les arguments des fonctions (comme NtDelayExecution ou NtAllocateVirtualMemory) sont répartis dans les registres au moment où on entre dans le stub (morceau de code) assembleur :
Registre
Rôle pour un Syscall
Ce qu'il contient dans ton code
RAX
ID de la fonction (SSN)
Le numéro 18h ou 34h que j'avais trouvé
RCX
Argument 1 (Temporaire)
La valeur Alertable ou le ProcessHandle.
R10
Argument 1 (Final)
Reçoit la copie de RCX pour le Kernel.
RDX
Argument 2
L'adresse de délai (&delay) .
R8
Argument 3
Le 3ème paramètre
R9
Argument 4
Le 4ème paramètre
Stack
Arguments 5, 6, ...
Si la fonction a plus de 4 arguments, ils sont sur la pile.
Ce qui nous donne donc en code C :
Et ça fonctionne très bien on commence utilisé la fonction extern défini dans notre code assembleur et ensuite on compile comme cela :
Désormais faisons le syscall direct de NtAllocateVirtualMemory qui sera plus dure car il y a plus de 4 arguments donc le 5 et 6 sera sur la stack :
On fait la même méthode on identifie le SSN et ensuite on code notre stub puis notre code C :
Je l'avais identifié c'est 0x18
Du coup avant de faire le syscall direct, regardons la call stack quand on utilise NtAllocateVirtualMemory :

On se rends compte que l'appel à la fonction est bien présente dans la call stack ainsi un edr qui analyse la callstack peut identifier cette appel, pour résoudre ceci faisons appel au syscall direct pour éviter d'avoir dans notre callstack une référence vers une fonction de ntdll :
Du coup voici le stub assembleur :
Le code C :

ça fonctionne mais ça serai cool de bien vérifier ce qu'on fait, si la call stack est clean regarder les registre de la pile pour bien identifier les arguments 5 et 6 de notre fonction :
Grâce à la ligne int3 présent dans notre stub, on va pouvoir obtenir un breakpoint juste avant l'opération syscall :
Quand on lance le go sur windbg on obtient bien notre breakpoint :

A partir de là on peut inspecter la callstack en utilisant la commande : k

On identifie notre stub sur la callstack et ainsi on note l'absence de la fonction NtAllocateVirtualMemory ce qui est un très bon point, cela veut dire que notre implémentation de notre syscall est bien celle utilisé pour alloué de la mémoire sans passer par ntdll.
Regardons désormais les registres :

Le registre RAX a pour valeur 0000000000000018 qui est le SSN de NtVirtualAlloc
Le registe RDX symbolise le deuxième arguments de notre fonction qui est l'adresse de la variable baseadress
Donc RDX = 0000009ddddff720
On regarde la valeur dans cette adresse qui normalement doit être égale à 0 car dans notre code on avait ça :
Du coup :
Pour le premier argument de ma fonction qui est dans le registre R10 :

On est donc bon pour R10 qui vaut bien -1
Passons au troisième argument de ma fonction qui est situé dans R8 :

R8 est bien égal à 0.
Désormais le quatrième argument qui est situé dans R9 :
R9 = 0000009ddddff728 qui est l'adresse de regionSize et regionSize pour rappel est egale à 12.
Donc avec la commande dq <adress> on va pouvoir identifier la valeur :

Ainsi c'est bon aussi, il nous reste désormais nos deux derniers arguments qu'on ne peut pas retrouver dans les registres mais sur la stack.
A finir : Il faut trouver comment accéder à la stack et je crois c'est via RSP.
Last updated