Zoom Icon

CloneDVD 2.5.0.0

From UIC Archive

CloneDVD 2.5.0.0: Debugging su Windows, Memory Patching, Fooling Asprotect, Protezione a Tempo

Contents


CloneDVD 2.5.0.0
Author: Bender0
Email: Mips-email.png
Website: http://www.bender0.altervista.org/
Date: 01/08/2004 (dd/mm/yyyy)
Level: Quite hard
Language: Italian Flag Italian.gif
Comments:



Introduction

Come da titolo, parliamo del debugging su windows e del memory patching; poi raggiriamo asprotect ;)

Ci troviamo di fronte a una versione trial di CloneDVD, con scadenza a 14 giorni dall'installazione. Anche stavolta crackato per un amico... non ce l'ho neanche il masterizzatore dvd :)

Il programma? CloneDVD è come CloneCD, solo che copia i DVD!


Tools

OllyDbg 1.10 + IsDebuggerPresent plugin
PeID
Ntsd (opzionale)
Microsoft Visual C++ 6.0 (o qualsiasi altro compilatore di qualsiasi linguaggio)
Una buona guida delle API, preferibilmente il pdk (Platform Software Development Kit) di Windows.

Se siete su linea lenta come me, scordatevi di scaricarlo con l'interfaccia che offre il sito, perchè non permette il resume al di fuori del giorno stesso. Come si scarica allora? Scaricate i singoli file da qui:
http://download.microsoft.com/download/platformsdk/sdk/update/win98mexp/en-us/3790.0/
Ecco la lista dei file necessari per avere l'ambiente di sviluppo e la documentazione:

PSDK-x86.cab
PSDK-common.0.0.cab
PSDK-x86.0.0.cab
PSDK-common.1.0.cab
PSDK-common.2.0.cab
PSDK-x86.2.0.cab
PSDK-common.4.0.cab
CoreSDK-x86.cab
CoreSDK-common.0.0.cab
CoreSDK-common.1.0.cab
CoreSDK-common.1.1.cab
CoreSDK-common.1.2.cab
CoreSDK-common.1.3.cab
CoreSDK-common.1.4.cab
CoreSDK-common.2.0.cab
CoreSDK-common.2.1.cab
CoreSDK-common.2.10.cab
CoreSDK-common.2.11.cab
CoreSDK-common.2.12.cab
CoreSDK-x86.2.0.cab
CoreSDK-common.3.0.cab
CoreSDK-x86.3.0.cab
CoreSDK-common.0.0.cab

per un totale di 134mb: tanto, ma ne vale la pena.


Link e Riferimenti

CloneDVD lo trovate qui: http://www.clonedvd.net


Essay

Caricate CloneDVD.EXE nel PeID, e leggete il responso: "ASProtect 1.23 RC4 - 1.3.08.24 -> Alexey Solodovnikov". Buone notizie, non vi pare?
Prima ancora di spiegare che metodo di attacco ho usato per rimuovere la protezione di questo programma è bene fare una bella digressione sul debugging sotto Win, che a quanto pare è la parte fatta meglio dell'intero sistema di API ;)
In effetti scrivere un debugger elementare (ai livelli di ntsd) è quasi una passeggiata: vediamo.

Il Debugging su Windows

  1. Debugging APIs
  2. Creazione\Attaching di\a un Processo
  3. Main Loop e Debug Events
  4. Struttura di un Debugger
  5. Interagire con il Debuggee (Breakpoints, Code Injection e Contexts)

Debugging APIs

Ecco l'elenco delle API (principali) relative al debugging, con breve commento; poi parlerò in dettaglio delle più importanti. NB: con "debuggee" si intende il processo debuggato dal debugger. Che frase oscena...

  • ContinueDebugEvent → In risposta a un evento di debug permette di far ripartire il thread che ha generato tale evento.
  • DebugActiveProcess → Permette di debuggare un processo già in corso.
  • DebugActiveProcessStop → "Libera" il debuggee dal debugger.
  • DbgBreakPoint → Genera un eccezione di breakpoint (int3).
Ecco il codice della funzione:
int 3

ret

