========================[ Invisibilidad en sistemas NT ]======================== Como ser invisible en Windows NT -------------------------------- Autor: Holy_Father Traduccion: Kintaro Version: 1.2 spanish Fecha: 01.01.2005 =====[ 1. Contenidos ]========================================================== 1. Contenidos 2. Introduccion 3. Ficheros 3.1 NtQueryDirectoryFile 3.2 NtVdmControl 4. Procesos 5. Registro 5.1 NtEnumerateKey 5.2 NtEnumerateValueKey 6. Servicios de sistema y drivers 7. Expandiendo los ganchos 7.1 Derechos 7.2 Gancho global 7.3 Nuevos procesos 7.4 DLL 8. Memoria 9. Handle (Manejadores) 9.1 Nombrando el handle y obteniendo el tipo 10. Puertos 10.1 Netstat, OpPorts en WinXP, FPort en WinXP 10.2 OpPorts en Win2k y NT4, FPort en Win2k 11. Terminando =====[ 2. Introduccion ]======================================================== Este documento trata sobre tecnicas de esconder objetos, ficheros, servicios, procesos, etc. en sistemas Windows NT. Estos metodos estan basados en lso ganchos a funciones de la Windows API que estan descritos en mi documentos "Hooking Windows API". Todo esto lo aprendi por mis investigaciones durante el desarrollo del codigo del rootkit, asique es posible que pueda ser escrito mejor y mas facilmente de lo que yo lo hago. Esto incluye mi implementacion del rootkit. Ocultacion de objeto arbitrario, en este documento, significa cambiar algunas funciones que muestran estos objetos de manera que evitemos que muestren el objeto. En el caso de que ese objeto fuese el valor de retorno de esa funcion, nosotros devolveriamos el valor como si el objeto no existiese. El metodo basico (excluyendo casos en que indique diferente) es que llamaremos la funcion original con los argumentos originales y lo que cambiariamos seria su salida. En esta de este texto se describen metodos de ocultar ficheros, procesos, claves y valores de registros, servicios de sistema y drivers, reservas de memoria y handles. =====[ 3. Ficheros ]============================================================ Hay varias posibilidades de ocultar ficheros de manera que no los vea el SO. Nosotros nos dedicaremos solo a modificar la APi, olvidandonos de aquellas tecnicas que realizan cambios en el sistema de ficheros. Asi es mucho mas facil ya que no necesitmos conocer el sistema de ficheros en particular. =====[ 3.1 NtQueryDirectoryFile ]=============================================== La busqueda de ficheros en WNT en algun directorio se basa en buscar todos sus ficheros y todos sus subdirectorios con ficheros. Para enumerar ficheros se usa la funcion NtQueryDirectoryFile. NTSTATUS NtQueryDirectoryFile( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG FileInformationLength, IN FILE_INFORMATION_CLASS FileInformationClass, IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileName OPTIONAL, IN BOOLEAN RestartScan ); Los parametros importantes para nosotros son FileHandle, FileInformation y FileInformationClass. FileHandle es el handle del directorio, el cual se obtiene con NtOpenFile. FileInformation es un puntero a una zona de memoria donde se escribira la informacion pedida. FileInformationClass determina el tipo de registro que se escribira en FileInformation. FileInformationClass es un tipo enumerado, y son cuatro los valores que necesitamos para la enumeracion de un directorio. #define FileDirectoryInformation 1 #define FileFullDirectoryInformation 2 #define FileBothDirectoryInformation 3 #define FileNamesInformation 12 la estructura escrita es FileInformation con FileDirectoryInformation es: typedef struct _FILE_DIRECTORY_INFORMATION { ULONG NextEntryOffset; ULONG Unknown; LARGE_INTEGER CreationTime; LARGE_INTEGER LastAccessTime; LARGE_INTEGER LastWriteTime; LARGE_INTEGER ChangeTime; LARGE_INTEGER EndOfFile; LARGE_INTEGER AllocationSize; ULONG FileAttributes; ULONG FileNameLength; WCHAR FileName[1]; } FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION; con FileFullDirectoryInformation: typedef struct _FILE_FULL_DIRECTORY_INFORMATION { ULONG NextEntryOffset; ULONG Unknown; LARGE_INTEGER CreationTime; LARGE_INTEGER LastAccessTime; LARGE_INTEGER LastWriteTime; LARGE_INTEGER ChangeTime; LARGE_INTEGER EndOfFile; LARGE_INTEGER AllocationSize; ULONG FileAttributes; ULONG FileNameLength; ULONG EaInformationLength; WCHAR FileName[1]; } FILE_FULL_DIRECTORY_INFORMATION, *PFILE_FULL_DIRECTORY_INFORMATION; con FileBothDirectoryInformation: typedef struct _FILE_BOTH_DIRECTORY_INFORMATION { ULONG NextEntryOffset; ULONG Unknown; LARGE_INTEGER CreationTime; LARGE_INTEGER LastAccessTime; LARGE_INTEGER LastWriteTime; LARGE_INTEGER ChangeTime; LARGE_INTEGER EndOfFile; LARGE_INTEGER AllocationSize; ULONG FileAttributes; ULONG FileNameLength; ULONG EaInformationLength; UCHAR AlternateNameLength; WCHAR AlternateName[12]; WCHAR FileName[1]; } FILE_BOTH_DIRECTORY_INFORMATION, *PFILE_BOTH_DIRECTORY_INFORMATION; y con FileNamesInformation: typedef struct _FILE_NAMES_INFORMATION { ULONG NextEntryOffset; ULONG Unknown; ULONG FileNameLength; WCHAR FileName[1]; } FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION; Esta funcion escribe una lista de estas estructuras en FileInformation. Solo tres varibles nos importan en cualquiera de estos tipos de estructuras. NextEntryOffset es la longitud en bytes del nodo estructura actual. El primer nodo empieza en la direccion FileInformation + 0. El segundo en FileInformation + NextEntryOffset (NextEntryOffset del primer nodo). El ultimo nodo tiene su campo NextEntryOffset puesto a cero. FileName es el nombre completo del fichero. FileNameLength es la longitud del nombre. Si queremos ocultar un fichero, debemos tener en cuenta estos cuatro tipos y para cada registro devuelto comparamos su nombre con el que queremos ocultar. Si queremos ocultar el primer registro tenemos que mover las estructuras siguientes al lugar de la primera. Esto causara la sobreescritura del primer registro. Si queremos ocultar otro registro, cambiamos el valor de NextEntryOffset del registro que le precede. (Nota del traductor: Aqui hay 2 posibilidades: 1)Mover solamente el segundo registro 2)Mover todos los registros siguientes La 1º es algo mas rapida y tambien mas facil, la 2º es mejor :). En la 1º moveriamos el segundo registro sobre el primero, sobreescribiendolo, y cambiamos el NextEntryOffset de este registro que acabamos de escribr al valor NextEntryOffset nuevo=NextEntryOffset del 1º registro + NextEntryOffset del 2º pero esto tiene un problema: si a alguien le da por leer esta zona de memoria vera que hay gato encerrado, detras del primer registro encontraria una copia del mismo registro,y este no estaria apuntado por el anterior. Un poco de buena vista bastara para detectarnos! En vez de eso podemos recurrir a la forma 2. Esta forma consiste en sobreescribir ese registro fantasma con los regitros siguientes, es decir desplazar todos los registros siguientes hacia el primera registro, de manera que todos los registros estan encadenados de forma contigua, no hay en memoria una zona desapuntada ni una copia de ningun otro registro. bueno, ahora si estas entendiendo esto seguramente no estaras de acuerdo en todo, porque en realidad no es del todo cierto: si que hay una copia de algun registro. ¿De cual? Pues del ultimo no? En este momento todo funcionaria bien, pero podria seguir siendo detectable de manera parecida a la anterior, aunque mas difícilmente, pero se puede mejorar, qué se os ocurre hacer? lo logico seria sobreescribirlo con los datos que hubiese antes tras el ultimo registro, pues vamos alla. ponemos el tamaño del ultimo registro fantasma a NULL. Ahora la detección esta mucho mas difícil, pero de nuevo no es perfecta! Existe una variable de salida de la función con el numero de bytes escritos, entonces un avispado forense podria comprobar esto y detectarnos de nuevo :/ . La solución aqui consiste en hacer una nueva llamada a la función pasandole como puntero del buffer la dirección justo despues del último registro. Ahora la variable con el número de bytes escritos contendrá el numero de bytes escritos en esta ocación. Pero seguiremos haciendo esta llamada hasta que en una de ella el resultado sea "espacio insuficiente", entonces la variable de bytes escritos ya no puede comprobarse ,jeje :P Para los curiosos, los posibles valores de retorno de la funcion, de tipo NTSYSAPI NTSTATUS NTAPI estan listados en el ntstatus.h ) Si queremos ocultar el ultimo registro, el nuevo valor de NextEntryOffset seria cero, en otro caso el valor seria la suma del NextEntryOffset del registro precedente y del NextEntryOffset del registro a esconder. En este caso deberiamos cambiar el valor de Unknown del registro previo, que es probablemente un indice para la busqueda siguiente. El valor de Unknown del registro previo debe contener el valor del Unknown del registro a ocultar. Si no se encuentran mas ficheros que sí queramos mostrar, devolveremos STATUS_NO_SUCH_FILE. #define STATUS_NO_SUCH_FILE 0xC000000F =====[ 3.2 NtVdmControl ]======================================================= Por razones desconocidas, la emulacion de DOS NTVDM puede obtener tambien una lista de ficheros con NtVdmContol. NTSTATUS NtVdmControl( IN ULONG ControlCode, IN PVOID ControlData ); ControlCode especifica la subfuncion que se aplica a los datos del buffer ControlData. Si ControlCode es VdmDirectoryFile, esta funcion hace lo mismo que NtQueryDirectoryFile con FileInformationClass puesto a FileBothDirectoryInformation. #define VdmDirectoryFile 6 Entonces ControlData es usado como FileInformation. La unica diferencia aqui es que no sabemos la longitud de este buffer. Asique tenemos que contarlo manualmente. Tenemos que sumar NextEntryOffset de todos los registros y FileNameLength del ultimo registro y 0x5E como longitud del ultimo registro excluyendo el nombre del fichero. Los metodos de ocultacion son los mismos que en el caso de NtQueryDirectoryFile. =====[ 4. Procesos ]============================================================ Cierta informacion esta disponible usando NtQuerySystemInformation. NTSTATUS NtQuerySystemInformation( IN SYSTEM_INFORMATION_CLASS SystemInformationClass, IN OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength OPTIONAL ); SystemInformationClass especifica el tipo de informacion que queremos obtener. SystemInformation es un puntero al buffer de salida. SystemInformationLength es la longitud de este buffer. ReturnLength es el numero de bytes escritos. Para la enumeracion de procesos en ejecucion pasamos como SystemInformationClass la constante SystemProcessesAndThreadsInformation #define SystemInformationClass 5 Estructura devuelta en el buffer SystemInformation: typedef struct _SYSTEM_PROCESSES { ULONG NextEntryDelta; ULONG ThreadCount; ULONG Reserved1[6]; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ProcessName; KPRIORITY BasePriority; ULONG ProcessId; ULONG InheritedFromProcessId; ULONG HandleCount; ULONG Reserved2[2]; VM_COUNTERS VmCounters; IO_COUNTERS IoCounters; // Windows 2000 solo SYSTEM_THREADS Threads[1]; } SYSTEM_PROCESSES, *PSYSTEM_PROCESSES; Ocultar procesos es igual que en el caso de ocultar ficheros. Tenemos que cambiar NextEntryDelta del registro previo a aquel que queremos ocultar. Usualmente no querremos cambiar nunca el primero registro de todos, pues este es el proceso Idle (Proceso inactivo de sistema) =====[ 5. Registro ]============================================================ El registro de windows es bastante grande en cuanto a la estructura de arbol, conteniendo dos tipos importantes de registros para nosotros que podriamos querer ocultar. El primer tipo son las claves de registro, y el segundo son los valores. La ocultacion del registro no es tan trivial como la ocultacion de ficheros o procesos. =====[ 5.1 NtEnumerateKey ]===================================================== Debido a su estructura no nos es posible obtener una lista de todas las claves en una parte determinada del registro. Tan solo podemos obtener informacion sobre una clave especificada por su indice en algun lugar del registro. La funcion que provee de esta informacion es NtEnumerateKey. NTSTATUS NtEnumerateKey( IN HANDLE KeyHandle, IN ULONG Index, IN KEY_INFORMATION_CLASS KeyInformationClass, OUT PVOID KeyInformation, IN ULONG KeyInformationLength, OUT PULONG ResultLength ); KeyHandle es un handle a una clave, y queremos solicitar la informacion de una subclave especificada por Index. El tipo de la informacion de salida se especifica mediante KeyInformationClass. Los datos se escriben en el buffer de salida KeyInformation, donde KeyInformationLength es el tamaño. ResultLength es el numero de bytes escritos. Lo mas importante a tener en cuenta es que si escondemos una clave, los indices de las claves siguientes serian todos desplazados. Entonces para obtener informacion de una clave con un indice mayor pediremos informacion sobre la clave con un indice menor, por tanto tenemos que contar cuantos registros hay antes de ocultar alguno y entonces nosotros devolvemos el correcto. Echad un vistazo a este ejemplo. Pongamos que tenemos tres claves: A, B, C, D, E, y F en alguna parte del registro. EL indice empieza en cero, por lo que el indice 4 corresponde a la clave E. Ahora si queremos esconder B y la aplicacion enganchada llama a NtEnumerateKey con Index a 4, nosotros deberiamos devolver informacion sobre F, porque el indice se ha desplazado. El problema es que no sabemos que desplazamiento ha habido. Y si no nos preocupamos de los desplazamientos y devolvemos E en lugar de F cuando se pida la clave con Index 4, entonces no deberiamos devolver nada cuando se pida la clave con Index 1, o bien devolveriamos C. Ambos casos son erroneos. Esto es por lo que tenemos que que tener cuidado con los desplazamientos. Ahora si contamos el desplazamiento mediante rellamadas a la funcion por cada indice de 0 a Index, entonces habria veces que deberiamos esperar años ( en un procesador de 1GHz tardaria 10 segundos con el registro estandar, lo cual es demasiado). Asique tenemos que resolverlo por otro metodo mas sofisticado. Sabemos que las claves (a excepcion de las referencias) estan ordenadas alfabeticamente. Si pasamos de las referencias (que no las queremos esconder) podemos contar el desplazamiento por el siguiente metodo. Ordenaremos alfabeticamente nuestra lista de nombres de claves que queremos ocultar (con RtlCompareUnicodeString por ejemplo), entonces cuando la aplicacion llame a NtEnumerateKey, no la rellamaremos con los argumentos inalterados sino que descubriremos el nombre del registro especificado por Index. NTSTATUS RtlCompareUnicodeString( IN PUNICODE_STRING String1, IN PUNICODE_STRING String2, IN BOOLEAN CaseInSensitive ); String1 y String2 son las cadenas a comparar, CaseInSensitive es True queremos comparar con distincion de mayuculas y minusculas. El resultado de la funcion describe la relacion entre String1 y String2: resultado > 0: String1 > String2 resultado = 0: String1 = String2 resultado < 0: String1 < String2 Ahora tenemos que encontrar un punto fronterizo en la lista. Compararemos alfabeticamente el nombre de la clave especificada por Index con los nombres de nuestra lista. El punto fronterizo seria el ultimo nombre menor de nuestra lista (el siguiente seria el mismo nombre). Sabemos que el desplazamiento es como mucho el numero de nombres que hay antes del punto fronterizo en nuestra lista. Pero no todos los items de nuestra lista tiene que ser una clave existente en la parte del registro en la que estamos. Asique pedimos para todos los items de nuestra lista hasta el punto frontera si estos estan en esta parte del registro. Esto se hace con NtOpenKey. NTSTATUS NtOpenKey( OUT PHANDLE KeyHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes ); KeyHandle es un handle de una superclave. Usaremos el valor de NtEnumerateKey aqui. DesiredAccess son los derechos de acceso. El valor correcto es KEY_ENUMERATE_SUB_KEYS. ObjectAttributes describe la subclave que queremos abrir (incluyendo su nombre). #define KEY_ENUMERATE_SUB_KEYS 8 si el resultado de NtOpenKey es 0 la apertura de clave se realizo con exito, lo que significa que esta clave de nuestra lista existe. La clave abierta se cierra con NtClose. NTSTATUS NtClose( IN HANDLE Handle ); Para cada llamada a NtEnumerateKey contaremos el desplazamiento como el numero de claves de nuestra lista que existen en la parte dada del registro. Entonces añadiremos este desplazamiento al argumento Index y finalmente llamaremos a la NtEnumerateKey original. Para obtener del nombre de la clave especificada por Index usaremos KeyBasicInformation como KeyInformationClass. #define KeyBasicInformation 0 NtEnumerateKey devuelve esta estructura en KeyInformation: typedef struct _KEY_BASIC_INFORMATION { LARGE_INTEGER LastWriteTime; ULONG TitleIndex; ULONG NameLength; WCHAR Name[1]; } KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION; Lo único que necesitamos aqui es Name y NameLength. Si no hay entrada para el Index desplazado devolveremos error STATUS_EA_LIST_INCONSISTENT. #define STATUS_EA_LIST_INCONSISTENT 0x80000014 =====[ 5.2 NtEnumerateValueKey ]================================================ Los valores de registro no estan ordenados alfabeticamente. Afortunadamente el numero de valores en una clave es bastante pequeño y podemos usar el metodo de rellamada para los desplazamientos NTSTATUS NtEnumerateValueKey( IN HANDLE KeyHandle, IN ULONG Index, IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation, IN ULONG KeyValueInformationLength, OUT PULONG ResultLength ); KeyHandle es de nuevo un handle de una superclave. Index es un indice de la lista de valores en la clave dada. KeyValueInformationClass describe un tipo de informacion que sera almacenado en el buffer KeyValueInformation, que es ocupa KeyValueInformationLength bytes. El numero de bytes escritos se develve en ResultLength. De nuevo podemos contar el desplazamiento pero segun el numero de valores en una clave podemos rellamar esta funcion para todos los indices desde 0 a Index. El numero del valor puede obtenerse cuando KeyValueInformationClass es puesto a KeyValueBasicInformation. #define KeyValueBasicInformation 0 Entonces obtendremos la siguiente estructura en el buffer KeyValueInformation : typedef struct _KEY_VALUE_BASIC_INFORMATION { ULONG TitleIndex; ULONG Type; ULONG NameLength; WCHAR Name[1]; } KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION; De nuevo lo que nos interesa solo es Name y Namelength. Si no hay entrada para el Index desplazado devolveremos STATUS_NO_MORE_ENTRIES. #define STATUS_NO_MORE_ENTRIES 0x8000001A =====[ 6. Servicios de sistema y drivers ]====================================== Los servicios de sistema y drivers se enumeran mediante cuatro funciones API, independientes unas de otras. La conexion entre ellas es diferente en cada version de Windows. Por eso es por lo que tenemos que usar ganchos con las cuatro funciones. BOOL EnumServicesStatusA( SC_HANDLE hSCManager, DWORD dwServiceType, DWORD dwServiceState, LPENUM_SERVICE_STATUS lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle ); BOOL EnumServiceGroupW( SC_HANDLE hSCManager, DWORD dwServiceType, DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle, DWORD dwUnknown ); BOOL EnumServicesStatusExA( SC_HANDLE hSCManager, SC_ENUM_TYPE InfoLevel, DWORD dwServiceType, DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle, LPCTSTR pszGroupName ); BOOL EnumServicesStatusExW( SC_HANDLE hSCManager, SC_ENUM_TYPE InfoLevel, DWORD dwServiceType, DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle, LPCTSTR pszGroupName ); Aqui lo importante es lpServices, que apunta al buffer donde se va a almacenar la lista de servicios. Y tambien nos importa lpServicesReturned que indica el numero de registros que se han creado en el buffer. La estructura de datos que sera grabada al buffer de salida depende de las funciones. Las funciones EnumServicesStatusA y EnumServicesGroupW devuelven una estructura del tipo: typedef struct _ENUM_SERVICE_STATUS { LPTSTR lpServiceName; LPTSTR lpDisplayName; SERVICE_STATUS ServiceStatus; } ENUM_SERVICE_STATUS, *LPENUM_SERVICE_STATUS; typedef struct _SERVICE_STATUS { DWORD dwServiceType; DWORD dwCurrentState; DWORD dwControlsAccepted; DWORD dwWin32ExitCode; DWORD dwServiceSpecificExitCode; DWORD dwCheckPoint; DWORD dwWaitHint; } SERVICE_STATUS, *LPSERVICE_STATUS; y EnumServicesStatusExA y EnumServicesStatusExW otras como estas: typedef struct _ENUM_SERVICE_STATUS_PROCESS { LPTSTR lpServiceName; LPTSTR lpDisplayName; SERVICE_STATUS_PROCESS ServiceStatusProcess; } ENUM_SERVICE_STATUS_PROCESS, *LPENUM_SERVICE_STATUS_PROCESS; typedef struct _SERVICE_STATUS_PROCESS { DWORD dwServiceType; DWORD dwCurrentState; DWORD dwControlsAccepted; DWORD dwWin32ExitCode; DWORD dwServiceSpecificExitCode; DWORD dwCheckPoint; DWORD dwWaitHint; DWORD dwProcessId; DWORD dwServiceFlags; } SERVICE_STATUS_PROCESS, *LPSERVICE_STATUS_PROCESS; Nada mas que nos interesa lpServiceName, que es el nombre del servicio de sistema. Los registros tienen tamaño estatico, asique ocultar uno es facil, solo hay que mover los registros siguientes hacia el que queremos ocultar, sobreescribiendolo. Tenemos que diferenciar entre el tamaño de SERVICE_STATUS y el de SERVICE_STATUS_PROCESS. =====[ 7. Expandiendo los ganchos ]============================================= Para conseguir el efecto deseado tenemos que meter los ganchos a todos los procesos en ejecucion, y tambien a los procesos nuevos que se creen despues. Los procesos nuevos deberian ser enganchados antes de que se ejecute su primera instruccion, de otra forma podria ver los objetos que hemos ocultado en ese lapso de tiempo antes de engancharlo. =====[ 7.1 Privilegios ]======================================================== Debes saber que lo primero que necesitamos es, como minimo, privilegios de administrador, para acceder a todos los procesos en ejecucion. La mejor posibilidad es ejecutar nuestro proceso como servicio de sistema, que es ejecutado como usuario SYSTEM en la maquina. Para instalar el servicio tambien necesitamos privilegios especiales. Tambien SeDebugPrivilege es muy util. Puede hacerse usando OpenProcessToken, LookupPrivilegeValue y AdjustTokenPrivileges. BOOL OpenProcessToken( HANDLE ProcessHandle, DWORD DesiredAccess, PHANDLE TokenHandle ); BOOL LookupPrivilegeValue( LPCTSTR lpSystemName, LPCTSTR lpName, PLUID lpLuid ); BOOL AdjustTokenPrivileges( HANDLE TokenHandle, BOOL DisableAllPrivileges, PTOKEN_PRIVILEGES NewState, DWORD BufferLength, PTOKEN_PRIVILEGES PreviousState, PDWORD ReturnLength ); El codigo sera algo parecido a esto: #define SE_PRIVILEGE_ENABLED 0x0002 #define TOKEN_QUERY 0x0008 #define TOKEN_ADJUST_PRIVILEGES 0x0020 HANDLE hToken; LUID DebugNameValue; TOKEN_PRIVILEGES Privileges; DWORD dwRet; OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,hToken); LookupPrivilegeValue(NULL,"SeDebugPrivilege",&DebugNameValue); Privileges.PrivilegeCount=1; Privileges.Privileges[0].Luid=DebugNameValue; Privileges.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken,FALSE,&Privileges,sizeof(Privileges), NULL,&dwRet); CloseHandle(hToken); =====[ 7.2 Gancho global ]====================================================== La enumeracion de procesos es hecha por los metodos ya mencionados de la funcion NtQuerySystemInformation. Hay unos pocos procesos nativos en el sistema, con los que usaremos el metodo de gancheo de reescribir las primeras instrucciones de la funcion. Haremos lo mismo para para todos los procesos en ejecucion. Reservaremos una parte de memoria en el proceso victima donde escribiremos nuestro nuevo codigo para las funciones que queramos enganchar. Entonces cambiaremos los cinco primeros bytes de estas funciones con la instruccion jmp. El salto redirigira el flujo de ejecucion a nuestro codigo. Asique el jmp sera ejecutado inmediatamente despues de llamar a la funcion. EL como salvar las instrucciones ya fue descrito en el capitulo 3.2.3. del documento "Hooking Windows API". Lo primero que tenemos que abrir es el proceso victima con NtOpenProcess y obtener su handle. Si no tenemos suficientes privilegios esto fallara. NTSTATUS NtOpenProcess( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL ); ProcessHandle es un puntero a un handle donde se va a almacenar el resultado. DesiredAccess debe ponerse al valor PROCESS_ALL_ACCESS. Pondremos el PID del proceso victima a UniqueProcess, en la estructura ClientId, y UniqueThread lo debemos poner a 0. El handle abierto siempre hay que cerrarlo con NtClose. #define PROCESS_ALL_ACCESS 0x001F0FFF Ahora vamos a reservar la parte de memoria que necesitamos para nuestro codigo. Para ello utilizamos NtAllocateVirtualMemory. NTSTATUS NtAllocateVirtualMemory( IN HANDLE ProcessHandle, IN OUT PVOID *BaseAddress, IN ULONG ZeroBits, IN OUT PULONG AllocationSize, IN ULONG AllocationType, IN ULONG Protect ); (***Nota del traductor: En la version original de este documento, holy_father escribe el segundo argumento como IN OUT PVOID BaseAddress, sin embargo en la documentacion de la funcion aparece IN OUT PVOID *BaseAddress, donde el asterisco indica puntero doble, yo lo he puesto como aparece en la documentacion) ProcessHandle es el obtenido con NtOpenProcess. BaseAddress es un puntero a otro puntero al comienzo de la zona que queremos reservar. Aqui se almacenara la direccion de la memoria reservada. El de entrada tambien puede ser cero ( en ese caso la direccion sera elegida por el sistema). AllocationSize es un puntero al numero de bytes que queremos reservar. Y de nuevo tambien es usado como valor de salida para el numero de bytes reservados. Es bueno poner AllocationType a MEM_TOP_DOWN junto con MEM_COMMIT porque asi la memoria sera reservada en la posicion mas alta posible. #define MEM_COMMIT 0x00001000 #define MEM_TOP_DOWN 0x00100000 Ahora ya podemos escribir nuestro codigo con NtWriteVirtualMemory. NTSTATUS NtWriteVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); BaseAddress sera la direccion devuelta por NtAllocateVirtualMemory. El buffer apunta a los bytes que queremos escribir, y BufferLength es el numero de bytes que queremos escribir. Ahora tenemos que poner ganchos a funciones sueltas. La unica libreria que es cargada independientemente por todos los procesos es ntdll.dll. Asi pues tendremos que comprobar si la funcion que queremos ganchear es importada o no desde ntdll.dll. Pero la mempria donde estaria esta funcion podria estar reservada , asique reescribir bytes en su direccion podria provocar un error en el proceso. Es por esto por lo que tenemos que comprobar si la libreria (donde esta la funcion que queremos ganchear) esta cargada en el proceso victima o no. Necesitamos obtener el PEB (Process Environment Block, Bloque de entorno de proceso) mediante NtQueryInformationProcess. NTSTATUS NtQueryInformationProcess( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL ); Pondremos ProcessInfromationClass a ProcessBasicInformation. Entonces la estructura PROCESS_BASIC_INFORMATION seria devuelta en el buffer ProcessInformation, cuyo tamaño es dado por ProcessInformationLength. #define ProcessBasicInformation 0 typedef struct _PROCESS_BASIC_INFORMATION { NTSTATUS ExitStatus; PPEB PebBaseAddress; KAFFINITY AffinityMask; KPRIORITY BasePriority; ULONG UniqueProcessId; ULONG InheritedFromUniqueProcessId; } PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION; PebBaseAddress es lo que estamos buscando. En PebBaseAddress+0x0C esta la direccion PPEB_LDR_DATA. Esto se obtiene llamando a NtReadVirtualMemory. NTSTATUS NtReadVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, OUT PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); Los parametros son igual que en NtWriteVirtualMemory. En PPEB_LDR_DATA+0x1C esta la direccion de InInitializationOrderModuleList. Es la lista de librerias cargadas al proceso. Nos interesa solo una parte de esta estructura. typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST { PVOID Next, PVOID Prev, DWORD ImageBase, DWORD ImageEntry, DWORD ImageSize, ... ); Next es un puntero al siguiente registro, Prev al anterior, el ultimo registro apunta al primero. ImageBase es la direccion del modulo en la memoria, ImageEntry es el EntryPoint del modulo, ImageSize el tamaño. Para todas las librerias que queremos enganchar tenemos que obtener su ImageBase (por ejemplo usando GetModuleHandle or LoadLibrary). Esta ImageBase la comparamos con ImagaBase de cada entrada en InInitializationOrderModuleList. Ahora estamos listos para enganchar. Como estamos enganchando procesos en ejecucion cabe la posibilidad de que el codido que queremos reescribir estuviera en ejecucion en ese momento. Esto causariaa un error, asique primero detendremos todos los hilos del proceso victima. La lista de sus hilos se obtiene via NtQuerySystemInformation con SystemProcessesAndThreadsInformation como SystemInformationClass. El resultado de esta funcion esta descrito en el capitulo 4. Pero tenemos que añadir la descripcion de la estructura SYSTEM_THREADS donde esta la informacion sobre el hilo. typedef struct _SYSTEM_THREADS { LARGE_INTEGER KernelTime; LARGE_INTEGER UserTime; LARGE_INTEGER CreateTime; ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientId; KPRIORITY Priority; KPRIORITY BasePriority; ULONG ContextSwitchCount; THREAD_STATE State; KWAIT_REASON WaitReason; } SYSTEM_THREADS, *PSYSTEM_THREADS; Para cada hilo tenemos que conseguir su handle usando NtOpenThread. Usaremos ClientId para ello. NTSTATUS NtOpenThread( OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId ); El handle que queremos sera almacenado en ThreadHandle. Ponemos DesiredAccess a THREAD_SUSPEND_RESUME. #define THREAD_SUSPEND_RESUME 2 ThreadHandle will be used for calling NtSuspendThread. ThreadHandle sera usado al llamar a NtSuspendThread. NTSTATUS NtSuspendThread( IN HANDLE ThreadHandle, OUT PULONG PreviousSuspendCount OPTIONAL ); El proceso suspendido esta listo para reescribir. Procederemos como esta explicado en el capitulo 3.2.2 en Hooking Windows API". La unica diferencia es que usaremos funciones para otros procesos. Despues de un gancho reviviremos todos los hilos del proceso mediante NtResumeThread. NTSTATUS NtResumeThread( IN HANDLE ThreadHandle, OUT PULONG PreviousSuspendCount OPTIONAL ); =====[ 7.3 Nuevos procesos ]==================================================== La infeccion de todos los procesos en ejecucion no afecta a los procesos que se ejecuten despues. Podriamos obtener la lista de procesos y al cabo de un rato volver a obtenerla e infectar aquellos que no estuvieran en la primera lista pues serian los nuevos. Pero este metodo no parece muy fiable. Es mucho mejor enganchar la funcion que se llama para crear un nuevo proceso. Con este metodo no perdemos ningun nuevo proceso. Podemos enganchar NtCreateThread, pero no es la mejor forma. Engancharemos NtResumeThread que tambien es llamado cada vez que un proceso se crea. Se le llama despues de NtCreateThread. El unico problema con NtResumeThread es que no solo se llama cuando comienza un proceso nuevo. Pero esto se puede superar con facilidad. NtQueryInformationThread nos dara informacion sobre que procesos posee el hilo especifico. La ultima cosa que nos queda por hacer es comprobar si el proceso ya esta enganchado o no. Esto lo hacemos leyendo el primer byte de cualquier funcion que enganchemos. NTSTATUS NtQueryInformationThread( IN HANDLE ThreadHandle, IN THREADINFOCLASS ThreadInformationClass, OUT PVOID ThreadInformation, IN ULONG ThreadInformationLength, OUT PULONG ReturnLength OPTIONAL ); ThreadInformationClass es el tipo de informacion y en nuestro caso deberia ser puesta a ThreadBasicInformation. ThreadInformation es el buffer para el resultado cuyo tamaño es ThreadInformationLength bytes. #define ThreadBasicInformation 0 Para el tipo ThreadBasicInformation se devuelve esta estructura: typedef struct _THREAD_BASIC_INFORMATION { NTSTATUS ExitStatus; PNT_TIB TebBaseAddress; CLIENT_ID ClientId; KAFFINITY AffinityMask; KPRIORITY Priority; KPRIORITY BasePriority; } THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION; En ClientId esta el PID del proceso que posee el hilo. Ahora tenemos que infectar el nuevo proceso. El problema es que el nuevo proceso solo tiene en memoria ntdll.dll. Todos los otros modulos son cargados inmediatamente despues de llamar NtResumeThread. Hay varias formas de solventar este problema. Por ejemplo, podemos enganchar la API LdrInitializeThunk que es llamada durante el inicio del proceso. NTSTATUS LdrInitializeThunk( DWORD Unknown1, DWORD Unknown2, DWORD Unknown3 ); Al principio ejecutaremos el codigo original y entonces engancharemos las funciones que queramos en este nuevo proceso. Pero es mejor que desenganchemos LdrInitializeThunk porque es llamado muchas veces despues y no queremos reenganchar todas las funciones otra vez. Aqui todo se hace antes de la ejecucion de la primera instruccion de la aplicacion enganchada. Por eso es por lo que no tiene posibilidad de llamar ninguna funcion enganchada antes de que nosotros la enganchemos. Los ganchos en si son igual que cuando enganchamos procesos en ejecucion solo que aqui no nos preocupamos de hilos en ejecucion. =====[ 7.4 DLL ]================================================================ En cada proceso del sistema hay una copia de ntdll.dll. Esto significa que podemos enganchar cualquier funcion de este modulo en el proceso desde un primer momento. ¿Pero que pasa con funciones de otros modulos como la kernel32.dll o advapi32.dll? Y hay varios procesos que solo tienen la ntdll.dll. Todos los demas modulos podrian ser cargados dinamicamente en mitad del codigo despues del gancho. Lo que tenemos que hacer entonces es enganchar LdrLoadDll que carga nuevos modulos. NTSTATUS LdrLoadDll( PWSTR szcwPath, PDWORD pdwLdrErr, PUNICODE_STRING pUniModuleName, PHINSTANCE pResultInstance ); Lo mas importante para nosotros aqui es pUniModuleName que es el nombre del modulo. pResultInstance contendra su direccion si la llamada tiene exito. Llamaremos al LdrLoadDll original y entonces engancharemos todas las funciones del modulo cargado. =====[ 8. Memoria ]============================================================= Cuando enganchamos una funciones modificamos sus 5 primeros bytes. Llamando a NtReadVirtualMemory cualquiera puede detectar si una funcion ha sufrido un gancho. Asique tenemos que enganchar NtReadVirtualMemory para prevenir la deteccion. NTSTATUS NtReadVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, OUT PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); Hemos cambiado bytes en el comienzo de todas las funciones que enganchamos y tenemos tambien memoria reservada para nuestro nuevo codigo. Deberiamos comprobar si la llamada lee alguno de estos bytes. Si tenemos nuestros bytes en el rango de BaseAddress a BaseAddress + BufferLength tenemos que camviar algunos bytes en Buffer. Si alguien pide bytes de nuestra zona reservada deberiamos retornar Buffer vacio y un error STATUS_PARTIAL_COPY. Este valor dice que no se copiaron todos los bytes pedidos en el buffer. Tambien se usa cuando pedimos memoria no reservada. ReturnLength deberia ser puesto a 0 en este caso. #define STATUS_PARTIAL_COPY 0x8000000D Si alguien pide los primeros bytes de una funcion enganchada tenemos que llamar al codigo original y copiar lo bytes originales al Buffer (los hemos salvado papra llamadas originales). Ahora el proceso no es capaz de detectar un gancho leyendo su memoria. Tambien si trazas el proceso enganchado con un debugger este tendria problemas. Mostrara los bytes originales pero ejecutara nuestro codigo :D Para conseguir un aocultacion perfecta tambien podemos enganchar NtQueryVirtualMemory. Esta funcion es usada para obtener informacion sobre memoria virtual. Podemos engancharla para prevenir detectar nuestra memoria reservada. NTSTATUS NtQueryVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN MEMORY_INFORMATION_CLASS MemoryInformationClass, OUT PVOID MemoryInformation, IN ULONG MemoryInformationLength, OUT PULONG ReturnLength OPTIONAL ); MemoryInformationClass especifica la clase de datos devueltos. Los primeros dos tipos nos interesan. #define MemoryBasicInformation 0 #define MemoryWorkingSetList 1 Para la clase MemoryBasicInformation se devuelve esta estructura: typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; ULONG AllocationProtect; ULONG RegionSize; ULONG State; ULONG Protect; ULONG Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION; Cada seccion de memoria tiene su tamaño RegionSize y su tipo Type. La memoria libre es del tipo MEM_FREE. #define MEM_FREE 0x10000 Si una seccion antes de la nuestra tiene tipo MEM_FREE deberiamos añadir el tamaño de nuestra seccion a su RegionSize. Si la siguiente seccion es tambien MEM_FREE deberiamos añadir al tamaño de la siguiente seccion otra vez la RegionSize. Si una seccion antes de la nuestra tiene otro tipo devolvemos MEM_FREE para nuestra seccion. Su tamaño es contado otra vez de acuerdo a la seccion consiguiente. Para la clase MemoryWorkingSetList se devuelve la estructura: typedef struct _MEMORY_WORKING_SET_LIST { ULONG NumberOfPages; ULONG WorkingSetList[1]; } MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST; NumberOfPages es el numero de elementos de WorkingSetList. Este numero deberia ser decrementado. Deberiamos encontrar nuestra seccion en WorkingSetList y mover los registros siguientes sobre los nuestros. WorkingSetList es un array de DWORDs donde los 20 bits de mayor peso especifican los 20 bits mas altos de la direccion de la seccion y los 12 de menor peso son flags. =====[ 9. Handle ]============================================================== Llamar NtQuerySystemInformation con SystemHandleInformation como clase nos devuelve un vector de todos los handles abiertos en la estructura _SYSTEM_HANDLE_INFORMATION_EX. #define SystemHandleInformation 0x10 typedef struct _SYSTEM_HANDLE_INFORMATION { ULONG ProcessId; UCHAR ObjectTypeNumber; UCHAR Flags; USHORT Handle; PVOID Object; ACCESS_MASK GrantedAccess; } SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION; typedef struct _SYSTEM_HANDLE_INFORMATION_EX { ULONG NumberOfHandles; SYSTEM_HANDLE_INFORMATION Information[1]; } SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX; ProcessId especifica el proceso que posee al handle. ObjectTypeNumber es el tipo del handle. NumberOfHandles es el numero de registros en el vector Information. Esconder un elemento es trivial. Tenemos que remover todos los registros siguientes uno por uno y decrementar NumberOfHandles. Borrar todos los siguientes es necesario porque los handles en el array estan agrupados por ProcessId. Esto significa que todos los handles de un mismo proceso estan juntos. Y para un proceso el numero de Handle es creciente. Ahora recuerda la estructura _SYSTEM_PROCESSES que es devuelta por esta funcion con clase SystemProcessesAndThreadsInformation. Aqui podemos ver que cada proceso tiene informacion sobre su numero de handles en HandleCount. Si queremos perfeccion deberiamos modificar HandleCount con cuantos handles ocultemos cuando se llame a esta funcion con clase SystemProcessesAndThreadsInformation. Pero esta correcion consumiria mucho tiempo. Normalmente hay muchos handles abriendose y cerrandose en espacios de tiempo muy cortos durante la ejecucion del sistema. Asique puede ocurrir facilmente que el numero de handles cambie entre dos llamadas de esta funcion y no necesitemos cambiar HandleCount. =====[ 9.1 Nombrando handles y obteniendo su tipo ]============================= Ocultar un handle es trivial pero encontrar que handle hay que esconder es mucho mas complicado. Si tenemos por ejemplo un proceso oculto deberiamos esconder todos sus handles y todos los handles con los que esta conectado. Esconder handles de este proceso es tambien trivial. Solo estamos comparando el processId del handle y el PID de nuestros procesos y cuando son iguales los ocultamos. pero los handles de otros procesos tienen que ser nombrados antes de que podamos comparar algo. El numero de handles en el sistema es usualmente muy grande, asi que lo mejor que podemos hacer es comparar el tiipo de handle primero anttes de intentar nombrarlo. Nombrar tipos llevar mucho tiempo para handles que no nos interesarian. Nombrar un handle y tipo de handle se hace via NtQueryObject. NTSTATUS ZwQueryObject( IN HANDLE ObjectHandle, IN OBJECT_INFORMATION_CLASS ObjectInformationClass, OUT PVOID ObjectInformation, IN ULONG ObjectInformationLength, OUT PULONG ReturnLength OPTIONAL ); ObjectHandle el handle del que queremos obtener informacion. ObjectInformationClass es el tipo de informacion que sera almacenada en el buffer ObjectInformation que ocupa ObjectInformation bytes. Usamos la clase ObjectNameInformation y ObjectAllTypesInformation. We will use class ObjectNameInformation and ObjectAllTypesInformation. ObjectNameInfromation llenara el buffer con la estructura OBJECT_NAME_INFORMATION y ObjectAllTypesInformation con la estructura OBJECT_ALL_TYPES_INFORMATION. #define ObjectNameInformation 1 #define ObjectAllTypesInformation 3 typedef struct _OBJECT_NAME_INFORMATION { UNICODE_STRING Name; } OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION; Name determina el nombre del handle. typedef struct _OBJECT_TYPE_INFORMATION { UNICODE_STRING Name; ULONG ObjectCount; ULONG HandleCount; ULONG Reserved1[4]; ULONG PeakObjectCount; ULONG PeakHandleCount; ULONG Reserved2[4]; ULONG InvalidAttributes; GENERIC_MAPPING GenericMapping; ULONG ValidAccess; UCHAR Unknown; BOOLEAN MaintainHandleDatabase; POOL_TYPE PoolType; ULONG PagedPoolUsage; ULONG NonPagedPoolUsage; } OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION; typedef struct _OBJECT_ALL_TYPES_INFORMATION { ULONG NumberOfTypes; OBJECT_TYPE_INFORMATION TypeInformation; } OBJECT_ALL_TYPES_INFORMATION, *POBJECT_ALL_TYPES_INFORMATION; Name determina el nombre del tipo del objeto que inmediatamente sigue cada estructura OBJECT_TYPE_INFORMATION. La siguiente estructura OBJECT_TYPE_INFORMATION sigue a este Name, comenzando en el limite de los cuatro primeros bytes. ObjectTypeNumber de la estructura SYSTEM_HANDLE_INFORMATION es un indice del array TypeInformation. Mas complicado es obtener el nombre del handle de otro proceso. Hay dos posibilidades de como nombrarlas. La primera es copiar el handle via NtDuplicateObject a nuestro proceso y entonces nombrarlas. Este metodo fallara para unos tipos especificos de handles. Pero fallara solo para unos pocos asique podemos usar tranquilos este metodo. NtDuplicateObject( IN HANDLE SourceProcessHandle, IN HANDLE SourceHandle, IN HANDLE TargetProcessHandle, OUT PHANDLE TargetHandle OPTIONAL, IN ACCESS_MASK DesiredAccess, IN ULONG Attributes, IN ULONG Options ); SourceProcessHandle es un handle del proceso que posee a SourceHandle que es el handle que queremos copiar. TargetProcessHandle es el handle del proceso a copiar. Este sera el handle de nuestro proceso en nuestro caso. TargetHandle es el puntero al handle donde queremos guardar una copia del handle original. DesiredAccess deberia ser puesto a PROCESS_QUERY_INFORMATION, Attributes y Options a 0. El segundo metodo que funciona con cualquier handle es usar un driver de sistema. El codigo fuente para esto esta disponible en el proyecto OpHandle en http://rootkit.host.sk. =====[ 10. Puertos ]============================================================ La manera mas facil de enumerar los puertos abiertos es usar AllocateAndGetTcpTableFromStack y AllocateAndGetUdpTableFromStack, y/o AllocateAndGetTcpExTableFromStack y AllocateAndGetUdpExTableFromStack de iphlpapi.dll. Las funciones Ex estan disponibles desde Windows XP. typedef struct _MIB_TCPROW { DWORD dwState; DWORD dwLocalAddr; DWORD dwLocalPort; DWORD dwRemoteAddr; DWORD dwRemotePort; } MIB_TCPROW, *PMIB_TCPROW; typedef struct _MIB_TCPTABLE { DWORD dwNumEntries; MIB_TCPROW table[ANY_SIZE]; } MIB_TCPTABLE, *PMIB_TCPTABLE; typedef struct _MIB_UDPROW { DWORD dwLocalAddr; DWORD dwLocalPort; } MIB_UDPROW, *PMIB_UDPROW; typedef struct _MIB_UDPTABLE { DWORD dwNumEntries; MIB_UDPROW table[ANY_SIZE]; } MIB_UDPTABLE, *PMIB_UDPTABLE; typedef struct _MIB_TCPROW_EX { DWORD dwState; DWORD dwLocalAddr; DWORD dwLocalPort; DWORD dwRemoteAddr; DWORD dwRemotePort; DWORD dwProcessId; } MIB_TCPROW_EX, *PMIB_TCPROW_EX; typedef struct _MIB_TCPTABLE_EX { DWORD dwNumEntries; MIB_TCPROW_EX table[ANY_SIZE]; } MIB_TCPTABLE_EX, *PMIB_TCPTABLE_EX; typedef struct _MIB_UDPROW_EX { DWORD dwLocalAddr; DWORD dwLocalPort; DWORD dwProcessId; } MIB_UDPROW_EX, *PMIB_UDPROW_EX; typedef struct _MIB_UDPTABLE_EX { DWORD dwNumEntries; MIB_UDPROW_EX table[ANY_SIZE]; } MIB_UDPTABLE_EX, *PMIB_UDPTABLE_EX; DWORD WINAPI AllocateAndGetTcpTableFromStack( OUT PMIB_TCPTABLE *pTcpTable, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); DWORD WINAPI AllocateAndGetUdpTableFromStack( OUT PMIB_UDPTABLE *pUdpTable, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); DWORD WINAPI AllocateAndGetTcpExTableFromStack( OUT PMIB_TCPTABLE_EX *pTcpTableEx, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); DWORD WINAPI AllocateAndGetUdpExTableFromStack( OUT PMIB_UDPTABLE_EX *pUdpTableEx, IN BOOL bOrder, IN HANDLE hAllocHeap, IN DWORD dwAllocFlags, IN DWORD dwProtocolVersion; ); Hay otra forma de hacer esto. Cuando un programa crea un socket y se pone a escuchar seguramente tiene un handle abierto para el y para el puerto abierto. Podemos enumerar todos los handles abiertos en el sistema y enviarles un buffer especial mediante NtDeviceIoControlFile para descubrir si el handle es para un puerto abierto o no. Esta dara tambien informacion sobre el puerto. Como hay muchos handles abiertos solo probaremos handles cuyo tipo sea File y nombre sea \Device\Tcp o \Device\Udp. Los puertos abiertos solo tienen handles de este tipo y nombre. Echando un vistazo al codigo de las funciones iphlpapi.dll que estan ahi arriba descubrimos que esas funciones tambien llaman a NtDeviceIoControlFiley envian buffers especiales para obtener una lista de todos los puertos abiertos en el sistema. Eso significa que la unica funcion que necesitamos para esconder puertos es NtDeviceIoControlFile. NTSTATUS NtDeviceIoControlFile( IN HANDLE FileHandle IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG IoControlCode, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer OPTIONAL, IN ULONG OutputBufferLength ); Los argumentos interesantes para nosotros son FileHandle que especifica un handle o dispositivo con el que comunicar, IoStatusBlock que apunta a una variable que recibe el estado de completacion final e informacion sobre la operacion solicitada, IoControlCode que es un numero especificando el tipo del dispositivo, metodo, acceso de fichero y una funcion. InputBuffer contiene datos de entrada que son InputBufferLength bytes y similarmente OutputBuffer y OutputbufferLength. =====[ 10.1 Netstat, OpPorts en WinXP, FPort en WinXP ]========================= Obtener una lista de todos los puertos abiertos es laa primera forma usada por ejemplo por OpPorts y FPort en Windows Xp y tambien Netstat. Estos programas llaman a NtDeviceIoControlFile dos veces con IoControlCode 0x000120003. OutputBuffer es escrito despues de una segunda llamada. El nombre de FileHandle es siempre \Device\Tcp. InputBuffer difiere para distintos tipos de llamada: 1) Para obtener vector de MIB_TCPROW InputBuffer seria: primera llamada: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 segunda llamada: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 2) Para obtener un vector de MIB_UDPROW: primera llamada: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 segunda llamada: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 3) Para obtener un vector de MIB_TCPROW_EX: primera llamada: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 segunda llamada: 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 4) Para obtener un vector de MIB_UDPROW_EX: primera llamada: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 segunda llamada: 0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00 0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 Puedes ver que los buffer son distintos en unos cuantos bytes solo. Podemos claramente agrupar estos: Las llamadas en que estamos interesados tienen InputBuffer[1] puesto a 0x04 y principalmente InputBuffer[17] on 0x01. Solo despues de estos datos de entrada se llenara el OutputBuffer con las tablas deseadas. Si queremos obtener info sobre puertos TCP ponemos InputBuffer[0] a 0x00, y para UDP a 0x01. Si queremos tablas extendidas de salida (MIB_TCPROW_EX o MIB_UDPROW_EX) usamos Inputbuffer[16] en la segunda llamada puesta a 0x02. Si descubrimos la llamada con estos parametros podemos cambiar el buffer de salida. Para obtener numeros de filas en el buffer de salida simplemente dividimos Information de IoStatusBlock por el tamaño de la fila. Esconder una fila es facil ahora. Solo reescribelo con las filas siguientes y borra la ultima. No olvides cambiar OutputBufferLength y IoStatusBlock. =====[ 10.2 OpPorts en Win2k y NT4, FPort en Win2k ]============================ Usamos NtDeviceIoControlFile con IoControlCode 0x00210012 para determinar si el handle de tipo File y nombre \Device\Tcp o \Device\Udp es el handle del puerto abierto. Asique lo primero comparamos IoControlCode y entonces el tipo y nombre del hanlde. Si es todavia interesante entonces comparamos la longitud del buffer de entrada que deberia ser igual a la longitud de la estructura TDI_CONNECTION_IN. Esta longitud es 0x18. OutputBuffer es TDI_CONNECTION_OUT. typedef struct _TDI_CONNECTION_IN { ULONG UserDataLength, PVOID UserData, ULONG OptionsLength, PVOID Options, ULONG RemoteAddressLength, PVOID RemoteAddress } TDI_CONNECTION_IN, *PTDI_CONNECTION_IN; typedef struct _TDI_CONNECTION_OUT { ULONG State, ULONG Event, ULONG TransmittedTsdus, ULONG ReceivedTsdus, ULONG TransmissionErrors, ULONG ReceiveErrors, LARGE_INTEGER Throughput LARGE_INTEGER Delay, ULONG SendBufferSize, ULONG ReceiveBufferSize, ULONG Unreliable, ULONG Unknown1[5], USHORT Unknown2 } TDI_CONNECTION_OUT, *PTDI_CONNECTION_OUT; La implementacion concreta de como determinar si el handle es puerto abierto esta disponible en el codigo fuente de OpPorts en http://hxdef.czweb.org (http://rootkit.host.sk). Estamos interesados en esconder un puerto especifico ahora. Ya comparamos InputBufferLength y IoControlCode. Ahora tenemos que comparar RemoteAddressLength. Esta es siempre 3 o 4 para un puerto abierto. La ultima cosa que tenemos que hcer es comparar ReceivedTsdus de OutputBuffer que contiene el puerto en formato network y nuestra lista de puertos que queremos esconder. La diferencia entre TCP y UDP esta en ell nombre del handle. Borrando OutputBuffer, cambiando IoStatusBlock y devolviendo el valor de STATUS_INVALID_ADDRESS esconderemos este puerto. =====[ 11. Finalizando ]======================================================== La implementacion las tecnicas aqui descritas estaran disponibles con las fuentes del Hacker Defender 1.0.0 en la pagina http://hxdef.czweb.org (http://rootkit.host.sk) o en http://www.rootkit.com. Es posible que añada mas información sobre invisibilidad en Windows NT en el futuro. Nuevas versiones de este documento podrian contener mejoras de las tecnicas comentadas. Agradecimientos especiales a Ratter que me enseño todo lo necesario para escribir este documento y escribir el Hacker Defender. Enviadme comentarios a holy_father@phreaker.net o al foro de http://hxdef.czweb.org (http://rootkit.host.sk.) (Saludos del traductor: holy_father-> he aprendido muchas cosas te lo agradezco :D salu2 a Ale(spiderxd en hotmail.com) muxos besos Elenikkita :*** te kero & TF !!! :) ===================================[ Fin ]======================================