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