ore e ore di coding...
  • DebugBreakProcess → Genera un eccezione di breakpoint (int3) nel processo specificato e di conseguenza passa il controllo al debugger.
  • GetThreadContext → Preleva il context di un thread.
  • IsDebuggerPresent → Chiamata da un processo (e dall'asprotect ;) per sapere se è debuggato.
  • OutputDebugString → Il processo può mandare una stringa al debugger che la mostrerà all'utente. Se il processo non è debuggato questa funzione non ha nessun effetto.
  • ReadProcessMemory → Permette di leggere nell'address space del debuggee.
  • SetThreadContext → Imposta il context di un thread.
  • WaitForDebugEvent → Aspetta che il programma sollevi un'eccezione e da il controllo al debugger.
  • WriteProcessMemory → Permette di scrivere nell'address space del debuggee.

Creazione\Attaching di\a un Processo

Prima di iniziare con il debugging vero e proprio (o con i nostri loschi propositi ;) è necessario segnalare al sistema operativo che il nostro programma deve agire da debugger sul processo target. Si può fare in due modi, sta a voi scegliere il migliore di volta in volta:


  • Creazione del Processo:

Permette di debuggare il processo target dalla prima all'ultima istruzione, "creandolo" alle dipendenze del nostro debugger. Lo svantaggio è che il debugger non può terminare la sua esecuzione senza che si chiuda anche il debuggee. Per creare un processo si usa la API CreateProcess, che non ho citato prima perchè non è propriamente una API di debug. Ecco la sintassi (questa volta spiego in breve i parametri): BOOL CreateProcess(

 LPCTSTR lpApplicationName,
 LPTSTR lpCommandLine,
 LPSECURITY_ATTRIBUTES lpProcessAttributes,
 LPSECURITY_ATTRIBUTES lpThreadAttributes,
 BOOL bInheritHandles,
 DWORD dwCreationFlags,
 LPVOID lpEnvironment,
 LPCTSTR lpCurrentDirectory,
 LPSTARTUPINFO lpStartupInfo,
 LPPROCESS_INFORMATION lpProcessInformation

);

lpApplicationName
[in] Stringa che specifica percorso e nome file del target.
lpCommandLine
[in, out] Un NULL andrà benissimo, windows usa il parametro precedente per individuare il file.
lpProcessAttributes
[in] Un NULL andrà benissimo, il processo avrà un descrittore di sicurezza standard.
lpThreadAttributes
[in] Un NULL andrà benissimo, il thread principale avrà un descrittore di sicurezza standard.
bInheritHandles
[in] Decide se ereditare le handle del debuggee per usarle nel debugger; male non fanno, passiamo true.
dwCreationFlags
[in] Specificando i valori DEBUG_PROCESS o DEBUG_ONLY_THIS_PROCESS (non debugga i processi creati dal debuggee), il nostro processo diventerà un debugger.
lpEnvironment
[in] Un NULL andrà benissimo.
lpCurrentDirectory
[in] Stringa che specifica la cartella di lavoro del debuggee. Se il debugger è nella stessa cartella passando NULL il debuggee userà la sua cartella.
lpStartupInfo
[in] Puntatore a una struttura STARTUPINFO che specifica dei parametri di avvio (vedi sotto).
lpProcessInformation
[out] Puntatore a una struttura PROCESS_INFORMATION che riceve dei parametri di identificazione del novo processo (vedi sotto).

CreateProcess ritorna zero se fallisce e nonzero altrimenti.

Questa API si serve di due strutture, STARTUPINFO e PROCESS_INFORMATION.

La prima è inutile ai nostri scopi e conviene passarla azzerata in questo modo: STARTUPINFO si; ZeroMemory(&si,sizeof(si)); si.cb = sizeof(si);

La seconda invece è molto importante; ecco come è definita: typedef struct _PROCESS_INFORMATION {

 HANDLE hProcess;
 HANDLE hThread;
 DWORD dwProcessId;
 DWORD dwThreadId;

} PROCESS_INFORMATION; Come vedete riceve handles e ids del debugee e del suo main thread, che sono indispensabili per usare le altre API di debug.

Ecco quindi come si può creare un processo: STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) );

int ret = CreateProcess("nome del file qui",NULL,NULL,NULL,true,DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);


  • Attaching:

In ogni momento si può iniziare a debuggare un processo. Con EnumProcesses si possono ottenere gli id di tutti i processi attivi; scelto il processo a cui "attaccarsi" si passa il suo id a DebugActiveProcess. Terminata la sessione di debug si usa DebugActiveProcessStop per "liberare" il processo target.

Main Loop e Debug Events

I debugger in user-mode (vedi ollydbg :) si basano su un loop di WaitForDebugEvent e ContinueDebugEvent. Ecco la sintassi delle funzioni, presa dalla documentazione del pdk (e tradotta): BOOL WaitForDebugEvent(

 LPDEBUG_EVENT lpDebugEvent,
 DWORD dwMilliseconds

);

