PEBCuando 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:
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.
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.
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.
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:
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ó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:
((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]