Syscalls en windows
Hace poco escribía sobre syscalls y shellcoding en windows, sin embargo se me hizo necesario antes hacer una introducción acerca de los syscalls, y el porque es importante aprenderlos para el desarrollo de malwares.
Como ya muchos deben saber, actualmente los OS manejan algo llamado protected-mode, que es la forma en como interactúa el OS con la memoria (en este caso virtual) a diferencia del real-mode, el cual antiguamente permitía interactuar directamente con la memoria, sin embargo esto hacía que ante cualquier error relacionado con la memoria el sistema crasheara.
Teniendo esto como premisa, es importante saber además que dentro de este modo, windows maneja en esencia 2 formas en las que interactúa una aplicación. Por un lado se tiene al user-mode y por el otro al kernel-mode tal como se describe en la siguiente imagen:
Al desarrollar una aplicación para Windows en algún lenguaje de bajo nivel debemos hacer uso de APIs que llamen a estas funciones, y de esta forma puedan ser invocadas. Por ejemplo al ejecutar un malware que has desarrollado, este va a ejecutar una función de WriteFile que permite escribir en un archivo cierto texto. Esta función es invocada a través de kernel32.dll que tiene la mayor parte de las implementaciones a usar de Win32API. Tradicionalmente se hace de la siguiente forma:
//https://docs.microsoft.com/en-us/windows/win32/fileio/opening-a-file-for-reading-or-writing
#include<windows.h>
int main(){
char DataBuffer[] = "Hacking n00b";
DWORD dwBytesToWrite = (DWORD)strlen(DataBuffer);
DWORD dwBytesWritten = 0;
BOOL bErrorFlag = FALSE;
hFile = CreateFileA("manifest.txt",
GENERIC_WRITE,
0,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL);
WriteFile( hFile, DataBuffer, dwBytesToWrite, dwBytesWritten, NULL);
}
Un código simple que crea un archivo llamado manifest.txt y luego escribe cierto contenido dentro. Si estas familiarizado con el desarrollo de malware, sabrás que normalmente se acostumbra a eliminar las referencias del IAT y de esta forma evadir el análisis estático y algunas veces dinámico, usando los ya conocidos LoadLibrary & GetProcAddress. Se hace de esta forma ya que el EDR/AV al hacer un análisis de las funciones que la aplicación llama, va directamente a esta tabla y consulta que peticiones al Win32API harás.
//https://docs.microsoft.com/en-us/windows/win32/fileio/opening-a-file-for-reading-or-writing
#include<windows.h>
typedef int (WINAPI * CREATE_FILE_A)(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
int main(){
HANDLE hFile;
HMODULE lib = LoadLibrary("kernel32.dll");
CREATE_FILE_A sCreateFile = (CREATE_FILE_A) GetProcAddress(lib, "CreateFileA");
char DataBuffer[] = "Hacking n00b";
DWORD dwBytesToWrite = (DWORD)strlen(DataBuffer);
DWORD dwBytesWritten = 0;
BOOL bErrorFlag = FALSE;
hFile = sCreateFile("manifest.txt",
GENERIC_WRITE,
0,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL);
WriteFile( hFile, DataBuffer, dwBytesToWrite, dwBytesWritten, NULL);
}
Esta es una técnica ya bastante antigua y muchas veces no es suficiente para hacer bypass a un EDR/AV y es aquí donde entran los syscalls. Cuando una función es invocada en ring3 (user-mode) muchas veces necesita ir a ring0 y continuar el flujo a través del kernel hasta realizar la operación:
Si ejecutamos el código anterior, creará un archivo manifest.txt
haciendo uso de CreateFileA
, sin embargo esta función ubicada en kernel32.dll
no es la “última” función en ser invocada, llega a través de un flujo que pasa por NtWriteFile (ntdll) y termina en ntoskrnl.exe(kernel-mode) . Si revisamos esta función con Windbg se puede ver algo así:
Cabe decir que ntdll.dll es la abstracción más básica de las APIs de windows, es decir casi todo pasa por ahi para llegar al kernel. Por ejemplo para este caso se puede ver un mov eax, 8
, y posteriormente un syscall
, que es donde ocurre todo. El 0x8 hace referencia a NtWriteFile para Windows 10 x64 nativo, el cual vendría a ser el syscall para esa función. Para las versiones de 32 bits funciona casi de la misma forma, hace una llamada directa al syscall para poder entrar a ring0, es decir continuar el flujo en kernel-mode. Sin embargo no siempre es así, ya que si ejecuto una aplicación de 32 bits en 64 bits habrá una transición previa que permita ejecutar esas instrucciones, no lo explicaré ahora ya que es parte de arquitectura wow64, pero lo dejo como futura referencia.
PS: eax 1A0008h
es el argumento para la llamada al syscall. En la dirección siguiente (771c2a35)
se puede ver que hace un call a Wow64SystemServiceCall
, y es que en una arquitectura nativa de x64 se hace una call directa a syscall como ya vimos; mientras que aqui sigue un flujo diferente para llegar a ring0; para explicar esto haré mas adelante una publicación de arquitectura wow64.
Ya con esto se puede entender básicamente lo que hacen las syscalls, ahora la pregunta es, porque son importantes? o porque se deben usar si usarlas implica más trabajo y puede resultar tedioso hacerlo por cada uno y hay que tener en cuenta que esto es por build de Windows. Pues la respuesta corta es la siguiente:
Como se puede ver en la imagen anterior, cuando se desarrolla un malware puede usarse la forma tradicional e invocar funciones del Win32 API, pero pasa que algunos EDR/AV hacen un hooking en user-mode para identificar que procesos son llamados y que operaciones hacen; lo cual puede dar como resultado que sea catalogado como malware. Para evitar esto muchas técnicas están enfocadas en programar usando syscalls, es decir obtener el stub del syscall para invocar directamente la función. Ya con esto creo queda más claro la utilidad de los syscalls.
Implementando syscalls
Ya con parte de la teoría entendida toca implementar syscalls es decir conseguir la instrucción y estructura de una función de ntdll.dll y posteriormente con la estructura obtenida construir la función y enviar directamente los argumentos. En este punto debo aclarar algo, el stub del syscall, es decir las instrucciones que tiene la función que voy a invocar, siempre tienen algo similar: los 4 primeros bytes. a4c 8b d1 b8
Esto es importante mencionarlo porque aquí viene lo que expliqué líneas arribas; que un EDR/AV a estas funciones de user-mode les hace hook para monitorear que calls se están haciendo. En esos casos los 4 primeros bytes serán diferentes pues tendrán un jmp hacia el EDR/AV. Entonces, suponiendo que tengo el siguiente código:
//https://docs.microsoft.com/en-us/windows/win32/fileio/opening-a-file-for-reading-or-writing
#include<windows.h>
int main(){
char DataBuffer[] = "Hacking n00b";
DWORD dwBytesToWrite = (DWORD)strlen(DataBuffer);
DWORD dwBytesWritten = 0;
BOOL bErrorFlag = FALSE;
hFile = CreateFileA("manifest.txt",
GENERIC_WRITE,
0,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
NULL);
WriteFile( hFile, DataBuffer, dwBytesToWrite, dwBytesWritten, NULL);
}
Se puede ver que hay un CreateFileA
; la pregunta es, como puedo transformar esto para que funcione con syscalls. Las funciones que son invocadas en ring3 la mayoría de veces termina en una ejecución de ring0, esto quiere decir que en este ejemplo, la función CreateFileA
está dentro de una interfaz simple para que un desarrollador pueda invocarla fácilmente, en este caso dentro de kernel32.dll
; pero el flujo sigue y posteriormente invoca a NtCreateFile
dentro de ntdll; que es donde se encuentra la llamada a la syscall. Entonces para poder invocar la syscall primero tengo que saber en que parte del ntdll
está, para así poder obtener las instrucciones que tiene y luego ejecutarla. Para lograr esto se pueden usar herramientas ya existentes o simplemente hacer debug a una función hasta que entra a ntdll.dll, es decir hasta que invoca alguna función Nt o Zw. Para este ejemplo usaré CreateFileA
:
Como se puede ver cuando inicia la función; esta inmediatamente hace un salto a hacia kernelbase.dll
Y si seguimos el flujo se puede ver que hay un call
hacia NtCreateFile
. Este es el salto hacia ntdll que estaba buscando.
Finalmente llega hasta el syscall
que es la entrada a ring0, no llegaremos a hacer debug en kernel porque está fuera de este alcance; posteriormente escribiré sobre la continuación de esto.
Ahora tenemos la estructura; los primeros 4 bytes lo confirman a4c 8b d1 b8
. Esto es lo que tengo que llevar al código en C, asignar un una estructura definida y ya luego hacer uso de esa función que se va a generar; tomando en cuenta claro la (no)documentación de estas funciones, las cuales puedes ver aquí: http://undocumented.ntinternals.net/. Finalmente para automatizar este proceso, hice un pequeño código en C que obtiene esta estructura de bytes para que pueda ser asignada a una estructura definida.
//x86_64-w64-mingw32-gcc getopcode.c -o opcode.exe -l ntdll
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include "winternl.h"
size_t SYSCALL_LEN = 21;
void _error(char * message) {
printf("%s: 0x%.8x\n", message, (unsigned int) GetLastError());
}
//load dll
HMODULE loadDLL(char * dll) {
HMODULE hDLL;
if ((hDLL = LoadLibraryA(dll)) == NULL) {
_error("Load failed dll");
return NULL;
}
return hDLL;
}
BOOL getFnProcess(HMODULE lib, char * txtGetProcAddress, LPVOID syscall) {
PIMAGE_DOS_HEADER imgDosHeader = (IMAGE_DOS_HEADER * ) lib;
PIMAGE_NT_HEADERS imgNtHeaders = (IMAGE_NT_HEADERS * )((size_t) lib + imgDosHeader -> e_lfanew);
PIMAGE_OPTIONAL_HEADER imgOptHeader = & imgNtHeaders -> OptionalHeader;
PIMAGE_DATA_DIRECTORY imgDataDirectory = (IMAGE_DATA_DIRECTORY * )( & imgOptHeader -> DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
PIMAGE_EXPORT_DIRECTORY imgExportDirectory = (IMAGE_EXPORT_DIRECTORY * )((size_t) lib + imgDataDirectory -> VirtualAddress);
DWORD * addrOfFn = (DWORD * )((size_t) lib + imgExportDirectory -> AddressOfFunctions);
DWORD * addrOfNames = (DWORD * )((size_t) lib + imgExportDirectory -> AddressOfNames);
WORD * addrOfNameOrdinals = (WORD * )((size_t) lib + imgExportDirectory -> AddressOfNameOrdinals);
char * targetFnText = NULL;
DWORD fnAddress = -1;
for (int index = 0; index < imgExportDirectory -> NumberOfFunctions; index++) {
targetFnText = (char * )((size_t) lib + addrOfNames[index]);
if (_stricmp(txtGetProcAddress, targetFnText) == 0) {
fnAddress = addrOfFn[addrOfNameOrdinals[index]];
printf("[*] Found %s -> %x\n", targetFnText, fnAddress);
memcpy(syscall, ((DWORD * )((size_t) lib + fnAddress)), SYSCALL_LEN);
//printf("%s", targetFnText);
//printf("\n");
break;
}
}
if (fnAddress == -1) {
_error("Not found function");
return FALSE;
}
return TRUE;
}
DWORD getAddrProcess(char * lib, char * fn, LPVOID destSysCall) {
HMODULE hLib = loadDLL(lib);
if (hLib == NULL) return -1;
return getFnProcess(hLib, fn, destSysCall);
}
int main(int argc, char ** argv) {
printf("\n** pheias 0.1 **\n\n");
if (argc < 2) {
printf("[*] Usage: .\\pheias.exe [NTDLL Function]\n");
exit(-1);
}
//until ret
char syscall[SYSCALL_LEN];
//define function and library to search
char * procAddress = argv[1];
char * dll = "ntdll";
//standard syscall stub
char syscall_std[4] = {
0x4c,
0x8b,
0xd1,
0xb8
};
if (getAddrProcess(dll, procAddress, syscall)) {
if (memcmp(syscall_std, syscall, 4) == 0) {
printf("[*] Not hooking detected\n");
printf("[*] Opcode %s ->", procAddress);
char * b = ((char * ) syscall);
for (int i = 0; i < SYSCALL_LEN; i++) {
printf("\\x%.2x", (BYTE) b[i]);
}
printf("\n");
printf("[*] Syscall number -> 0x%x", (BYTE) b[4]);
printf("\n");
} else {
//detect if the fn is hooked by any EDR/AV
printf("[!] Function %s hooked!!!\n", procAddress);
}
}
printf("\n");
}
Al ejecutarlo se obtiene el opcode de la función y obviamente el syscall al que corresponde; ya con esto solo queda estructurar la salida y el malware NO estaría pasando por ninguna dll si no estaría yendo directo a la llamada en kernel. Sin embargo los EDR/AVs también llegan por ahí; pero ese ya es un problema a soluciona luego.
Conclusión y siguientes pasos
La explicación la dejaré hasta aquí ya que a continuación solo queda 1 paso, es el de definir la estructura de la función y tomar como referencia la (no)documentación de windows y especificar los argumentos necesarios para la función. En el output de la consola se puede ver no solo el Opcode de la función NtCreateFile, si no también el numero de syscall que luego va al SSDT y continua por ring0. La ejecución de la función tendría que ir algo así.
C_NtCreateFile NtCreateFile = (C_NtCreateFile)(LPVOID) syscall
Claro aquí falta la definición de C_NtCreateFile
y en que forma se ingresan los parámetros; pero eso ya será para otra publicación. Como pudo verse obtener el stub de la syscall no es muy complicado y termina resultando útil. Escribí esto como introducción (o idea general) sobre los syscalls porque luego publicaré acerca de shellcoding usando syscalls, y pues era necesario hacer explicar algunas cosas antes.