lpDebugEvent
[out] Puntatore a una struttura DEBUG_EVENT che riceve informazioni sull'evento di debug.
dwMilliseconds
[in] Tempo di attesa per un evento di debug in millisecondi. Se questo parametro è zero, la funzione controlla se c'è un evento di debug e ritorna immediatamente. Se il parametro è INFINITE (-1), la funzione non ritorna finchè non avviene un evento di debug. BOOL ContinueDebugEvent(

 DWORD dwProcessId,
 DWORD dwThreadId,
 DWORD dwContinueStatus

);

dwProcessId
[in] Id del processo da continuare.
dwThreadId
[in] Id del thread da continuare. La combinazione di id del processo e id del thread deve identificare un thread che ha provocato un evento di debug in precedenza.
dwContinueStatus
[in] Opzioni per continuare il thread che ha provocato un evento di debug.
Le flag specificabili per questo parametro sono due:
DBG_CONTINUE termina l'exception handling (assumendo che sia stato compiuto dal debugger) e continua il thread.
DBG_EXCEPTION_NOT_HANDLED procede con l'exception handling del processo e continua il thread.

Detto questo, il main loop di un debugger può apparire così: // questo è pseudo-codice // crea il processo (come mostrato al punto 2)

DEBUG_EVENT DebugEv; DWORD dwContinueStatus;

for(;;) {

 dwContinueStatus = DBG_CONTINUE;
 WaitForDebugEvent(&DebugEv, INFINITE);
 // rispondi agli eventi di debug (come mostrerò al punto 4)
 ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);

}

Ora la domanda è: "cos'è un evento di debug?"
In un certo senso è il modo in cui il debuggee comunica (spesso a sua insaputa) con il debugger. Tutte le eccezioni sollevate dal debugee passano al debugger sotto forma di eventi di debug. Lo stesso fanno eventi come la creazione e la distruzione di un processo. Anche i breakpoints (che sono comunque un tipo di eccezione) subiscono lo stesso processo.

Il sistema da parte sua si occupa di raccogliere le eccezioni e gli altri eventi e di passarli per prima cosa al debugger (se ce n'è uno...); se il debugger afferma di aver risolto il problema (nel caso di un'eccezione), il processo continua; se il debugger non risolve il problema l'eccezione viene passata al seh del processo. Se neanche lì l'eccezione viene risolta il sistema procede con la chiusura del processo.

Il debugger riceve questi eventi dal sistema chiamando WaitForDebugEvent; il thread che ha generato l'evento si ferma automaticamente e il debugger ha campo libero e accesso completo sul debuggee per risolvere il problema o rispondere ad altri eventi.

Il sistema passa l'evento al debugger attraverso una struttura complessa, ovvero DEBUG_EVENT, così definita: typedef struct _DEBUG_EVENT {

 DWORD dwDebugEventCode;
 DWORD dwProcessId;
 DWORD dwThreadId;
 union {
   EXCEPTION_DEBUG_INFO Exception;
   CREATE_THREAD_DEBUG_INFO CreateThread;
   CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
   EXIT_THREAD_DEBUG_INFO ExitThread;
   EXIT_PROCESS_DEBUG_INFO ExitProcess;
   LOAD_DLL_DEBUG_INFO LoadDll;
   UNLOAD_DLL_DEBUG_INFO UnloadDll;
   OUTPUT_DEBUG_STRING_INFO DebugString;
   RIP_INFO RipInfo;
 } u;

} DEBUG_EVENT, *LPDEBUG_EVENT;

dwProcessId e dwThreadId ci danno rispettivamente l'id del processo e l'id del thread dove è avvenuto l'evento. Il membro dwDebugEventCode ci informa del tipo di evento segnalato; può assumere i seguenti valori:

EXCEPTION_DEBUG_EVENT → eccezione
CREATE_THREAD_DEBUG_EVENT → creazione di un thread
CREATE_PROCESS_DEBUG_EVENT → creazione di un processo
EXIT_THREAD_DEBUG_EVENT → distruzione di un thread
EXIT_PROCESS_DEBUG_EVENT → distruzione di un processo
LOAD_DLL_DEBUG_EVENT → caricamento di una dll
UNLOAD_DLL_DEBUG_EVENT → liberazione di una dll
OUTPUT_DEBUG_STRING_EVENT → stringa di debug
RIP_EVENT → system debugging error

Il membro u e' un unione; quindi i suoi membri occupano tutti la stessa porzione di memoria. Per scegliere il membro giusto dobbiamo basarci sul valore di dwDebugEventCode.

