Appeler les syscall Windows directement indépendamment de la version pour éviter les hooks user mode des antivirus
Nous allons voir ci-dessous un exemple de programme complet permettant d’appeler les syscall de Windows. L’avantage de faire ce genre d’appel direct est d’éviter les hook en user mode de certains antivirus / solution de sécurité permettant ainsi de travailler plus silencieusement.
Cependant avec l’utilisation des syscall il y a toujours un petit problème : à chaque version de Windows le numéro du syscall change. Pour consulter la liste des syscalls en fonction des versions vous pouvez vous rendre à cette https://j00ru.vexillium.org/syscalls/nt/64/.
Pour résoudre ce problème, nous allons donc parser nous même la table d’export de ntdll.dll chargé en mémoire et calculer le numéro du syscall pour ensuite l’appeler. Ainsi notre programme pourra s’exécuter sur n’importe quelle version de Windows.
Lors de la récupération de la liste des syscall nous calculerons un hash (ici CRC32) ce qui nous permettra par la suite de l’utiliser pour retrouver le numéro du syscall et faire l’appel.
La fonction qui effectuera l’appel, sera complétée pendant l’exécution du programme pour éviter certaine détection statique. Tout ce processus permettant d’être un peu plus opaque doit être amélioré mais ce n’est pas le but de cet article. Ici le c’était surtout pour mettre l’accent sur le fait qu’il faut penser à éviter la présence de l’opcode « syscall ».
Avec le programme ci-dessous nous pourrons appeler n’importe quel syscall de manière générique sans devoir écrire une fonction par appel différent et par version de Windows.
Vous trouverez le code complet du projet sur mon GitHub :
https://github.com/arnotic/Syscall
Ceci n’est qu’un POC pas du tout optimiser si l’on veut réellement écrire un code de production.
Pour en savoir un peu plus sur le format PE et comprendre mieux le code vous pouvez consulter cette page https://fr.wikipedia.org/wiki/Portable_Executable
Le détails du code :
Pour récupérer l’adresse du TEB :
ALIGN 16
getAddrTEB PROC
mov rax, gs:[30h]
ret
getAddrTEB ENDP
Voici la fonction permettant de trouver le module NTDLL, de lister la table d’export et d’enregistrer les différents syscall trouvés.
On enregistre toutes les fonctions commençant par « Zw » qui reprensent un syscall et on calcul un hash qui nous permettra plus tard de faire de retrouver son numéro.
On réalise un tri grâce aux adresses trouvées par ordre croissant qui nous. Ce tri nous permet de connaître le numéro du syscall.
void findSyscall() {
PPEB_LDR_DATA pLdrData;
PLDR_DATA_TABLE_ENTRY LdrEntry;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_DATA_DIRECTORY DataDirectory;
PIMAGE_EXPORT_DIRECTORY pExports;
SYSCALL scTemp;
PDWORD pFunctions;
PDWORD pNames;
PWORD pOrdinals;
PCHAR szDllName;
PCHAR szFunctionName;
DWORD i;
DWORD j;
DWORD VirtualAddress;
// RECUPERATION DE L'ADRESSE DE PEB_LDR_DATA QUI CONTIENT LES INFORMATIONS DES MODULES CHARGES
pLdrData = (PPEB_LDR_DATA)getAddrTEB()->ProcessEnvironmentBlock->Ldr;
for (LdrEntry = (PLDR_DATA_TABLE_ENTRY)pLdrData->Reserved2[1]; LdrEntry->DllBase != 0; LdrEntry = (PLDR_DATA_TABLE_ENTRY)LdrEntry->Reserved1[0])
{
// ON RECUPERE LES ADRESSES DES DIFFERENTES STRUCTURES
pDosHeader = (PIMAGE_DOS_HEADER)LdrEntry->DllBase;
pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)LdrEntry->DllBase + pDosHeader->e_lfanew);
DataDirectory = (PIMAGE_DATA_DIRECTORY)pNtHeaders->OptionalHeader.DataDirectory;
// ON VERIFIE LA TABLE D'EXPORT
if (!DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress) continue;
pExports = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)LdrEntry->DllBase + DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// RECUPERATION DU NOM DU MODULE
szDllName = (PCHAR)((DWORD_PTR)LdrEntry->DllBase + pExports->Name);
// SI PAS NTDLL ON CONTINUE DE TESTER
if ((*(DWORD*)szDllName) != 0x6C64746E) continue; // ldtn
if ((*(DWORD*)(szDllName + 4)) == 0x6C642E6C) break; // ld.l
}
pFunctions = (PDWORD)((DWORD_PTR)LdrEntry->DllBase + pExports->AddressOfFunctions);
pNames = (PDWORD)((DWORD_PTR)LdrEntry->DllBase + pExports->AddressOfNames);
pOrdinals = (PDWORD)((DWORD_PTR)LdrEntry->DllBase + pExports->AddressOfNameOrdinals);
// ON ENREGISTRE TOUTES LES FONCTIONS COMMENCANT PAR "Zw" QUI NOUS INFORME QUE C'EST UN SYSCALL
i = pExports->NumberOfNames - 1;
do {
szFunctionName = (PCHAR)((DWORD_PTR)LdrEntry->DllBase + pNames[i]);
if (*(USHORT*)szFunctionName == 0x775A) { // Zw
ListSyscall.f[ListSyscall.dwNbSyscall].addr = pFunctions[pOrdinals[i]];
ListSyscall.f[ListSyscall.dwNbSyscall].name = szFunctionName;
// CALCUL DE NOTRE HASH
ListSyscall.f[ListSyscall.dwNbSyscall].hash = calcCrc32(-1, ListSyscall.f[ListSyscall.dwNbSyscall].name, strlen(ListSyscall.f[ListSyscall.dwNbSyscall].name));
// ON INCREMENTE LE NOMBRE DE FONCTIONS TRAITEES
ListSyscall.dwNbSyscall++;
// SECURITE
if (ListSyscall.dwNbSyscall == MAX_SYSCALLS) break;
}
} while (--i);
// TRI DES SYSCALL EN FONCTION DE L'ADRESSE (ASC)
for (i = 0; i < (ListSyscall.dwNbSyscall - 1); i++) {
for (j = 0; j < (ListSyscall.dwNbSyscall - i - 1); j++) {
if (ListSyscall.f[j].addr > ListSyscall.f[j + 1].addr) {
scTemp.addr = ListSyscall.f[j].addr;
scTemp.name = ListSyscall.f[j].name;
scTemp.hash = ListSyscall.f[j].hash;
ListSyscall.f[j].addr = ListSyscall.f[j + 1].addr;
ListSyscall.f[j].name = ListSyscall.f[j + 1].name;
ListSyscall.f[j].hash = ListSyscall.f[j + 1].hash;
ListSyscall.f[j + 1].addr = scTemp.addr;
ListSyscall.f[j + 1].name = scTemp.name;
ListSyscall.f[j + 1].hash= scTemp.hash;
}
}
}
}
Pour faire l’appel au syscall nous allons utiliser ces 2 fonctions :
ALIGN 16
callSyscall PROC
mov r10, rcx
mov eax, idxSysCall
nop ; ON LAISSE 2 OCTETS DE LIBRE POUR ECRIRE PENDANT L'EXECUTION LES OPCODES POUR 'syscall'
nop ; -----
ret
callSyscall ENDP
callSyscall va effectuer l’appel. Comme vous pouvez le voir j’ai laissé 2 nop qui seront remplacés pendant l’execution par les opcodes de « syscall » via la fonction modifyFunction(). Il y a d’autre moyen de faire ça. Cependant ce n’est pas le but de l’article. C’était surtout pour penser à éviter d’utiliser l’opcode syscall qui pourrait être détecté.
void modifyFunction() {
DWORD dwOldProtect;
VirtualProtect(((BYTE *)&callSyscall + 3), 2, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// ON ECRIT L'OPCODE 'syscall'
*((BYTE*)((BYTE *)&callSyscall + 9)) = 0x0F;
*((BYTE*)((BYTE *)&callSyscall + 10)) = 0x05;
VirtualProtect(((BYTE *)&callSyscall + 3), 2, dwOldProtect, &dwOldProtect);
}
Par la suite il nous suffit de définir le prototype de nos fonctions :
typedef NTSTATUS syscallCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength);
typedef NTSTATUS syscallCloseHandle(HANDLE Handle);
typedef NTSTATUS syscallWriteFile(HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine, PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer, ULONG Length, PLARGE_INTEGER ByteOffset, PULONG Key);
On fait pointer ça sur notre fonction d’appel :
void initFunction() {
sysCreateFile = (syscallCreateFile*)&callSyscall;
sysCloseHandle = (syscallCloseHandle*)&callSyscall;
sysWriteFile = (syscallWriteFile*)&callSyscall;
}
Il nous reste alors qu’a appeler nos fonctions, ici un exemple avec ZwCreateFile() :
// CreateFile
idxSysCall = getIdxSyscall(0x7235aa8c); // { name=0x00007ff8a9731bcd "ZwCreateFile" hash=0x7235aa8c }
dwStatus = sysCreateFile(&hfl, FILE_ALL_ACCESS, &oa, &osb, 0, 0, 0, FILE_OVERWRITE_IF, FILE_SYNCHRONOUS_IO_NONALERT, 0, 0);
if (dwStatus != 0) {
WriteConsoleA(hstdOut, "[!] Error : CreateFile", 22, &dwrw, NULL);
goto ON_EXIT;
}
Comme vous pouvez le voir j’utilise le hash que j’avais calculé quand je listais les syscall de ntdll.dll pour retrouver le numéro de celui-ci.
Le programme est compilé sans CRT ce qui nous permet une certaine indépendance…
Vous trouverez aussi dans le code du projet plusieurs fonctions écrites en assembleur nous permettant de nous substituer des dépendances à la CRT.
Référence :