Archivio storico · Reverse engineering · Spiegazione dettagliata

Lezione 8: il debug in Windows

Come lavorava un debugger sotto Windows 3.x e Windows 95, quali strumenti circolavano e che cosa insegna un controllo di registrazione basato su una semplice moltiplicazione.

In breve

L'ottava lezione è la prima parte dedicata a Windows. Spiega come lavorano i debugger per Windows, dalle funzioni non documentate di Windows 3.x al meccanismo ufficiale di Windows 95, elenca gli strumenti dell'epoca e termina con un esempio: un programma di cifratura che controllava un numero di registrazione con una semplice relazione aritmetica.

La lezione 7 non è disponibile e questa pagina riguarda solo il testo ricevuto. Il documento rimanda alle parti 2 e 3 della stessa lezione, che non ho visto. Non riporto la modifica che il testo indica per aggirare il controllo di registrazione.

1. Nota iniziale e formato dei file

L'autore avverte che le lezioni su Windows sono «non rifinite»: lavorava già su programmi per Windows 95 e correggeva al volo informazioni su Windows 3.1 e NT. Il motto latino significa che «gli uomini, mentre insegnano, imparano».

Secondo il testo, il formato NE degli eseguibili di Windows 3.x offrirebbe «l'equivalente di una tabella dei simboli di debug». È un'esagerazione: in generale quel formato rende leggibili i nomi o i numeri d'ordine delle funzioni esportate e importate, ma non contiene tutti i simboli che servono a un debugger, come variabili locali e righe di sorgente. Non ho verificato la specifica del formato NE per questa pagina.

2. Il debug «non documentato»

La parte centrale accusa Microsoft di usare funzioni non documentate per danneggiare i concorrenti, citando la lotta contro Borland, e di voler trasformare Windows in una «scatola nera». Sono accuse e opinioni dell'autore: il documento non porta prove, e la pagina non le presenta come fatti. Lo stesso vale per il lungo passaggio politico su governi, lobby e televisione.

La parte tecnica è più utile. Il testo dice che i debugger per Windows 3.x passavano da una libreria con funzioni non documentate, chiamata WINDEBUG.DLL, ribattezzata CVWIN.DLL e TDWIN.DLL in Windows 3.1. La sua funzione principale riceveva comandi come avvio, passo singolo, lettura e scrittura di registri e memoria, e restituiva il controllo al termine del comando, a un breakpoint o al caricamento di una DLL. L'autore la collega a una funzione di OS/2 1.x e, a sua volta, a ptrace() di Unix: è una genealogia plausibile, ma non l'ho verificata.

Un dettaglio corretto in linea di principio: poiché i debugger collegavano la funzione solo mentre erano in esecuzione, tramite GetProcAddress(), la sua presenza non compariva nella mappa delle importazioni del programma.

Interruzioni sotto Windows

Il testo ricorda che gestire interruzioni ed eccezioni in Windows 3.x era delicato. Nelle due modalità del sistema, standard e avanzata, le informazioni sullo stack erano diverse; alcuni gestori venivano installati con servizi DOS, altri con i servizi DPMI. La libreria TOOLHELP «cucina» gli eventi e li passa al programma in forma più semplice. Il testo aggiunge che Windows usava le eccezioni di protezione generale come scorciatoia per passaggi di livello altrimenti non consentiti, e la memoria virtuale della modalità avanzata si basava sugli errori di pagina.

3. Gli strumenti dell'epoca

CategoriaA che cosa servivanoEsempi citati
DebuggerFermare il programma, osservare registri e memoriaCodeView e Turbo Debugger per Windows, Windows Codeback, WDEB386, SoftICE per Windows
Ispettori post mortemAnalizzare un programma dopo un arresto anomaloDr. Watson, Coroner, Winspector di Borland
Ispettori di messaggiMostrare i messaggi che Windows invia alle finestreMS-Spy, WinSight, Voyeur e altri strumenti di MicroQuill
SnooperMostrare informazioni su moduli, finestre e risorseInfSpy, SuperSpy, WinView, TPWSpy

Il giudizio che SoftICE sia il migliore «di gran lunga» è una preferenza dell'autore. L'indicazione che Winspector possa creare un file di simboli partendo da un eseguibile senza informazioni di debug è presentata come funzione specifica di quello strumento: non l'ho confrontata con la documentazione.

4. Come funziona un debugger in Windows 95

Questa sezione è la più solida. Descrive il debugger come un programma che ne avvia un altro e poi lo controlla attraverso eventi.

  1. Il debugger crea il processo chiedendo che venga eseguito sotto debug.
  2. Entra in un ciclo e chiama WaitForDebugEvent, che lo blocca finché non succede qualcosa nel programma osservato. La documentazione Microsoft descrive proprio questa funzione come inizio del ciclo principale.
  3. Quando l'evento arriva, la struttura restituita indica che cosa è successo e dove.
  4. Il debugger legge lo stato dei registri con GetThreadContext e può leggere o scrivere la memoria del programma.
  5. Per riprendere, ripristina lo stato con SetThreadContext e chiama ContinueDebugEvent, poi torna all'inizio del ciclo.