Ora, per un debugger che può servire ai nostri scopi è sufficiente rispondere a due eventi: EXIT_PROCESS_DEBUG_EVENT (per terminarsi quando termina il debuggee) e soprattutto EXCEPTION_DEBUG_EVENT. Il membro di u relativo a questo evento è Exeption, che punta a una struttura di tipo EXCEPTION_DEBUG_INFO, definita così: typedef struct _EXCEPTION_DEBUG_INFO {

 EXCEPTION_RECORD ExceptionRecord;
 DWORD dwFirstChance;

} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

Il membro che ci interessa è ExceptionRecord, che punta a una struttura EXCEPTION_RECORD, che contiene informazioni essenziali sull'eccezione, definita così: typedef struct _EXCEPTION_RECORD {

 DWORD ExceptionCode;
 DWORD ExceptionFlags;
 struct _EXCEPTION_RECORD* ExceptionRecord;
 PVOID ExceptionAddress;
 DWORD NumberParameters;
 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

Il membro ExceptionCode indica il tipo di eccezione. Assume valori interessanti come EXCEPTION_ACCESS_VIOLATION, EXCEPTION_BREAKPOINT, EXCEPTION_INT_DIVIDE_BY_ZERO, EXCEPTION_SINGLE_STEP, EXCEPTION_STACK_OVERFLOW e via dicendo.

Il membro ExceptionFlags indica con uno zero che l'eccezione è continuabile e con EXCEPTION_NONCONTINUABLE il contrario.

Il membro ExceptionAddress indica l'indirizzo dove è avvenuta l'eccezione nell'address space del debuggee.

Struttura di un Debugger

Per completare il nostro debugger general purpose, dobbiamo rispondere agli eventi di debug. Ecco una possibile implementazione: // questo è pseudo-codice switch (DebugEv.dwDebugEventCode) {

 case EXCEPTION_DEBUG_EVENT:
 if (DebugEv.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT)
 {
   // rispondi al breakpoint
 }
 else
 {
   // rispondi a un'eccezione
 }
 break;
 case EXIT_PROCESS_DEBUG_EVENT:
   // chiama ContinueDebugEvent per non bloccare il debuggee e esci.
 break;
 }

Interagire con il Debuggee (Breakpoints, Code Injection e Contexts)

Sottotitolo: Come far diventare il debuggee una marionetta nelle nostre mani.

  • Breakpoints

Sappiamo già che un breakpoint è un'eccezione posta appositamente in un certo punto dell'esecuzione del target per dare il controllo al debugger. Nel concreto l'eccezione in questione viene sollevata quando viene eseguita l'istruzione int 3, il cui opcode è un singolo byte, ovvero CCh.

Se solo potessimo sovrascrivere il primo byte di un'istruzione del debuggee con un CCh potremmo quindi breakkare dove vogliamo nel target, e nel punto giusto avremmo molte possibilità di azione. Ovviamente possiamo ;)

Per scrivere nell'address space del debuggee il sistema di debugging del buon vecchio win ci propone la API WriteProcessMemory, che funziona più o meno come WriteFile. Eccone la sintassi: BOOL WriteProcessMemory(

 HANDLE hProcess,
 LPVOID lpBaseAddress,
 LPCVOID lpBuffer,
 SIZE_T nSize,
 SIZE_T* lpNumberOfBytesWritten

);

hProcess
[in] Handle del processo di cui vogliamo sovrascrivere la memoria.
lpBaseAddress
[in] Puntatore al blocco di memoria da sovrascrivere.
lpBuffer
[in] Puntatore al buffer che contiene i byte che verranno scritti nella memoria del target.
nSize
[in] Numero di bytes da scrivere.
lpNumberOfBytesWritten
[out] Puntatore che riceve il numero di bytes effettivamente scritti. Diversamente da WriteFile se non vi interessa sapere quanti byte sono stati scritti potete passare NULL.

Quindi se vogliamo settare un breakpoint all'indirizzo 0040123c, chiameremo WriteProcessMemory così: BYTE int3byte = 0xCC; DWORD addr = 0x0040123c; WriteProcessMemory(hProc,(void*)addr,&int3byte,1,NULL);

Non preoccupatevi del valore di ritorno; anche se la funzione fallisce (ritornando 0) probabilmente l'errore è del tipo "la richiesta non è stata completata del tutto", ma in quel caso il(i) byte(s) vengono scritti ugualmente.

Ovviamente quando il debugger risponderà all'eccezione dovrà sistemare il tutto prima di far ripartire il processo; questo include rispristinare il byte originale e risistemare il registro eip (per la modifica dei registri vedi contexts).

  • Code Injection

