Reverse Engineering: ProcAPI.dll
Pubblicato prima del 07/05/2008 - Informazione assente
Premessa: Il reverse engineering, inteso come processo di esamina e interpretazione del funzionamento di un programma non è considerato reato. Tuttavia la legittimità di molti aspetti del reversing è ancor'oggi dibattuta. --- Argomento: In questo articolo mostrerò come è possibile, tramite il reverse engineering, documentare le funzioni presenti in una DLL, i cui prototipi/scopi sono sconosciuti. Mi occupero dell'analisi della libreria ProcApi.dll, distribuita assieme al programma TAT (Thermal Analysis Tool) di Intel Corporation. Questo software ha il compito di monitorare i sensori della temperatura presenti nei core dei processori core2duo, include una procedura di test stabilità e fornisce diverse informazioni sull'hardware presente nel computer. La procApi.dll si occupa di una parte di quest'ultimo aspetto. --- Conoscenze: Voglio precisare che i temi trattati da questo articolo sono di livello medio-avanzato. Per comprenderlo a fondo consiglio solide conoscenze di C, Assembler e win32 API. --- Programmi utilizzati: Esistono numerosi disassemblatori presenti sul web, gratuiti e non. Personalmente preferisco IDA Pro Advanced, distribuito da DataRescue in versione trial della durata di 30 giorni. Per chi volesse acquistarlo sono appena 690$ ;) La grandiosità di IDA è nella generazione dei flow-charts delle funzioni, ovvero dei grafici ad albero che rappresentano il flusso del codice. Inoltre colora la sintassi e identifica tipi e variabili globali a partire dagli argomenti delle funzioni note. La strada del free ci porta invece a Ollydbg. E' free solo per i privati, per l'uso professionale bisogna comprare la licenza. Ha una buona interfaccia, un po più spartana di IDA, ma fa lo stesso egregiamente il suo compito. --- Iniziamo l'analisi della dll: --- Primo passo: l'Export Table L'export table è la lista delle funzioni esposte da una dll, ovvero quelle funzioni che possono essere richiamate da programmi esterni: Name Entry Ordinal ProcAPI_AcquireThermalAPI 10002560 1 ProcAPI_GetProcessorCount 10002480 2 ProcAPI_GetProcessorFrequency 100024A0 3 ProcAPI_GetThermalMonitorStatus 100025B0 4 ProcAPI_ReleaseThermalAPI 10002580 5 DllEntryPoint 10002C8D A parte la consueta DllEntryPoint che viene chiamata quando la libreria viene caricata (ad esempio con LoadLibrary), notiamo la presenza di altre 5 funzioni. I nomi sono auto-esplicativi, quello che non sappiamo ancora è come chiamarle, ovvero dobbiamo riuscire a capire che prototipi hanno queste funzioni. Il passo successivo consisterà nel capire come usarle e quali informazioni restituiranno. Possiamo già immaginare che ProcAPI_AcquireThermalAPI servirà per l'inizializzazione e ProcAPI_ReleaseThermalAPI rilascerà le risorse/i drivers della libreria. Ma analizziamole più nel dettaglio. --- Prima fn: ProcAPI_AcquireThermalAPI Diamo subito un'occhiata al codice: ================================================== 10002560 public ProcAPI_AcquireThermalAPI 10002560 ProcAPI_AcquireThermalAPI proc near 10002560 10002560 arg_0 = dword ptr 4 10002560 10002560 mov eax, dword_1000DFD0 10002565 cmp dword ptr [eax], 0 10002568 jz short loc_10002570 1000256A mov eax, 0A0000205h 1000256F retn 10002570 10002570 loc_10002570: 10002570 mov ecx, [esp+arg_0] 10002574 mov [eax], ecx 10002576 xor eax, eax 10002578 retn 10002578 10002578 ProcAPI_AcquireThermalAPI endp =================================================== Come vedete è molto semplice, IDA inoltre tiene traccia dello stack frame e quindi è in grado di approssimare quanti parametri vengono passati basandosi sulla loro dimensione e sull'uso nel codice. Vediamo che viene passato un parametro lungo 4 bytes (32 bits) che potrebbe essere un DWORD ma non lo sappiamo ancora. Le prime due istruzioni effettuano il controllo sul valore puntato da una variabile globale chiamata arbitrariamente "dword_1000DFD0". Se il suo valore è 0 (jz -> Jump If Zero) salta all'etichetta loc_10002570: mov ecx, [esp+arg_0] mov [eax], ecx queste due istruzioni non fanno altro che copiare il valore del parametro passato nella variabile globale citata prima. L'istruzione seguente imposta eax a 0 (facendo lo xor di una variabile su se stessa la si imposta a 0) e la funzione ritorna al chiamante. Nella convenzione di chiamata stdcall (quella delle win32 API)Il registro eax viene utilizzato per il valore di ritorno della funzione, quindi le ultime righe di codice equivalgono a un "return 0;" . Torniamo ad analizzare il secondo flow path, nel caso il valore puntato da dword_1000DFD0 non sia 0 imposta eax a 0x0A0000205, una sorta di error code che interpreteremo andando avanti nella nostra analisi. Questa funzione è tanto semplice che potremmo riscriverne il sorgente! Proviamoci allora: DWORD ProcAPI_AcquireThermalAPI(DWORD dwPar) { if (!*pdwUnknown_Global) { *pdwUnknown_Global = dwPar; return 0; } else { return 0x0A0000205; } } Wow, niente male... peccato per quel pdwUnknown_Global, ma tanto prima o poi capiremo :) --- Seconda fn: ProcAPI_ReleaseThermalAPI Consueta occhiata al codice: =================================================== 10002580 public ProcAPI_ReleaseThermalAPI 10002580 ProcAPI_ReleaseThermalAPI proc near 10002580 10002580 arg_0 = dword ptr 4 10002580 10002580 mov ecx, dword_1000DFD0 10002586 mov eax, [ecx] 10002588 test eax, eax 1000258A jnz short loc_10002592 1000258C mov eax, 0A0000206h 10002591 retn 10002592 10002592 loc_10002592: 10002592 cmp eax, [esp+arg_0] 10002596 jz short loc_1000259E 10002598 mov eax, 0A0000205h 1000259D retn 1000259E 1000259E loc_1000259E: 1000259E mov dword ptr [ecx], 0 100025A4 xor eax, eax 100025A6 retn 100025A6 100025A6 ProcAPI_ReleaseThermalAPI endp =================================================== E' sempre meglio analizzare prima le funzioni di inizializzazione e di finalizzazione perchè ci possono dare molte informazioni preziose e immediate, data la loro particolare brevità. Anche qui niente di complesso, accetta un parametro di lunghezza DWORD ma notiamo subito che effettua una lettura alla stessa variabile globale vista in ProcAPI_AcquireThermalAPI ovvero dword_1000DFD0. Viene caricato in eax il valore della varibile globale e testato il valore 0 (l'istruzione test al posto di cmp effettua un and tra i suoi operandi quindi l'unico valore che testato da solo da false è proprio lo 0). Se dword_1000DFD0 non è zero (jnz -> Jump if Not Zero) salta all'etichetta loc_10002592, dove viene confrontato con il parametro passato. Se hanno lo stesso valore va a loc_1000259E dove il valore puntato da dword_1000DFD0 viene impostato a 0, come anche il valore di ritorno della funzione. Torniamo indietro agli altri flow paths, nel caso arg_0 e dword_1000DFD0 non combaciassero la funzione ritornerebbe 0x0A0000205. Se invece la variabile globale fosse 0 ritornerebbe 0x0A0000206. Questi due ultimi valori sono i migliori candidati per essere error codes, come quello visto nella ProcAPI_AcquireThermalAPI. Anche questa funzione non è eccessivamente complessa, quindi possiamo agevolmente riscriverne il codice: DWORD ProcAPI_ReleaseThermalAPI(DWORD dwPar) { if (!*pdwUnknown_Global) { if (*pdwUnknown_Global == dwPar) { *pdwUnknown_Global = 0; return 0; } else return 0x0A0000205; else return 0x0A0000206; } Bene, possiamo iniziare fare qualche congettura su pdwUnknown_Global: sembrerebbe una sorta di variabile globale il cui valore rappresenta una specie di "ID di sessione" assegnato all'inizializzazione della libreria con ProcAPI_AcquireThermalAPI e che deve necessariamente essere necessariamente lo stesso per scaricare la libreria con ProcAPI_ReleaseThermalAPI. Sarà vero? --- Terza fn: ProcAPI_GetProcessorCount Ecco il codice: =================================================== 10002480 public ProcAPI_GetProcessorCount 10002480 ProcAPI_GetProcessorCount proc near 10002480 10002480 arg_0 = dword ptr 4 10002480 10002480 mov eax, dword_1000DFD0 10002485 mov edx, [esp+arg_0] 10002489 mov ecx, [eax+8] 1000248C xor eax, eax 1000248E mov [edx], ecx 10002490 retn 10002480 10002490 ProcAPI_GetProcessorCount endp =================================================== Questa funzione, a dir del nome, restituirà il numero di processori presenti nel sistema, la domanda ora è: come? Analizziamo riga per riga per scoprirlo. Torna in gioco la (solita) variabile globale dword_1000DFD0, ma stavolta in modo diverso: mov ecx, [eax+8] questa istruzione accende subito un campanello d'allarme, ma allora dword_1000DFD0 non è una semplice variabile globale DWORD! Il puntatore viene incrementato di 8 bytes, possiamo supporre che dword_1000DFD0 sia un puntatore a una struttura che potrebbe avere questa dichiarazione: struct Unk_Struct { DWORD dwID; // [dword_1000DFD0] DWORD dwUnknown; // [dword_1000DFD0 + 4] DWORD dwProcessorCount; // [dword_1000DFD0 + 8] }; Di solito i membri delle strutture sono 32bit aligned il che significa che ogni "slot" è di 32bit, il fatto che il puntatore alla struttura venga incrementato di 8 bytes significa che "salta" la dwID e un secondo membro di cui non possiamo dire niente. Chi riempie questa struttura non lo sappiamo, però possiamo capire in che modo vengono utilizzati i suoi campi. Andiamo avanti nella lettura del codice: xor eax, eax mov [edx], ecx Il solito xor imposta il valore di ritorno a 0, invece la seconda riga ci dice, anzi ci confessa, cosa'è il parametro arg_0: un puntatore. Infatti il campo numero 3 della struttura (dwProcessorCount) viene copiato nel valore puntato da arg_0! Anche qui è possibile ricostruire il codice sorgente: DWORD ProcAPI_GetProcessorCount(PDWORD pdwNum) { *pdwNum = pUnknown_Global -> dwProcessorCount; return 0; } --- Quarta fn: ProcAPI_GetProcessorFrequency Codice: =================================================== 100024A0 public ProcAPI_GetProcessorFrequency 100024A0 ProcAPI_GetProcessorFrequency proc near 100024A0 100024A0 var_8 = dword ptr -8 100024A0 var_4 = dword ptr -4 100024A0 arg_0 = dword ptr 4 100024A0 arg_4 = dword ptr 8 100024A0 100024A0 sub esp, 8 100024A3 push ebx 100024A4 mov ebx, [esp+0Ch+arg_4] 100024A8 push esi 100024A9 mov esi, [esp+10h+arg_0] 100024AD mov dword ptr [ebx], 0 100024B3 mov eax, dword_1000DFD0 100024B8 mov [esp+10h+var_8], 0 100024C0 mov [esp+10h+var_4], 0 100024C8 cmp esi, [eax+8] 100024CB jb short loc_100024D8 100024CD pop esi 100024CE mov eax, 0A0000201h 100024D3 pop ebx 100024D4 add esp, 8 100024D7 retn 100024D8 100024D8 loc_100024D8: 100024D8 mov ecx, dword_1000DFE4 100024DE push 3E8h 100024E3 push ecx 100024E4 call ds:WaitForSingleObject 100024EA test eax, eax 100024EC jnz short loc_10002547 100024EE lea edx, [esp+10h+var_4] 100024F2 push eax 100024F3 push edx 100024F4 lea eax, [esp+18h+var_8] 100024F8 lea ecx, [esi+esi*4] 100024FB push 4 100024FD push eax 100024FE mov eax, dword_1000DFD4 10002503 lea edx, [esi+ecx*2] 10002506 push 2Ch 10002508 lea ecx, [eax+edx*4] 1000250B push ecx 1000250C mov ecx, dword_1000DFDC 10002512 push 8031200Ch 10002517 call sub_10001400 1000251C test eax, eax 1000251E jz short loc_1000253A 10002520 mov edx, [esp+10h+var_8] 10002524 mov [ebx], edx 10002526 mov eax, dword_1000DFE4 1000252B push eax 1000252C call ds:ReleaseMutex 10002532 pop esi 10002533 xor eax, eax 10002535 pop ebx 10002536 add esp, 8 10002539 retn 1000253A 1000253A loc_1000253A: 1000253A mov ecx, dword_1000DFE4 10002540 push ecx 10002541 call ds:ReleaseMutex 10002547 10002547 loc_10002547: 10002547 pop esi 10002548 mov eax, 0A0000000h 1000254D pop ebx 1000254E add esp, 8 10002551 retn 10002551 10002551 ProcAPI_GetProcessorFrequency endp =================================================== Finalmente abbiamo pane per i nostri denti! Bene, iniziamo la nostra analisi. sub esp, 8 Con questa istruzione la funzione riserva dello spazio nello stack per due variabili locali di 4 bytes ciascuna, in pratica sottrae 4 bytes al registro esp che è il puntatore allo stack. 100024A3 push ebx 100024A4 mov ebx, [esp+0Ch+arg_4] 100024A8 push esi 100024A9 mov esi, [esp+10h+arg_0] 100024AD mov dword ptr [ebx], 0 100024B3 mov eax, dword_1000DFD0 100024B8 mov [esp+10h+var_8], 0 100024C0 mov [esp+10h+var_4], 0 100024C8 cmp esi, [eax+8] dopo aver caricato arg_4 in ebx e arg_0 in esi, azzerano il valore puntato da arg_4, var_8 e var_4. Ritorna in gioco il puntatore alla struttura globale dword_1000DFD0. Il suo terzo campo, che abbiamo detto rappresenta il numero di processori del sistema, viene confrontato con arg_0. Se il valore presente nell'argomento arg_0 è inferiore di dwProcessorsCount il codice prosegue, altrimenti la funzione ritorna l'errore 0x0A0000201. Quindi possiamo immaginare che il primo parametro rappresenza il numero del processore di cui vogliamo ottenere la frequenza. Procedendo con l'analisi del codice ci imbattiamo in una chiamata a WaitForSingleObject: 100024D8 mov ecx, dword_1000DFE4 100024DE push 3E8h 100024E3 push ecx 100024E4 call ds:WaitForSingleObject 100024EA test eax, eax 100024EC jnz short loc_10002547 WaitForSingleObject ha il seguente prototipo: DWORD WINAPI WaitForSingleObject( __in HANDLE hHandle, __in DWORD dwMilliseconds ); La convenzione di chiamata delle api win32 è stdcall, il che vuol dire che i parametri della funzione vengono pushati inversamente rispetto al prototipo. Quindi "3E8h" (= 1000) sono i millisecondi di attesa ed ecx contiene l'handle all'oggetto la cui disponibilità va aspettata. Ma qual'è l'handle che aspetta questa funzione? Vediamo nel dettaglio il frammento di codice in cui dword_1000DFE4 viene impostato: [Estratto da DllMain] 10001A46 push offset aProcapifrequen ; "ProcAPIFrequencyMutex" 10001A4B add eax, 0Ch 10001A4E push ebx 10001A4F push ebx 10001A50 mov dword_1000DFD4, eax 10001A55 call ds:CreateMutexA 10001A5B cmp eax, ebx 10001A5D mov dword_1000DFE4, eax Un mutex è uno degli strumenti di sincronizzazione tra threads concorrenti, e serve a smistare l'accesso a un'area di memoria condivisa. Potremmo scendere ulteriormente nel dettaglio ma andrebbe oltre gli scopi di questo articolo, a noi basta sapere che i dati presenti nella struttura pUnknown_Global risiedono in un'area di memoria condivisa tra più threads. E quindi per accedervi in sicurezza bisogna chiedere l'accesso esclusivo tramite un mutex. Torniamo a ProcAPI_GetProcessorFrequency. Le ultime due righe testano il valore di ritorno e se non è zero (jnz -> Jump if Not Zero) l'esecuzione continua, altrimenti salta all'etichetta loc_10002547 dove viene restituito l'errore 0x0A0000000. 100024EE lea edx, [esp+10h+var_4] 100024F2 push eax 100024F3 push edx 100024F4 lea eax, [esp+18h+var_8] 100024F8 lea ecx, [esi+esi*4] 100024FB push 4 100024FD push eax 100024FE mov eax, dword_1000DFD4 10002503 lea edx, [esi+ecx*2] 10002506 push 2Ch 10002508 lea ecx, [eax+edx*4] 1000250B push ecx 1000250C mov ecx, dword_1000DFDC 10002512 push 8031200Ch 10002517 call sub_10001400 1000251C test eax, eax 1000251E jz short loc_1000253A Queste righe sono abbastanza complesse, ma possiamo cercare di comprendere il loro scopo partendo dal fatto che alla riga 10002517 viene chiamata la subroutine sub_10001400. Quindi le istruzioni che precedono questa chiamata, e in particolare i push, sono importanti. Vediamo in dettaglio il codice di sub_10001400: 10001400 sub_10001400 proc near 10001400 10001400 arg_4 = dword ptr 4 10001400 arg_8 = dword ptr 8 10001400 arg_12 = dword ptr 0Ch 10001400 arg_16 = dword ptr 10h 10001400 arg_20 = dword ptr 14h 10001400 arg_24 = dword ptr 18h 10001400 arg_28 = dword ptr 1Ch 10001400 10001400 mov eax, [esp+arg_28] 10001404 mov edx, [esp+arg_24] 10001408 push eax 10001409 mov eax, [esp+4+arg_20] 1000140D mov ecx, [ecx+0C8h] 10001413 push edx 10001414 mov edx, [esp+8+arg_16] 10001418 push eax 10001419 mov eax, [esp+0Ch+arg_12] 1000141D push edx 1000141E mov edx, [esp+10h+arg_8] 10001422 push eax 10001423 mov eax, [esp+14h+arg_4] 10001427 push edx 10001428 push eax 10001429 push ecx 1000142A call ds:DeviceIoControl 10001430 neg eax 10001432 sbb eax, eax 10001434 neg eax 10001436 retn 1Ch 10001436 10001436 sub_10001400 endp Lo scopo di questa funzione è semplice. La chiamata a DeviceIoControl avviene con i parametri passati, a parte ecx il cui valore viene pushato a parte prima della chiamata alla sub. Quel valore è l'handle del device su cui effettuare la chiamata a DeviceIoControl. Riassumendo: questa funzione è un semplice wrapper per DeviceIoControl. L'api DeviceIoControl ritorna un buffer che contiene le informazioni richieste, e il valore contenuto in questo buffer viene assegnato al valore puntato da ebx quindi al parametro arg_4. Questo assegnamento ci fa capire che il secondo argomento è un puntatore e che viene utilizzato per restituire l'effettiva frequenza di clock del processore specificato con arg_0. Fatte queste considerazioni, possiamo senza timore scrivere il prototipo di questa funzione: DWORD ProcAPI_GetProcessorFrequency(DWORD dwProc, PDWORD pdwFreq); In cui dwProc è il numero del processore e pdwFreq è il puntatore all'area di memoria che ospiterà la frequenza del processore specificato. --- Quinta fn: ProcAPI_GetThermalMonitorStatus Codice: =================================================== 100025B0 public ProcAPI_GetThermalMonitorStatus 100025B0 ProcAPI_GetThermalMonitorStatus proc near 100025B0 100025B0 var_8 = dword ptr -8 100025B0 var_4 = dword ptr -4 100025B0 arg_0 = dword ptr 4 100025B0 arg_4 = dword ptr 8 100025B0 arg_12 = dword ptr 0Ch 100025B0 100025B0 sub esp, 8 100025B3 mov ecx, dword_1000DFD0 100025B9 mov [esp+8+var_4], 0 100025C1 mov [esp+8+var_8], 0 100025C9 push esi 100025CA mov eax, [ecx] 100025CC test eax, eax 100025CE jnz short loc_100025DA 100025D0 mov eax, 0A0000206h 100025D5 pop esi 100025D6 add esp, 8 100025D9 retn 100025DA 100025DA loc_100025DA: 100025DA cmp [esp+0Ch+arg_0], eax 100025DE jz short loc_100025EA 100025E0 mov eax, 0A0000205h 100025E5 pop esi 100025E6 add esp, 8 100025E9 retn 100025EA 100025EA loc_100025EA: 100025EA mov eax, [esp+0Ch+arg_4] 100025EE mov edx, [ecx+8] 100025F1 cmp eax, edx 100025F3 jb short loc_100025FF 100025F5 mov eax, 0A0000201h 100025FA pop esi 100025FB add esp, 8 100025FE retn 100025FF 100025FF loc_100025FF: 100025FF lea ecx, [eax+eax*4] 10002602 lea edx, [esp+0Ch+var_4] 10002606 push 0 10002608 push edx 10002609 lea esi, [eax+ecx*2] 1000260C mov ecx, dword_1000DFD4 10002612 shl esi, 2 10002615 lea eax, [esp+14h+var_8] 10002619 push 4 1000261B push eax 1000261C lea edx, [esi+ecx] 1000261F mov ecx, dword_1000DFDC 10002625 push 2Ch 10002627 push edx 10002628 push 80312010h 1000262D call sub_10001400 10002632 mov eax, [esp+0Ch+var_8] 10002636 test eax, eax 10002638 jz short loc_10002676 1000263A mov ecx, [esp+0Ch+arg_12] 1000263E mov edx, dword_1000DFD4 10002644 lea eax, [esp+0Ch+var_4] 10002648 push 0 1000264A push eax 1000264B push 10h 1000264D push ecx 1000264E mov ecx, dword_1000DFDC 10002654 add esi, edx 10002656 push 2Ch 10002658 push esi 10002659 push 80312014h 1000265E call sub_10001400 10002663 neg eax 10002665 sbb eax, eax 10002667 pop esi 10002668 and eax, 60000000h 1000266D add eax, 0A0000000h 10002672 add esp, 8 10002675 retn 10002676 10002676 loc_10002676: 10002676 mov eax, 0A0000204h 1000267B pop esi 1000267C add esp, 8 1000267F retn 1000267F 1000267F ProcAPI_GetThermalMonitorStatus endp =================================================== Ottimo, anche questa funzione è abbastanza lunga, vediamo che accetta 3 parametri, ma dobbiamo ancora scoprire sia che funzione hanno sia il loro tipo. Il codice non è complicato in se, ma va afferrata la logica: 100025B0 sub esp, 8 100025B3 mov ecx, dword_1000DFD0 100025B9 mov [esp+8+var_4], 0 100025C1 mov [esp+8+var_8], 0 100025C9 push esi 100025CA mov eax, [ecx] 100025CC test eax, eax 100025CE jnz short loc_100025DA 100025D0 mov eax, 0A0000206h 100025D5 pop esi 100025D6 add esp, 8 100025D9 retn Questa prima parte si occupa dell'inizializzazione delle variabili locali. In più effettua il controllo se esiste l'ID della nostra sessione, contenuto in pdwUnknown_Global->dwID, se l'id esiste (dwID != 0) continua con l'esecuzione, se è 0 ritorna l'errore 0x0A0000206. 100025DA loc_100025DA: 100025DA cmp [esp+0Ch+arg_0], eax 100025DE jz short loc_100025EA 100025E0 mov eax, 0A0000205h 100025E5 pop esi 100025E6 add esp, 8 100025E9 retn Qui invece controlla che l'id coincida con quello salvato alla chiamata di ProcAPI_AcquireThermalAPI, se non coincide ritorna 0x0A0000205. 100025EA loc_100025EA: 100025EA mov eax, [esp+0Ch+arg_4] 100025EE mov edx, [ecx+8] 100025F1 cmp eax, edx 100025F3 jb short loc_100025FF 100025F5 mov eax, 0A0000201h 100025FA pop esi 100025FB add esp, 8 100025FE retn L'ultimo controllo è sul secondo parametro, se è un numero di processore valido prosegue nell'esecuzione del codice, se invece supera il numero di processori installati nel sistema ritorna l'errore 0x0A0000201. Dopo i dovuti controlli sulla validità degli argomenti arriviamo al codice vero e proprio della funzione. Possiamo già affermare con certezza che il primo parametro è l'ID della sessione e il secondo è il numero del processore. Ma non possiamo dire ancora niente sul terzo parametro, quindi cerchiamo di scoprire di più: 100025FF lea ecx, [eax+eax*4] 10002602 lea edx, [esp+0Ch+var_4] 10002606 push 0 10002608 push edx 10002609 lea esi, [eax+ecx*2] 1000260C mov ecx, dword_1000DFD4 10002612 shl esi, 2 10002615 lea eax, [esp+14h+var_8] 10002619 push 4 1000261B push eax 1000261C lea edx, [esi+ecx] 1000261F mov ecx, dword_1000DFDC 10002625 push 2Ch 10002627 push edx 10002628 push 80312010h 1000262D call sub_10001400 10002632 mov eax, [esp+0Ch+var_8] 10002636 test eax, eax 10002638 jz short loc_10002676 Il primo blocco di istruzioni prepara gli argomenti per la chiamata a DeviceIoControl (sub_10001400 -> Wrapper per DeviceIoControl), se la funzione ha successo prosegue con l'esecuzione altrimenti ritorna l'errore 0x0A0000204: 10002676 loc_10002676: 10002676 mov eax, 0A0000204h 1000267B pop esi 1000267C add esp, 8 1000267F retn Adesso un'altro blocco analogo prepara una seconda chiamata a DeviceIoControl: 1000263A mov ecx, [esp+0Ch+arg_12] 1000263E mov edx, dword_1000DFD4 10002644 lea eax, [esp+0Ch+var_4] 10002648 push 0 1000264A push eax 1000264B push 10h 1000264D push ecx 1000264E mov ecx, dword_1000DFDC 10002654 add esi, edx 10002656 push 2Ch 10002658 push esi 10002659 push 80312014h 1000265E call sub_10001400 10002663 neg eax 10002665 sbb eax, eax 10002667 pop esi 10002668 and eax, 60000000h 1000266D add eax, 0A0000000h 10002672 add esp, 8 10002675 retn In ecx viene salvato il return buffer di lunghezza 16 della DeviceIoControl, questo valore viene direttamente restituito in arg_12, ovvero il nostro terzo argomento! Abbiamo quindi capito che il return buffer della seconda DeviceIoControl ce lo ritroviamo come valore di ritorno, ma come interpretiamo questi 16 bytes? Non possiamo saperlo osservando solo il codice della funzione, dobbiamo vedere come tat.exe interpreta questo buffer. Ecco un estratto del codice che processa il valore di ritorno: [da tat.exe] 00402B86 loc_402B86: 00402B86 mov al, byte ptr [esp+18h+var_10+1] 00402B8A test al, al 00402B8C jz short loc_402B94 00402B8E mov dword ptr [edi], 1 In var_10 c'è il buffer di ritorno. Si testa il secondo byte ritornato: se è 0 il Thermal Monitor è in Idle, se 1 è Active. Ma cos'è il Thermal Monitor? E' una tecnologia implementata in tutti i processori recenti prodotti dall'Intel e ha lo scopo di raffreddare il processore in quelle situazioni in cui la temperatura sale oltre una determinata soglia. Con i processori dal moltiplicatore sbloccato verso il basso il Thermal Monitor downclocca il processore per diminuire il calore prodotto a scapito di un calo nelle prestazioni. --- Riassunto: Ecco qui una quick reference delle funzioni presenti nella ProcAPI.dll: API REFERENCE: ProcApi.dll DWORD ProcAPI_AcquireThermalAPI(DWORD dwPar); dwPar: Session ID RET: 0x0A0000205 -> Initialized yet RET: 0 -> OK DWORD ProcAPI_GetProcessorCount(PDWORD pdwNum); pdwNum: Pointer to the variable that is to receive the processor count RET 0 -> OK DWORD ProcAPI_GetProcessorFrequency(DWORD dwProc, PDWORD pdwFreq); dwProc: Processor Number (it needs to be less than ProcessorCount) pdwFreq: Pointer to the variable that is to receive the frequency of the specified processor RET: 0x0A0000201 -> Bad Proc param ( > iCount) RET: 0x0A0000000 -> API Failed RET: 0 -> OK DWORD ProcAPI_GetThermalMonitorStatus(DWORD dwPar, DWORD dwProc, PCHAR pBuff); dwPar: Session ID dwProc: Processor Number (it needs to be less than ProcessorCount) pBuff: 16 bytes long PHAR buffer (bit 2 is bMonitorStatus) RET: 0x0A0000206 -> Not Initialized RET: 0x0A0000204 -> API Failed RET: 0x0A0000201 -> Bad Proc param ( > iCount) RET: 0x0A0000205 -> Bad ID RET: 0 -> OK DWORD ProcAPI_ReleaseThermalAPI(DWORD dwPar); dwPar: Session ID RET: 0x0A0000206 -> Not Initialized RET: 0x0A0000205 -> Bad ID RET: 0 -> OK --- Conclusioni: Abbiamo visto come documentare una semplice libreria di codice. Potevamo spingerci anche oltre e capire il funzionamento interno della dll andando ad analizzare le interazioni con il suo device driver tat.sys... le potenzialità del reverse engineering sono immense! Spero di aver aperto una piccola finestrella su questo mondo magnifico, ma tanto piccolo da sfuggire alla nostra vista.. by HeDo
Programmatore in C/C++, BASIC, VB6, VB.NET, C#, ASP.NET 4.0 (EF + WF + WCF), Java, Javascript, Pascal, Python, Delphi, HTML, CSS, PHP4/5, SQL, T-SQL, XML, XSD e XSLT. Esperto di sistemi di protezione del software, reversing e antireversing (IA-32)
|
Aggiungi un commento