========================[ Invisibilità sui sistemi NT ]======================== Come diventare invisibili su Windows NT --------------------------------------- Autor: Holy_Father Traduzione: Gabriele Romanato Version: 1.2 italian Data: 15.01.2006 Web: http://www.hxdef.org, http://hxdef.net.ru, http://hxdef.czweb.org, http://rootkit.host.sk Mirror: http://hxdef.xtremescripter.de Link: http://www.rootkit.com =====[ 1. Contenuti ]========================================================== 1. Contenuti 2. Introduzione 3. File 3.1 NtQueryDirectoryFile 3.2 NtVdmControl 4. Processi 5. Registro 5.1 NtEnumerateKey 5.2 NtEnumerateValueKey 6. Servizi di sistema e driver 7. Agganciare e diffondersi 7.1 Diritti 7.2 Aggancio globale 7.3 Nuovi processi 7.4 DLL 8. Memoria 9. Handle 9.1 Nominare un handle ed ottenere il tipo 10. Porte 10.1 Netstat, OpPorts in WinXP, FPort in WinXP 10.2 OpPorts in Win2k e NT4, FPort in Win2k 11. Conclusione =====[ 2. Introduzione ]======================================================== Questo documento tratta delle tecniche per nascondere oggetti, file, servizi, processi, etc. sui sistemi Windows NT. Questi metodi sono basati sugli agganci alle funzioni della Windows API che sono descritti nel mio documento "Hooking Windows API". Tutto questo lo appresi dalle mie ricerche durante la scrittura del codice dei rootkit, sicchè è possibile que possa essere scritto meglio e più facilmente di quello che ho fatto. Questo include la mia implementazione dei rootkit. L'occultazione di un oggetto arbitrario, in questo documento, significa modificare alcune funzioni che mostrano questo oggetto, in modo da evitare che mostrino l'oggetto. Nel caso che questo oggetto fosse il valore di ritorno di questa funzione, restituiremo il valore come se l'oggetto non esistesse. Il metodo di base (tranne i casi in cui diremo diversamente) è quello di chiamare la funzione originale con gli argomenti originali e poi di cambiare il suo output. Nella versione di questo testo si descrivono i metodi per nascondere file, processi, chiavi e valori del registro, servizi di sistema e driver, memoria allocata ed handles. =====[ 3. File ]================================================================ Vi sono diverse possibilità di nascondere file in modo che non lo veda il SO. Ci dedicheremo solo a modificare la API, tralasciando quelle tecniche che effettuano variazioni nel filesystem. Così è anche molto più facile e non abbiamo bisogno di conoscere un particolare tipo di filesystem. =====[ 3.1 NtQueryDirectoryFile ]=============================================== La ricerca di file su Win NT in alcune directory si basa sul cercare tutti i suoi file e tutte le sue sottodirectory con file. Per l'enumerazione dei file si usa la funzione 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 ); I parametri importanti per noi sono FileHandle, FileInformation y FileInformationClass. FileHandle è l'handle della directory, che si può ottenere con NtOpenFile. FileInformation è un puntatore ad una zona di memoria dove verrà scritta l'informazione richiesta. FileInformationClass determina il tipo di registro che verrà scritto in FileInformation. FileInformationClass è un tipo enumerativo, e sono quattro i valori di cui abbiamo bisogno per l'enumerazione di una directory. #define FileDirectoryInformation 1 #define FileFullDirectoryInformation 2 #define FileBothDirectoryInformation 3 #define FileNamesInformation 12 La struttura scritta in FileInformation per FileDirectoryInformation: 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; Per 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; Per 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; e per FileNamesInformation: typedef struct _FILE_NAMES_INFORMATION { ULONG NextEntryOffset; ULONG Unknown; ULONG FileNameLength; WCHAR FileName[1]; } FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION; Questa funzione scrive una lista di queste strutture in FileInformation. Solo tre variabili ci riguardano in ognuno di questi tre tipi di strutture. NextEntryOffset è la lunghezza in byte di un particolare elemento della lista. Il primo elemento è all'indirizzo FileInformation + 0. Il secondo in FileInformation + NextEntryOffset (NextEntryOffset del primo). L'ultimo elemento ha NextEntryOffset settato a zero. FileName è il nome completo del file. FileNameLength è la lunghezza del nome del file. Se vogliamo nascondere un file, dobbiamo tenere presenti questi quattro tipi e per ogni registro restituito compariamo il suo nome con quello che vogliamo nascondere. Se vogliamo nascondere il primo registro dobbiamo spostare le strutture seguenti secondo la dimensione della prima. Ciò causerà la sovrascrittura del primo registro. Si vogliamo nascondere un altro, cambiamo il valore di NextEntryOffset del registro precedente. Se vogliamo nascondere l'ultimo registro, il nuovo valore di NextEntryOffset sarà zero, altrimenti il valore sarà la somma del NextEntryOffset del registro precedente e del NextEntryOffset del registro successivo. In tal caso dobbiamo cambiare il valore di Unknown del registro precedente, che è un indice per la ricerca seguente. Il valor di Unknown del registro precedente deve contenere il valore di Unknown del registro da nascondere. Se il registro che dovrebbe essere visto non venisse trovato, restituiremmo l'errore STATUS_NO_SUCH_FILE. #define STATUS_NO_SUCH_FILE 0xC000000F =====[ 3.2 NtVdmControl ]======================================================= Per ragioni sconosciute, l'emulazione del DOS NTVDM può ottenere una lista di file anche con NtVdmContol. NTSTATUS NtVdmControl( IN ULONG ControlCode, IN PVOID ControlData ); ControlCode specifica la sottofunzione che si applica a i dati del buffer ControlData. Se ControlCode è uguale a VdmDirectoryFile, tale funzione fa la stessa cosa di NtQueryDirectoryFile con FileInformationClass settato su FileBothDirectoryInformation. #define VdmDirectoryFile 6 Quindi ControlData viene usato come FileInformation. L'unica differenza qui è che non sappiamo la lunghezza di questo buffer, così dobbiamo calcolarla manualmente. Dobbiamo sommare NextEntryOffset di tutti i registri e FileNameLength dell'ultimo registro e 0x5E come lunghezza dell'ultimo registro escludendo il nome del file. I metodi per nascondere sono gli stessi che nel caso di NtQueryDirectoryFile. =====[ 4. Processi ]============================================================ Sono disponibili varie informazioni usando NtQuerySystemInformation. NTSTATUS NtQuerySystemInformation( IN SYSTEM_INFORMATION_CLASS SystemInformationClass, IN OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength OPTIONAL ); SystemInformationClass specifica il tipo di informazione che vogliamo ottenere. SystemInformation è un puntatore al buffer di output. SystemInformationLength è la lunghezza di questo buffer. ReturnLength è il numero di byte scritti. Per l'enumerazione dei processi in esecuzione usiamo SystemInformationClass settata su SystemProcessesAndThreadsInformation. #define SystemInformationClass 5 La struttura restituita nel 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; // solo Windows 2000 SYSTEM_THREADS Threads[1]; } SYSTEM_PROCESSES, *PSYSTEM_PROCESSES; Nascondere processi è lo stesso che nascondere file. Dobbiamo cambiare NextEntryDelta del registro precedente a quello che vogliamo nascondere. Solitamente non cambieremo in questo caso il primo registro, perchè è il processo Idle. =====[ 5. Registro ]============================================================ Il registro di Windows è semplicemente una grande struttura ad albero contenente due tipi importanti di registri che potremmo voler nascondere. Il primo sono le chiavi del registro, ed il secondo sono i valori. Nascondere chiavi e valori non è così banale come nascondere file o processi. =====[ 5.1 NtEnumerateKey ]===================================================== A causa della sua struttura, non è possibile ottenere una lista di tutte le chiavi in una specifica parte del registro. Possiamo solo ottenere informazioni su una chiave specificata dal suo indice in una parte del registro. La funzione che fornisce questa informazione è NtEnumerateKey. NTSTATUS NtEnumerateKey( IN HANDLE KeyHandle, IN ULONG Index, IN KEY_INFORMATION_CLASS KeyInformationClass, OUT PVOID KeyInformation, IN ULONG KeyInformationLength, OUT PULONG ResultLength ); KeyHandle è un handle ad una chiave, e vogliamo ottenere informazioni per una sottochiave specificata da Index. Il tipo di informazione restituita è specificato mediante KeyInformationClass. I dati vengono scritti sul buffer di output KeyInformation, di cui KeyInformationLength è la lunghezza. ResultLength è il numero di byte scritti. La cosa più importante da capire è che se nascondiamo una chiave, gli indici delle chiavi seguenti saranno spostati. E poichè per ottenere informazioni su una chiave con un indice maggiore dobbiamo ottenere informazioni su una chiave con indice minore, perciò dobbiamo calcolare quanti registri sono stati nascosti prima, e poi restituire quello corretto. Facciamo un esempio. Poniamo di avere alcune chiavi: A, B, C, D, E, ed F in ogni parte del registro. L'indice parte da zero, perciò quella con indice 4 corrisponde alla chiave E. Ora se vogliamo nascondere B e l'applicazione agganciata chiama NtEnumerateKey con Index 4, noi dobbiamo restituire l'informazione su F, perchè l'indice si è spostato. Il problema è che non sappiamo quale spostamento è avvenuto. E se non ci preoccupassimo dello spostamento e restituissimo E invece di F quando richiediamo la chiave con Index 4, non restituiremmo nulla in caso chiedessimo la chiave con Index 1,oppure restituiremmo C. Entrambi i casi sono errori. Dobbiamo quindi tener conto dello spostamento. Ora se calcolassimo lo spostamento richiamando la funzione per ogni indice da 0 a Index, potremmo a volte aspettare anni ( su un processore a 1GHz ritarderemmo 10 secondi con il registro standard, che è la stessa cosa). Così dobbiamo cercare di risolverlo con un metodo più sofisticato. Sappiamo che le chiavi (ad eccezione delle reference) sono ordinate alfabeticamente. Se tralasciassimo le reference (che non vogliamo nascondere) possiamo calcolare lo spostamento con il seguente metodo. Ordineremo alfabeticamente la nostra lista di nomi di chiavi che vogliamo nascondere (con RtlCompareUnicodeString per esempio), e poi, quando l'applicazione chiama NtEnumerateKey, non la richiameremo con gli argomenti inalterati, finchè non scopriamo il nome del registro specificato per Index. NTSTATUS RtlCompareUnicodeString( IN PUNICODE_STRING String1, IN PUNICODE_STRING String2, IN BOOLEAN CaseInSensitive ); String1 e String2 sono le stringhe da comparare, CaseInSensitive è True se vogliamo comparare con distinzione fra maiuscole e minuscole. Il risultato della funzione descrive la relazione fra String1 e String2: risultato > 0: String1 > String2 risultato = 0: String1 = String2 risultato < 0: String1 < String2 Ora dobbiamo trovare un punto limite nella lista. Compareremo alfabeticamente il nome della chiave specificata per Index con i nomi della nostra lista. Il punto limite sarà l'ultimo nome minore della nostra lista (il successivo sarà il medesimo nonme). Sappiamo che lo spostamento è per lo più il numero di nomi prima del punto limite della nostra lista. Però non tutte le voci della nostra lista devono essere chiavi esistenti nella parte del registro in cui ci troviamo. Così cerchiamo tutte le voci della nostra lista fino al punto limite si queste si trovano in questa parte del registro. Questo si può fare con NtOpenKey. NTSTATUS NtOpenKey( OUT PHANDLE KeyHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes ); KeyHandle è un handle di una chiave sovraordinata. Qui useremo il valore di NtEnumerateKey . DesiredAccess sono i diritti di accesso. Il valore corretto è KEY_ENUMERATE_SUB_KEYS. ObjectAttributes descrive la sottochiave che vogliamo aprire (incluso il suo nome). #define KEY_ENUMERATE_SUB_KEYS 8 Se il risultato di NtOpenKey è 0 l'apertura della chiave ha avuto successo, il che significa che questa chiave della nostra lista esiste. La chiave aperta sarà chiusa con NtClose. NTSTATUS NtClose( IN HANDLE Handle ); Per ogni chiamata a NtEnumerateKey conteremo lo spostamento come il numero di chiavi della nostra lista che esistono nella parte data del registro. Poi aggiungeremo questo spostamento all'argomento di Index e infine chiameremo l' NtEnumerateKey originale. Per ottenere il nome della chiave specificata per Index useremo KeyBasicInformation come KeyInformationClass. #define KeyBasicInformation 0 NtEnumerateKey restituisce questa struttura in KeyInformation: typedef struct _KEY_BASIC_INFORMATION { LARGE_INTEGER LastWriteTime; ULONG TitleIndex; ULONG NameLength; WCHAR Name[1]; } KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION; La sola cosa che ci serve qui è Name e NameLength. Se non vi è entrata per l'Index spostato restituiremo l'errore STATUS_EA_LIST_INCONSISTENT. #define STATUS_EA_LIST_INCONSISTENT 0x80000014 =====[ 5.2 NtEnumerateValueKey ]================================================ I valori del registro non sono ordinati alfabeticamente. Fortunatamente il numero di valori in una chiave è abbastanza esiguo e possiamo usare il metodo di richiamata per gli spostamenti. La API per ottenere informazioni su un valore è chiamata NtEnumerateValueKey. NTSTATUS NtEnumerateValueKey( IN HANDLE KeyHandle, IN ULONG Index, IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation, IN ULONG KeyValueInformationLength, OUT PULONG ResultLength ); KeyHandle è di nuovo un handle di una chiave sovraordinata. Index è un indice della lista di valori nella chiave data. KeyValueInformationClass descrive un tipo di informazione che sarà registrato nel buffer KeyValueInformation, che occupa KeyValueInformationLength bytes. Il numero di byte scritti è restituito in ResultLength. Di nuovo possiamo calcolare lo spostamento, ma secondo il numero di valori in una chiave possiamo richiamare questa funzione per tutti gli indici da 0 a Index. Il nome del valore può essere ottenuto quando KeyValueInformationClass è settato a KeyValueBasicInformation. #define KeyValueBasicInformation 0 Poi otterremo la seguente struttura nel 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; Di nuovo quello che ci interessa è solo Name e Namelength. Se non vi è entrata per l'Index spostato restituiremo STATUS_NO_MORE_ENTRIES. #define STATUS_NO_MORE_ENTRIES 0x8000001A =====[ 6. Servizi di sistema e driver ]====================================== I servizi di sistema ed i driver sono enumerati da quattro funzioni della API, independenti l'una dall'altra. La connessione fra loro è differente in ogni versione di Windows. Per questo motivo dobbiamo agganciare tutte e quattro le funzioni. 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 ); Qui l'importante è lpServices, che punta al buffer dove si andrà a registrare la lista dei servizi. E' anche importante lpServicesReturned che indica il numero di registri che si è creato nel buffer. La struttura dei dati che verrà restituita al buffer di output dipende dalle funzioni. Le funzioni EnumServicesStatusA e EnumServicesGroupW restituiscono una struttura 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; e EnumServicesStatusExA e EnumServicesStatusExW sarà: 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; Nulla più di lpServiceName ci interessa, che è il nome del servizio di sistema. I registri hanno grandezza statica, così se vogliamo nasconderne uno, dobbiamo solo spostare i registri seguenti fino a quello che vogliamo nascondere, sovrascrivendolo. Dobbiamo distinguere fra la grandezza di SERVICE_STATUS e quella di SERVICE_STATUS_PROCESS. =====[ 7. Agganciare e diffondersi ]============================================ Per ottenere l'effetto desiderato dobbiamo agganciare tutti i processi in esecuzione, ed anche tutti i processi che saranno creati in seguito. I nuovi processi devono essere agganciati prima che eseguano la loro prima istruzione, altrimenti saranno in grado di vedere gli oggetti che abbiamo nascosto prima che essi vengano agganciati. =====[ 7.1 Diritti ]============================================================ Bisogna sapere per prima cosa che abbiamo bisogno, come minimo, dei diritti dell'amministratore per accedere a tutti i processi in corso. La migliore possibilità è eseguire i nostri processi come servizio di sistema, che viene eseguito come utente SYSTEM sulla macchina. Per installare il servizio abbiamo anche bisogno di speciali diritti. Anche ottenere SeDebugPrivilege è molto utile. Si può fare usando OpenProcessToken, LookupPrivilegeValue e 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 ); Tralasciando gli errori il codice può apparire come questo: #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 Aggancio globale ]==================================================== L'enumerazione dei processi è eseguita per i metodi già citati dalla funzione NtQuerySystemInformation. Vi sono pochi processi nativi nel sistema, così useremo il metodo di riscrivere la prima istruzione della funzione per agganciarli. Faremo lo stesso per tutti i processi in esecuzione. Allocheremo una parte di memoria nel processo vittima dove scriveremo il nostro nuovo codice per le funzioni che vogliamo agganciare. Quindi cambieremo i primi cinque byte di queste funzioni con l'istruzione jmp. Così tale istruzione sarà eseguita immediatamente quando la funzione agganciata viene chiamata. Come salvare le istruzioni è stato descritto nel capitolo 3.2.3. del documento "Hooking Windows API". La prima cosa da fare è aprire il processo vittima con NtOpenProcess e ottenere il suo handle. Se non abbiamo sufficienti privilegi ciò fallirà. NTSTATUS NtOpenProcess( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL ); ProcessHandle è un puntatore a un handle dove si andrà a registrare il risultato. DesiredAccess deve essere settato al valore PROCESS_ALL_ACCESS. Porremo il PID del processo vittima a UniqueProcess, nella struttura ClientId, e UniqueThread dovrà essere posto a 0. L'handle aperto si può sempre chiuderlo con NtClose. #define PROCESS_ALL_ACCESS 0x001F0FFF Ora allocheremo la parte di memoria di cui necessitiamo per il nostro codice. Per quello utilizziamo NtAllocateVirtualMemory. NTSTATUS NtAllocateVirtualMemory( IN HANDLE ProcessHandle, IN OUT PVOID *BaseAddress, IN ULONG ZeroBits, IN OUT PULONG AllocationSize, IN ULONG AllocationType, IN ULONG Protect ); ProcessHandle è ottenuto con NtOpenProcess. BaseAddress è un puntatore ad un altro puntatore all'inizio della zona dove allocheremo. Qui si registrerà l'indirizzo della memoria allocata. Il valore di input può essere NULL. AllocationSize è un puntatore al numero di byte che vogliamo allocare. E di nuovo viene anche usato come valore di output per il numero di byte allocati. E' bene porre AllocationType a MEM_TOP_DOWN in aggiunta con MEM_COMMIT perchè cosi la memoria sarà allocata il più in alto possibile vicino alle DLL. #define MEM_COMMIT 0x00001000 #define MEM_TOP_DOWN 0x00100000 Quindi possiamo scrivere il nostro codice con NtWriteVirtualMemory. NTSTATUS NtWriteVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); BaseAddress sarà l'indirizzo restituito per NtAllocateVirtualMemory. Il buffer punta ai byte che vogliamo scrivere, è BufferLength è il numero di byte che vogliamo scrivere. Ora dobbiamo agganciare le funzioni singole. L'unica libreria che è caricata per tutti i processi è ntdll.dll. Così dobbiamo stabilire se la funzione che vogliamo agganciare è importata e se non viene da ntdll.dll. Perè la memoria dove sarà questa funzione potrà restare riservata, sicchè riscrivere byte al suo indirizzo potrà provocare un errore nel processo vittima. E' per questo che dobbiamo stabilire se la libreria (dove si trova la funzione che vogliamo agganciare) è caricata nel processo vittima o no. Dobbiamo ottenere il PEB (Process Environment Block, Blocco d'ambiente del processo) mediante NtQueryInformationProcess. NTSTATUS NtQueryInformationProcess( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL ); Setteremo ProcessInformationClass a ProcessBasicInformation. Quindi la struttura PROCESS_BASIC_INFORMATION sarà restituita nel buffer ProcessInformation, la cui dimensione è data da 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 è quello che stiamo cercando. In PebBaseAddress+0x0C vi è l'indirizzo PPEB_LDR_DATA. Questo verrà ottenuto chiamando NtReadVirtualMemory. NTSTATUS NtReadVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, OUT PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); I parametri sono uguali a quelli in NtWriteVirtualMemory. In PPEB_LDR_DATA+0x1C vi è l'indirizzo di InInitializationOrderModuleList. E' la lista delle librerie caricate nel processo. Ci interessa solo una parte di questa struttura. typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST { PVOID Next, PVOID Prev, DWORD ImageBase, DWORD ImageEntry, DWORD ImageSize, ... ); Next è un puntatore al successivo registro, Prev al precedente, l'ultimo registro punta al primo. ImageBase è l'indirizzo del modulo nella memoria, ImageEntry è l'EntryPoint del modulo, ImageSize è la dimensione. Per tutte le librerie che vogliamo agganciare dobbiamo ottenere la loro ImageBase (per esempio usando GetModuleHandle or LoadLibrary). Tale ImageBase la compariamo con ImagaBase di ogni entrata in InInitializationOrderModuleList. Ora siamo pronti per agganciare. Poichè stiamo agganciando processi in esecuzione c'è la possibilità che il codice che eseguiremo al momento venga riscritto. Questo causerà un errore, così per prima cosa fermeremo tutti i thread del processo vittima. La lista dei suoi thread si ottiene con NtQuerySystemInformation, con SystemProcessesAndThreadsInformation come SystemInformationClass. Il risultato di questa funzione è descritto nel capitolo 4. Ma dobbiamo aggiungere la descrizione della struttura SYSTEM_THREADS dove si trova l'informazione sul thread. 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; Per ogni thread dobbiamo ottenere il suo handle usando NtOpenThread. Useremo ClientId per questo. NTSTATUS NtOpenThread( OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId ); L'handle che vogliamo sarà registrato in ThreadHandle. Settiamo DesiredAccess su THREAD_SUSPEND_RESUME. #define THREAD_SUSPEND_RESUME 2 ThreadHandle will be used for calling NtSuspendThread. ThreadHandle sarà usato per chiamare NTSuspendThread. NTSTATUS NtSuspendThread( IN HANDLE ThreadHandle, OUT PULONG PreviousSuspendCount OPTIONAL ); Il processo sospeso è pronto per essere riscritto. Procederemo come vien descritto nel capitolo 3.2.2 in "Hooking Windows API". L'unica differenza è che useremo funzioni per altri processi. Dopo un aggancio riceveremo tutti i thread del processo mediante NtResumeThread. NTSTATUS NtResumeThread( IN HANDLE ThreadHandle, OUT PULONG PreviousSuspendCount OPTIONAL ); =====[ 7.3 Nuovi processi ]==================================================== L' infezione di tutti i processi in esecuzione non colpisce i processi che saranno eseguiti in seguito. Possiamo avere la lista dei processi e dopo averne una nuova ed infettare quelli che non si troveranno nella prima lista poichè saranno nuovi. Tuttavia questo metodo non è attuabile. E' molto meglio agganciare la funzione che si chiama per creare un nuovo processo. Con questo metodo non mancheremo alcun processo. Possiamo agganciare NtCreateThread, ma non è semplice. Agganceremo NtResumeThread che vien sempre chiamato ogni volta che si crea un nuovo processo. E' chiamato anche dopo NtCreateThread. L' unico problema con NtResumeThread è che non si chiama solo quando comincia un nuovo processo. Ma questo si supera con facilità. NtQueryInformationThread ci darà l'informazione su quale processo possiede lo specifico thread. L'ultima cosa da fare è verificare se il processo è già agganciato o no. Lo facciamo leggendo il primo byte di ogni funzione che agganciamo. NTSTATUS NtQueryInformationThread( IN HANDLE ThreadHandle, IN THREADINFOCLASS ThreadInformationClass, OUT PVOID ThreadInformation, IN ULONG ThreadInformationLength, OUT PULONG ReturnLength OPTIONAL ); ThreadInformationClass è il tipo di informazione che in tal caso deve essere settata a ThreadBasicInformation. ThreadInformation è il buffer da cui risulta di quanti byte è la dimensione di ThreadInformationLength. #define ThreadBasicInformation 0 Per la classe ThreadBasicInformation è restituita questa struttura: 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; In ClientId è PID del processo che possiede il thread. Ora dobbiamo infettare il nuovo processo. Il problema è che il nuovo processo ha solo in memoria ntdll.dll. Tutti gli altri moduli sono caricati subito dopo aver chiamato NtResumeThread. Ci sono diversi modi di gestire questo problema. Per esempio, possiamo agganciare la API LdrInitializeThunk che è chiamata durante l'inizializzazione del processo. NTSTATUS LdrInitializeThunk( DWORD Unknown1, DWORD Unknown2, DWORD Unknown3 ); Per prima cosa eseguiremo il codice originale e poi agganceremo le funzioni che vogliamo in questo nuovo processo. Ma è meglio sganciare LdrInitializeThunk perchè è chiamato molte volte dopo e noi non vogliamo riagganciare tutte le funzionidi nuovo. Tutto qui si fa prima dell'esecuzione della prima istruzione dell'applicazione agganciata.Perciò non si può chiamare nessuna funzione agganciata prima che noi l'agganciamo. L'aggancio in sè è lo stesso quando si agganciano processi in esecuzione, ma qui non ci curiamo dei thread in esecuzione. =====[ 7.4 DLL ]================================================================ In ogni processo del sistema c'è una copia di ntdll.dll. Ciò significa che possiamo agganciare ogni funzione di questo modulo nell'inizializzazione del processo. Ma che fare con funzioni di altri moduli come kernel32.dll o advapi32.dll? E ci sono vari processi che hanno solo ntdll.dll. Tutti gli altri moduli possono essere caricati dinamicamente nel mezzo del codice dopo l'aggancio. Perciò dobbiamo poi agganciare LdrLoadDll che carica i nuovi moduli. NTSTATUS LdrLoadDll( PWSTR szcwPath, PDWORD pdwLdrErr, PUNICODE_STRING pUniModuleName, PHINSTANCE pResultInstance ); La cosa più importante per noi qui è pUniModuleName, che è il nome del modulo. pResultInstance conterrà il suo indirizzo se la chiamata ha successo. Chaimeremo la LdrLoadDll originale e poi agganceremo tutte le funzioni del modulo caricato. =====[ 8. Memoria ]============================================================= Quando agganciamo una funzione modifichiamo i suoi primi 5 byte. Chiamando NtReadVirtualMemory chiunque può scoprire che una funzione è stata agganciata. Così dobbiamo agganciare NtReadVirtualMemory per evitare di essere scoperti. NTSTATUS NtReadVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, OUT PVOID Buffer, IN ULONG BufferLength, OUT PULONG ReturnLength OPTIONAL ); Abbiamo cambiato i byte all'inizio di tutte le funzioni che agganciamo e abbiamo anche allocato memoria per il nostro nuovo codice. Dobbiamo vedere se chi chiama legge qualcuno di questi byte. Se abbiamo i nostri byte nel range da BaseAddress a BaseAddress + BufferLength dobbiamo cambiare alcuni byte in Buffer. Si uno chiede byte della nostra zona allocata dobbiamo restituire Buffer vuoto e un errore STATUS_PARTIAL_COPY. Questo valore dice che non sono stati copiati tutti i byte nel buffer. Si usa anche quando si chiede memoria non allocata. ReturnLength dovrà in tal caso essere settato a 0. #define STATUS_PARTIAL_COPY 0x8000000D Se uno chiede i primi byte di una funzione agganciata dobbiamo chiamare il codice originale e copiare i byte originali il Buffer (li abbiamo salvati per le chiamate originali). Ora il processo non è in grado di scoprire l'aggancio leggendo la memoria. Anche se analizzassimo il processo originale con un debugger questo avrà problemi. Mostrerà i byte originali ma eseguirà il nostro codice. Per un'occultazione perfetta possiamo anche agganciare NtQueryVirtualMemory. Questa funzione è usata per ottenere informazioni sulla memoria virtuale. Possiamo agganciarla per non far scoprire la nostra memoria allocata. NTSTATUS NtQueryVirtualMemory( IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN MEMORY_INFORMATION_CLASS MemoryInformationClass, OUT PVOID MemoryInformation, IN ULONG MemoryInformationLength, OUT PULONG ReturnLength OPTIONAL ); MemoryInformationClass specifica la classe dei datos restituiti. Ci interessano i primi due tipi. #define MemoryBasicInformation 0 #define MemoryWorkingSetList 1 Per la classe MemoryBasicInformation si restituisce questa struttura: 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; Ogni sezione di memoria ha la sua dimensione RegionSize e il suo tipo Type. La memoria libera è del tipo MEM_FREE. #define MEM_FREE 0x10000 Se una sezione prima della nostra ha il tipo MEM_FREE dobbiamo aggiungere la dimensione della nostra sezione al suo RegionSize. Se la sezione dopo è anche MEM_FREE dobbiamo aggiungere la sezione seguente a quella RegionSize. Se una sezione prima della nostra ha un altro tipo restituiremo MEM_FREE per la nostra sezione. La sua dimensione è calcolata di nuovo secondo la sezione successiva. Per la classe MemoryWorkingSetList si restituisce la struttura: typedef struct _MEMORY_WORKING_SET_LIST { ULONG NumberOfPages; ULONG WorkingSetList[1]; } MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST; NumberOfPages è il numero degli elementi di WorkingSetList. Questo numero deve essere decrementato. Dobbiamo trovare la nostra sezione in WorkingSetList e muovere i registri seguenti sopra i nostri. WorkingSetList è un array di DWORDs dove i 20 bit di maggior peso specificano i 20 bit più alti dell'indirizzo della sezione e i 12 bit più bassi specificano i flag. =====[ 9. Handle ]============================================================== Chiamare NtQuerySystemInformation con SystemHandleInformation come classe ci restituisce un array di tutti gli handle aperti nella struttura _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 specifica il processo che possiede l'handle. ObjectTypeNumber è il tipo di handle. NumberOfHandles è il numero di registri nell'array Information. Nacondere un elemento è banale. Dobbiamo rimuovere tutti i registri seguenti uno per uno e decrementare NumberOfHandles. Rimuovere tutti i seguenti è necessario perchè gli handle nell'array sono raggruppati per ProcessId. Ciò significa che tutti gli handle di un medesimo processo sono insieme. E per un processo il numero di Handle è crescente. Ora ricordate la struttura _SYSTEM_PROCESSES che è restituita per questa funzione con classe SystemProcessesAndThreadsInformation. Qui si può vedere che ogni processo ha un'informazione sul suo numero di handle in HandleCount. Se vogliamo la perfezione dobbiamo modificare HandleCount con quanti handle nascondiamo quando si chiama questa funzione con classe SystemProcessesAndThreadsInformation. Tuttavia questa correzione consumerà troppo tempo. Ci sono molti handle che si aprono e si chiudono in un lasso di tempo molto breve durante l'esecuzione del sistema. Così può accadere che il numero di handle cambi durante due chiamate a questa funzione e non abbiamo bisogno di cambiare HandleCount. =====[ 9.1 Nominare un handle ed ottenere il tipo ]============================= Nascondere un handle è banale, ma trovare l'handle da nascondere è molto più complicato. Se abbiamo per esempio un processo nascosto dobbiamo nascondere tutti i suoi handle e tutti gli handle ad esso connessi. Nascondere gli handle di tale processo è ancora banale. Compariamo solo il processId dell'handle e il PID dei nostri processi e quando sono uguali li nascondiamo. Ma gli handle degli altri processi devono essere nominati prima di poter comparare alcuno. Il numero di handle nel sistema è di solito molto grande, così il meglio che possiamo fare è comparare il tipo di handle prima di tentare di nominarlo. Nominare i tipi ci risparmierà molto tempo per gli handle che non ci interessano. Nominare un handle e il tipo di handle si fa con NtQueryObject. NTSTATUS ZwQueryObject( IN HANDLE ObjectHandle, IN OBJECT_INFORMATION_CLASS ObjectInformationClass, OUT PVOID ObjectInformation, IN ULONG ObjectInformationLength, OUT PULONG ReturnLength OPTIONAL ); ObjectHandle è l'handle di cui vogliamo ottenere informazioni. ObjectInformationClass è il tipo di informazione che sarà registrata nel buffer ObjectInformation che occupa ObjectInformation byte. Usiamo la classe ObjectNameInformation e ObjectAllTypesInformation. We will use class ObjectNameInformation and ObjectAllTypesInformation. ObjectNameInfromation riempirà il buffer con la struttura OBJECT_NAME_INFORMATION e ObjectAllTypesInformation con la struttura 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 il nome dell'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 il nome del tipo di oggetto che immediatamente segue ogni struttura OBJECT_TYPE_INFORMATION. La seguente struttura OBJECT_TYPE_INFORMATION segue questo Name, cominciando nel limite dei primi quattro bytes. ObjectTypeNumber della estructura SYSTEM_HANDLE_INFORMATION è un indice dell'array TypeInformation. Più difficile è ottenere il nome dell'handle di un altro processo. Ci sono due possibilità di nominarlo. La prima è copiare l'handle con NtDuplicateObject al nostro processo e poi nominarlo. Questo metodo fallirà per alcuni tipi specifici di handle. Ma fallirà solo per pochi, così possiamo usare tranquillamente questo 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 è un handle del processo che possiede SourceHandle, che è l'handle che vogliamo copiare. TargetProcessHandle è l'handle del processo da copiare. Questo sarà l'handle del nostro processo nel nostro caso. TargetHandle è il puntatore all'handle dove vogliamo salvare una copia dell'handle originale. DesiredAccess dovrà essere settato a PROCESS_QUERY_INFORMATION, Attributes e Options a 0. Il secondo metodo che funziona con ogni handle è quello di usare un driver di sistema. Il codigo sorgente di questo è disponible nel progetto OpHandle su http://rootkit.host.sk. =====[ 10. Porte ]============================================================ Il modo più facile di enumerare le porte aperte è usare AllocateAndGetTcpTableFromStack e AllocateAndGetUdpTableFromStack, oppure AllocateAndGetTcpExTableFromStack e AllocateAndGetUdpExTableFromStack di iphlpapi.dll. Le funzioni Ex sono disponibili da 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; ); C'è un altro modo di fare ciò. Quando un programma crea un socket e si pone in ascolto sicuramente ha un handle aperto per quello e per la porta aperta. Possiamo enumerare tutti gli handle aperti nel sistema e inviargli un buffer speciale mediante NtDeviceIoControlFile per scoprire se l'handle è per una porta aperta o no. Questo ci darà anche informazioni sulla porta. Poichè ci sono molti handle aperti testeremo solo gli handle il cui tipo sia File e il nome sia \Device\Tcp o \Device\Udp. Le porte aperte hanno solo handle di questo tipo e nome. Quando vediamo il codice delle funzioni di iphlpapi.dll di sopra scopriamo che anche queste funzioni chiamano NtDeviceIoControlFiley ed inviano buffer specialei per ottenere una lista di tutte le porte aperte nel sistema. Ciò significa che l'unica funzione di cui bisogniamo per nascondere le porte è 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 ); Gli argomenti interessanti per noi sono FileHandle che specifica un handle del dispositivo con cui comunicare, IoStatusBlock che punta a una variabile che riceve lo stato del completamento finale e informazioni sulla operazione richiesta, IoControlCode che è un numero che specifica il tipo del dispositivo, metodo, accesso del file e una funzione. InputBuffer contiene dati di input che sono InputBufferLength bytes e similmente OutputBuffer e OutputbufferLength. =====[ 10.1 Netstat, OpPorts in WinXP, FPort in WinXP ]========================= Ottenere una lista di tutte le porte aperte è il primo modo usato per esempio da OpPorts e FPort in Windows Xp e anche Netstat. Questi programmi chiamano NtDeviceIoControlFile due volte con IoControlCode 0x000120003. OutputBuffer è scritto dopo una seconda chiamata. Il nome di FileHandle è sempre \Device\Tcp. InputBuffer differisce per distinti tipi di chiamata: 1) Per ottenere un array di MIB_TCPROW InputBuffer si avrà: prima chiamata: 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 seconda chiamata: 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) Per ottenere un array di MIB_UDPROW: prima chiamata: 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 seconda chiamata: 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) Per ottenere un array di MIB_TCPROW_EX: prima chiamata: 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 seconda chiamata: 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) Per ottenere un array di MIB_UDPROW_EX: prima chiamata: 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 seconda chiamata: 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 Si vede che i buffer sono differenti solo in pochi byte. Possiamo chiaramente raggrupparli: Le chiamate che ci interessano hanno InputBuffer[1] settato a 0x04 e principalmente InputBuffer[17] su 0x01. Solo dopo questi dati di input si riempirà l' OutputBuffer con le tavole desiderate. Se vogliamo avere info sulle porte TCP settiamo InputBuffer[0] a 0x00, e per UDP a 0x01. Se vogliamo le tavole estese di output (MIB_TCPROW_EX o MIB_UDPROW_EX) usiamo Inputbuffer[16] nella seconda chiamata posta a 0x02. Se scopriamo la chiamata con questi parametri possiamo cambiare il buffer di output. Per ottenere i numeri di fila nel buffer di output semplicemente dividiamo Information da IoStatusBlock per la dimensione della fila. Nascondere una fila ora è facile. Soltanto riscriverla con le file seguenti cancellandone l'ultima. Non dimenticate di cambiare OutputBufferLength e IoStatusBlock. =====[ 10.2 OpPorts in Win2k e NT4, FPort in Win2k ]============================ Usiamo NtDeviceIoControlFile con IoControlCode 0x00210012 per determinare se l'handle di tipo File e nome \Device\Tcp o \Device\Udp è l'handle della porta aperta. Così dapprima compariamo IoControlCode e poi il tipo e nome dell'handle. Sè è ancora interessante compariamo poi la lunghezza del buffer di input che deve essere uguale alla lunghezza della struttura TDI_CONNECTION_IN. Questa lunghezza è 0x18. OutputBuffer è 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; L'implementazione concreta di come determinare se l'handle è una porta aperta è disponibile nel codice sorgente di OpPorts in http://www.hxdef.org. Siamo interessati ora a nascondere una porta specifica. Già abbiamo comparato InputBufferLength e IoControlCode. Ora dobbiamo comparare RemoteAddressLength. Questa è sempre 3 o 4 per una porta aperta. L'ultima cosa che dobbiamo fare è comparare ReceivedTsdus di OutputBuffer che contiene la porta nella forma network e la nostra lista di porte che si vuole nascondere. La differenza fra TCP e UDP è nel nome dell'handle. Cancellando OutputBuffer, cambiando IoStatusBlock e restituendo il valore di STATUS_INVALID_ADDRESS nasconderemo questa porta. =====[ 11. Conclusione ]======================================================== L'implementazione delle tecniche qui descritte saranno disponibili con il sorgente di Hacker Defender 1.0.0 alla pagina http://www.hxdef.org o su http://www.rootkit.com. E' possibile che aggiungerò più informazioni sull'invisibilità in Win NT in futuro. Nuove versioni di questo documento potranno contenere miglioramenti delle tecniche descritte o nuovi commenti. Ringraziamenti speciali a Ratter che mi insegnò il necessario per scrivere questo documento e per scrivere Hacker Defender. Inviatemi tutti i commenti a holy_father@phreaker.net o al forum di http://www.hxdef.org. (Ringraziamenti del traduttore: Grazie ad holy_father per aver permesso questa traduzione. Grazie al forum di sicurezza di Html.it ed in particolare ad amvinfe, Habanero, cso, holifay e Lucass e a tutti gli altri. Un grazie infine va alla crew e a tutti gli utenti di Hackeralliance.net.) ===================================[ Fine ]======================================