La code injection quando si hanno privilegi da debugger è semplicissima. Reversatevi il target e segnatevi gli offset dei byte da modificare; chiamate poi WriteProcessMemory tante volte quante sono le modifiche da effettuare, passandogli di volta in volta l'indirizzo, il puntatore al buffer e la grandezza di quest'ultimo appropriati. Il procedimento è analogo a quello per settare breakpoints appena spiegato.

  • Contexts

Questa è la parte più ostica ma anche la più interessante (imo) del discorso.

Domanda: "cos'è un thread context?"
Sappiamo che windows e un sistema operativo che supporta il multitasking e il multithreading. Ogni processo ha il suo address space e i suoi threads; i diversi threads e i diversi processi vengono spesso eseguiti in contemporanea, o almeno questo è quello che accade in apparenza. In realtà il processore non può eseguire più istruzioni di diversi thread simultaneamente. Inoltre, ogni thread ha i suoi registri che devono essere preservati.

Il modo in cui windows amministra l'esecuzione dei thread è detto "scheduling". I thread attivi compaiono in una lista e vengono detti "ready threads", a significare che sono pronti a essere eseguiti in ogni momento. Lo scheduling consiste nel passare al processore questo o quest'altro thread per l'esecuzione, a seconda della loro priorità; questi cambi vengono detti "thread switch". Ma come ho già detto ogni thread ha i registri da preservare; infatti quando si effettua un thread switch queste informazioni vengono salvate in una struttura per poi essere rispristinate quando riprende l'esecuzione di quel particolare thread. Questa struttura è la struttura CONTEXT.

Ora fai un respiro profondo e rileggi questa parte ;)

La definizione della struttura CONTEXT varia a seconda dell'architettura del processore ed è quindi hardware-dependent. La seguente definizione si applica ai processori x86: typedef struct _CONTEXT {

 // flags, le spiego più avanti.
 DWORD ContextFlags;
 // registri di debug.
 DWORD Dr0;
 DWORD Dr1;
 DWORD Dr2;
 DWORD Dr3;
 DWORD Dr6;
 DWORD Dr7;
 // punta a una struttura con parametri che riguardano la fpu.
 FLOATING_SAVE_AREA FloatSave;
 // segment registers.
 DWORD SegCs;
 DWORD SegGs;
 DWORD SegFs;
 DWORD SegEs;
 DWORD SegSs;
 DWORD SegDs;
 // general-purpose registers, eip e le flags.
 DWORD Edi;
 DWORD Esi;
 DWORD Ebx;
 DWORD Edx;
 DWORD Ecx;
 DWORD Eax;
 DWORD Ebp;
 DWORD Esp;
 DWORD EFlags;
 DWORD Eip;
 // extended registers.
 BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

Per modificare il context di un thread (e quindi le sue flags e i suoi registri, eip incluso), win ci fornisce due API: GetThreadContext e SetThreadContext (che fantasia alla Microsoft!). Ecco le sintassi: BOOL GetThreadContext(

 HANDLE hThread,
 LPCONTEXT lpContext

);

hThread
[in] Handle del thread da cui prelevare il context.
lpContext
[in, out] Puntatore a una struttura CONTEXT che riceve il context del thread. BOOL SetThreadContext(

 HANDLE hThread,
 const CONTEXT* lpContext

);

hThread
[in] Handle del thread di cui si vuole modificare il context.
lpContext
[in] Puntatore a una struttura CONTEXT che contiene il nuovo context del thread.

Prima di prelevare o impostare il context bisogna settare il membro ContextFlags della struttura CONTEXT. Tale membro specifica quali valori del context devono essere prelevati\impostati. Può avere diversi valori, ma per andare sul sicuro conviene settare CONTEXT_ALL. Se avete degli headers vecchi è possibile che manchi questa costante; in tal caso passate direttamente il suo valore, 0x1003F.

Con questo procedimento potete modificare a piacimento i registri di un thread. La possibilità di modificare anche i registri di debug del target ci permette di settare degli hardware breakpoints. Infatti se scriviamo degli indirizzi nei primi quattro registri di debug (Dr0 .. Dr3), se e quando verranno eseguite delle istruzioni a quegli indirizzi verrà generata un'eccezione di breakpoint che verrà ovviamente passata al debugger.

Attacchiamo CloneDVD

Bene, finita questa (enorme!) digressione, possiamo cominciare a studiare il target, CloneDVD.EXE, che come abbiamo visto prima è packato con asprotect.

Il mio obiettivo non è ottenere un dump funzionante; se vi interessa unpackare asprotect leggetevi il tutorial di AndreaGeddon (bel tute ;).