Breakpoint e passo singolo

Per un breakpoint, il debugger salva il primo byte di un'istruzione e lo sostituisce con il byte CC, cioè l'istruzione dedicata ai debugger (interruzione 3). Quando il breakpoint scatta, ripristina il byte originale e porta indietro di uno il puntatore di istruzione, così l'istruzione sostituita viene eseguita davvero. Per il passo singolo imposta un bit nel registro dei flag, che fa generare l'interruzione 1 dopo ogni istruzione. Il testo attribuisce questo meccanismo all'80386, ma il bit di trap esiste già nei processori x86 precedenti.

Questa spiegazione vale anche oggi: la differenza è che la manutenzione di questi dettagli è affidata alle API del sistema, come osserva l'autore. La sua previsione che pochi programmatori sapranno ancora fare reverse engineering è un'opinione, non un fatto dimostrato.

Informazioni di debug nei file

Il testo presenta le sezioni .stab e .stabstr come sezioni particolari dei file Win32. In realtà sono il formato dei simboli usato dalla catena di strumenti GNU di allora, come l'autore dice subito dopo, mentre Borland e Microsoft usavano altri formati. La sezione .stab contiene voci di lunghezza fissa e .stabstr contiene le stringhe a cui quelle voci puntano. Il formato descrive tipi, simboli, indirizzi di funzioni e numeri di riga: serve a un debugger per presentare il programma nel linguaggio originale.

L'autore chiude con osservazioni sul fatto che Windows 95 invochi servizi DOS dal codice a 32 bit e sull'uso di un mutex per proteggere parti del nucleo. Sono dettagli di implementazione che non ho verificato. Dà anche un giudizio severo sulla documentazione di Win32 e invita i dipendenti Microsoft a divulgare documentazione riservata. Non condivido quell'invito, perché è un incoraggiamento a violare obblighi di riservatezza.

5. L'esempio di un programma di cifratura

Il caso è il front-end per Windows di un programma di cifratura, versione 4.1. L'autore descrive un metodo per arrivare al punto del programma che decide se il numero di registrazione è corretto: cerca i due messaggi, «congratulazioni» e «numero non corretto», e risale a chi li usa. È lo stesso schema delle lezioni precedenti, spostato su Windows.

Che cosa fa il controllo

Nel listato il programma prende un valore a 16 bit, lo estende a 32 bit e lo moltiplica per una costante. Poi confronta il risultato con il numero ricevuto dall'utente. Se coincidono restituisce 1; altrimenti restituisce 0.

Il motivo per cui l'autore parla di «vecchia minestra» è chiaro: il controllo ha la stessa struttura di quello dei giochi per DOS. Il programma calcola la risposta attesa partendo da un dato e la confronta con la risposta dell'utente, poi comunica l'esito con un solo valore. Chi progetta software dovrebbe notare due debolezze: la relazione aritmetica sta nel programma distribuito, e l'intero esito dipende da un singolo confronto.

Un indizio nel codice

L'autore sostiene di usare uno strumento che cerca blocchi in cui quattro o più istruzioni consecutive iniziano con il byte 66 e che questo gli basterebbe per individuare più della metà dei controlli di questo tipo in pochi secondi. Il byte 66 è il prefisso che, nel codice a 16 bit, fa usare registri a 32 bit, e compare quindi in sequenze di calcolo come quella descritta. L'affermazione sulla percentuale e sulla velocità non è verificabile.

La parte non riportata

Il testo conclude indicando come cambiare una sola istruzione per superare il controllo. Non riporto né i dettagli né gli indirizzi: indicarli servirebbe a aggirare la registrazione di un programma specifico.

6. Che cosa distinguere

Il finale ripete l'invito a scrivere all'autore, tramite un indirizzo anonimo, per ricevere le lezioni mancanti. Non posso dire se quel canale fosse ancora attivo o sicuro.

Glossario

Debugger
Programma che controlla l'esecuzione di un altro e ne mostra lo stato interno.
Breakpoint
Punto in cui l'esecuzione viene fermata.
Contesto di un thread
Insieme dei registri e dello stato di un flusso di esecuzione.
Simboli di debug
Informazioni che collegano il codice macchina ai nomi del sorgente.
NE e PE
Formati degli eseguibili Windows a 16 bit (NE) e a 32 bit (PE).
Prefisso 66
Byte che cambia la dimensione degli operandi nel codice x86 a 16 bit.

Fonti e verifiche

Il testo è stato incollato dall'utente in un unico blocco e non risulta allegato come file. Per il ciclo di eventi del debugger ho consultato la documentazione Microsoft su il ciclo principale di un debugger e su WaitForDebugEvent. Non ho eseguito programmi né applicato modifiche. La lezione 7 non è stata fornita.

Leggi il documento originale: ORC_8.DOC