Lo carichiamo nel debugger (io ho usato ntsd per il test, ma qualsiasi debugger fungerà allo scopo). Eseguiamo CloneDVD... dopo esserci sorbiti le numerose eccezioni generate da asprotect per disturbarci, ecco il nag che ci informa che non dovremmo usare il debugger. Sembra una messagebox... proviamo a breakkare su MessageBoxA. Bingo. Steppiamo fino al ret e poi steppiamoci sopra, per vedere da dove viene chiamata questa messagebox. Niente da fare: prima di chiamare MessageBox il return address della call viene modificato, e il risultato è che ci troviamo su ExitProcess.

Questo è il momento di usare un debugger un pò più serio ;) Carichiamo il target in Olly, mettiamo un bp su IsDebuggerPresent e eseguiamo con f9.

NB: anche se di solito quando si lavora con i packer è meglio usare gli hardware breakpoints per non modificare l'area di memoria eseguibile, asprotect spazza via i vostri hw breakpoints ogni volta che genera un eccezione, quindi usiamo (per quanto possibile) i breakpoint "tradizionali".

Bene, breakka, e siamo @00A1169C (quindi il check del debugger lo fa asprotect, non CloneDVD). Riavviamo e attiviamo il plugin IsDebuggerPresent che rende il nostro debugger invisibile a questa funzione, poi eseguiamo. Funziona e il programma parte, ma dato che siamo ancora entro i limiti del trial period non vediamo il nag che ci dice che il prog è expired - quindi tiriamo avanti l'orologio di windows di un anno (anche meno se volete ;) e lo rilanciamo.

Ecco un'altra messagebox, breakkiamo anche su questa, la chiamata è fatta @00403342. Guardiamoci intorno: qualche chiamata interna, un pò di salti condizionali, la messagebox di prima... dopo sembra continuare l'esecuzione normalmente, anche perchè chiama CreateDVDCloner, l'unico export della dll che lo accompagna. Vediamo cosa succede se non viene eseguito quel salto @00403365. Steppiamo fino al salto, poi click destro sull'istruzione successiva, "new origin here". così abbiamo cambiato eip, come se quel salto non ci fosse mai stato. Eseguiamo e, come previsto, il programma parte anche se è scaduto il trial period. Segnatevi l'offset del salto da eliminare e chiudete il debugger. Fine della parte noiosa.

Ora viene il bello: studiamo l'attacco. L'idea è di creare un loader che si comporti da debugger su CloneDVD, che ignori completamente asprotect e che patchi la memoria per togliere quel jmp. Ecco che ci si pongono davanti diversi problemi:

  1. Ignorare asprotect non è immediato dato che genera parecchie eccezioni.
  2. Abbiamo visto che asprotect cerca il debugger, dobbiamo nasconderci.
  3. Dobbiamo patchare la memoria al momento giusto, ovvero DOPO essere stata decriptata e PRIMA di essere eseguita.

Risolviamoli, nell'ordine :)

1) È vero che asprotect genera molte eccezioni ma è anche vero che il programma non crasha se lanciato normalmente. Questo perchè le eccezioni vengono riparate dal seh del processo. L'unica cosa da fare è dire al sistema che noi (il debugger) non siamo riusciti a riparare l'eccezione, così ci penserà il processo.

Per far questo, è sufficiente chiamare ContinueDebugEvent così: ContinueDebugEvent(

 DebugEv.dwProcessId,         // id del processo
 DebugEv.dwThreadId,          // id del thread
 DBG_EXCEPTION_NOT_HANDLED    // passa l'exception handling al processo

); 2) Asprotect cerca il debugger con IsDebuggerPresent. Prima per evitare questo check con Olly abbiamo usato quel plugin. Come funziona il plugin? Come fa IsDebuggerPresent a capire se c'è un debugger o meno? Andiamo a reversarci questa API.

Avete l'ultima versione di OllyDbg? Dalla 1.10 si possono caricare dll a sè stanti e chiamare gli exports "fingendo" dei parametri. IsDebuggerPresent non ha parametri, comunque possiamo caricare kernel32.dll (che esporta questa API) in Olly e guardarcela. Andate su debug→call dll export, scegliete IsDebuggerPresent e clickate Follow In Disassembler. Ecco IsDebuggerPresent in tutto il suo splendore: MOV EAX,DWORD PTR FS:[18] MOV EAX,DWORD PTR DS:[EAX+30] MOVZX EAX,BYTE PTR DS:[EAX+2] RET Ben tre mov, anche qui ore e ore di coding... ad ogni modo vediamo che con le prime due istruzioni si calcola un'indirizzo, e con la terza muove in eax (con estensione di zero) il byte che si trova 2 byte dopo quell'indirizzo, probabilmente un valore booleano. Quindi pare che a quell'indirizzo venga settato il byte se il processo è debuggato. Allora andiamo a scriverci uno zero noi ;) che è quello che fa anche il plugin. questo è il codice: bool HideDebugger(HANDLE hProc,HANDLE hT) {

 // legge il thread context
 CONTEXT tc;
 tc.ContextFlags = CONTEXT_ALL;
 if(!GetThreadContext(hT,&tc)) return false;
 // calcola l'indirizzo del byte in modo analogo a IsDebuggerPresent
 DWORD addr,buf;
 ReadProcessMemory(hProc,(void*)(tc.SegFs+0x18),&addr,4,NULL);
 ReadProcessMemory(hProc,(void*)(addr+0x30),&addr,4,NULL);
 ReadProcessMemory(hProc,(void*)addr,&buf,4,NULL);
 addr = buf+2;
 // patcha il byte
 BYTE bnull = 0;
 WriteProcessMemory(hProc,(void*)addr,&bnull,1,NULL);
 return true;

} Bene, questo codice ci nasconderà da IsDebuggerPresent, ma quando lo eseguiamo? Dato che il nostro loader creerà il processo con CreateProcess, ho pensato che la scelta migliore fosse patchare il byte al system breakpoint (poco prima di cominciare l'esecuzione di un processo il sistema chiama DbgBreakPoint, che è per certo il primo EXCEPTION_BREAKPOINT che il nostro programma registra, e quindi facilmente individuabile). if (DebugEv.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT) {

 if (exceptioncounter == 0 )
 {
   if (debug) printf("exception (%u) at [%.8X] (DbgBreakPoint)\n",
   exceptioncounter,DebugEv.u.Exception.ExceptionRecord.ExceptionAddress);
   HideDebugger((DWORD)hProc,(DWORD)hT);
 }

} 3) Sono stato a lungo indeciso su questo punto. In realtà la mia idea iniziale era di patchare i jumps precedenti la messagebox, per avere un'esecuzione perfetta. Si creava però il problema del timing: come trovare il momento giusto per patchare la memoria? Il programma non genera eccezioni dopo aver decriptato la memoria, e nessuno mi garantisce che mentre cerco di patcharla le istruzioni non vengano eseguite. Per aggirare l'ostacolo (in modo poco ortodosso...) ho sfruttato il fatto che la messagebox è eseguita in stato modale; avviso l'utente di non chiudere la messagebox, creo un thread che prova a patchare ogni mezzo secondo la memoria (ovviamente non patchandola se è ancora criptata), e una volta patchata avviso l'utente di procedere.

Ecco il mio thread (il resto mi sembra superfluo): DWORD WINAPI PatchWait(HANDLE hProc) {

 patchloop:
 // timeout: 0,5s
 Sleep(500);
 // legge i byte da patchare
 ReadProcessMemory(hProc,(void*)patchaddr,&patchbuffer,patchsize,NULL);
 // li confronta con quelli originali decriptati
 for (unsigned int i=0;i&ltpatchsize;i++)
 {
   if (patchbuffer[i] != origpatchbytes[i]) goto patchloop;
 }
 // se sono uguali, patcha e avvisa l'utente
 WriteProcessMemory(hProc,(void*)patchaddr,&patchbytes,patchsize,NULL);
 printf("wait 2 seconds then click ok to launch CloneDVD");
 return 0;

} Per rendere il loader più generico il thread usa queste variabili globali, facili da reimpostare: DWORD patchaddr; DWORD patchsize; BYTE patchbuffer[patchsize]; BYTE origpatchbytes[patchsize]; BYTE patchbytes[patchsize];

Nel caso di CloneDVD: DWORD patchaddr = 0x00403365; DWORD patchsize = 5; BYTE patchbuffer[5]; // vuoto, è solo un buffer BYTE patchbytes[] = {0x90,0x90,0x90,0x90,0x90}; // qualche nop.. BYTE origpatchbytes[] = {0xE9,0x4A,0x01,0x00,0x00}; // i bytes originali del salto, per il confronto Ecco fatto... fatevi il vostro loader personalizzato (se non ne avete già uno). Questo è un loader generico per asprotect, ma si può fare molto di meglio ;)

Per concludere, pasto qui il sorgente completo del mio loader: #include <windows.h>

  1. include <stdio.h>

// patch info char filename[] = "CloneDVD.EXE"; DWORD patchaddr = 0x00403365; DWORD patchsize = 5; BYTE patchbytes[] = {0x90,0x90,0x90,0x90,0x90}; BYTE origpatchbytes[] = {0xE9,0x4A,0x01,0x00,0x00}; BYTE patchbuffer[5]; // fine info

DWORD WINAPI PatchWait(HANDLE hProc) {

 patchloop:
 // timeout: 0,5s
 Sleep(500);
 // legge i byte da patchare
 ReadProcessMemory(hProc,(void*)patchaddr,&patchbuffer,patchsize,NULL);
 // li confronta con quelli originali decriptati
 for (unsigned int i=0;i&ltpatchsize;i++)
 {
   if (patchbuffer[i] != origpatchbytes[i]) goto patchloop;
 }
 // se sono uguali, patcha e avvisa l'utente
 WriteProcessMemory(hProc,(void*)patchaddr,&patchbytes,patchsize,NULL);
 printf("click ok to launch CloneDVD\n");
 return 0;

}

bool HideDebugger(HANDLE hProc,HANDLE hT) {

 // legge il thread context
 CONTEXT tc;
 tc.ContextFlags = CONTEXT_ALL;
 if(!GetThreadContext(hT,&tc)) return false;
 // calcola l'indirizzo del byte in modo analogo a IsDebuggerPresent
 DWORD addr,buf;
 ReadProcessMemory(hProc,(void*)(tc.SegFs+0x18),&addr,4,NULL);
 ReadProcessMemory(hProc,(void*)(addr+0x30),&addr,4,NULL);
 ReadProcessMemory(hProc,(void*)addr,&buf,4,NULL);
 addr = buf+2;
 // patcha il byte
 BYTE bnull = 0;
 WriteProcessMemory((void*)hProc,(void*)addr,&bnull,1,NULL);
 printf("debugger hidden.\n");
 return true;

}

void main() {

 STARTUPINFO si;
 PROCESS_INFORMATION pi;
 ZeroMemory( &si, sizeof(si) );
 si.cb = sizeof(si);
 ZeroMemory( &pi, sizeof(pi) );
 CreateProcess(
   filename,
   NULL,NULL,NULL,
   false,
   DEBUG_ONLY_THIS_PROCESS,
   NULL,NULL,
   &si,&pi);
 HANDLE hProc = pi.hProcess;
 HANDLE hMainT = pi.hThread;
 DEBUG_EVENT DebugEv;
 DWORD dwContinueStatus;
 int exceptioncounter=0;
 for(;;)
 {
   dwContinueStatus = DBG_CONTINUE;
   WaitForDebugEvent(&DebugEv, INFINITE);
   switch (DebugEv.dwDebugEventCode)
   {
     case EXCEPTION_DEBUG_EVENT:
     if (DebugEv.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT)
     {
       if (exceptioncounter == 0 )
         {
           printf("exception (%u) at [%.8X] (DbgBreakPoint)\n",
           exceptioncounter,DebugEv.u.Exception.ExceptionRecord.ExceptionAddress);
           HideDebugger(hProc,hMainT);
           DWORD id;
           CreateThread(NULL,0,PatchWait,hProc,0,&id);
         }
       else
       {
         printf("brakpoint hit (%u) at [%.8X]\n",
         exceptioncounter,DebugEv.u.Exception.ExceptionRecord.ExceptionAddress);
       }
     }
     else
     {
       if ((DWORD)DebugEv.u.Exception.ExceptionRecord.ExceptionAddress<0x70000000)
       {
         if ((DWORD)DebugEv.u.Exception.ExceptionRecord.ExceptionAddress>0x00A00000)
         {
           printf(
           "exception (%u) at [%.8X] (aspr trap) not handled :)\n",
           exceptioncounter,DebugEv.u.Exception.ExceptionRecord.ExceptionAddress);
           dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
         }
         else
         {
           printf(
           "exception (%u) at [%.8X] not handled\n",
           exceptioncounter,DebugEv.u.Exception.ExceptionRecord.ExceptionAddress);
           dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
         }
       }
       else
       {
         printf("exception (%u) at [%.8X] (system)\n",
         exceptioncounter,DebugEv.u.Exception.ExceptionRecord.ExceptionAddress);
         dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
       }
     }
     exceptioncounter++;
     break;
     case EXIT_PROCESS_DEBUG_EVENT:
       ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
       return;
       break;
   }
 ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
 }

}


Note Finali

Questa volta ho deciso di scrivere un essay di livello più elevato perchè ho trovato un target che mi ha stuzzicato la fantasia; come al solito giocare con i packer è molto divertente ;)
Se poi vi ho anche insegnato qualcosa, sono ben contento :D
Ovviamente per chiarimenti, migliorie o correzioni mandatemi una mail.
Ci vediamo al prossimo essay...bye!

bender0


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.