Home Site map Contacts CZ

Jan Ringoš, Tringi.TRIMCORE.cz

Papers & C++  »  Papers  » 

Obecná schémata ochrany kódu

The content of this page is only available in Czech and as such is displayed below. We apologize for the inconvenience.

Článek se v obecné rovině zabývá nejdůležitějšími tématy nastíněné problematiky; především návrhem a implementací kontroly klíčů, obranou proti útokům a rozpoznáváním a reakcemi na pokusy o napadení programu. Dále zahrnuje několik tipů pro vývoj shareware programů, konkrétní techniky zatemňování kódu, možnosti obrany proti existujícím crackům, a mnoho dalších zajímavostí.

Tento článek byl poprvé publikován v roce 2006 v Magazínu Českých Her [link leads out of this web]

Když už se programátor nebo tým dostane na takovou úroveň, že začne své produkty prodávat, dříve či později se tváří v tvář setká s fenoménem zvaným cracking. O tomto tématu samotném již bylo napsáno mnoho, a určitě se nejedná o nic příjemného, pokud jste na straně poškozených a unikají vám peníze za odvedenou práci. Pokud shareware či zcela komerční software vyvíjíte a ochranami jste se ještě nezačali hlouběji zabývat, pak věřím, že vám tento článek pomůže najít vhodné řešení.

Úvodem

Pokud chceme začít implementovat do svého software ochranné mechanizmy, musíme si uvědomit, že vývoj účinné ochrany je dlouhodobá záležitost. Je třeba celé schéma vyvážit, protože zde více než jinde platí, že nejslabší článek zrazuje. Navíc testování těchto ochran není zrovna jednoduchá záležitost, jelikož my se pohybujeme na úrovni vyšších jazyků (C++, C#, Java), a crackeři na úrovni strojového kódu (byte-kódu či MSIL chcete-li). Co se týče celkové časové náročnosti, pak se musíme zamyslet, zda má vůbec cenu zpracovávat ochranné mechanizmy a jestli by nebylo lepší investovat čas do vylepšení vlastního software.

A nakonec ta nejhorší zpráva. Dokonalý systém ochrany neexistuje, minimálně ne na úrovni software, a to už z principu věci, ať se vám už prodejci takovýchto ochran snaží namluvit cokoliv. My se ale pokusíme život crackerům ztížit jak jen to půjde.

Uživatel

Při vývoji ochranného mechanizmu musíme zohlednit toho nejdůležitějšího a tím je uživatel. Základem je dobře definovaná licenční smlouva. Dnes u velké části software můžeme najít velmi dlouhé a často špatně srozumitelné texty, kterým úplně nerozumí snad ani právníci, natož běžný uživatel, a konkrétní detaily se zde špatně hledají. Tyto smlouvy nejsou špatné, právě naopak, chrání práva a poskytují záruky oběma stranám, avšak mnoho uživatelů je z uvedených důvodů nečte vůbec. Proto se doporučuje vždy zpracovat velmi stručný výtah základních bodů, především co se týče manipulace se softwarem, která se uživateli umožňuje, a která ne. Co ve smlouvě rozhodně být musí, jsou ale postihy, minimálně ve formě zrušení licence, v případě úniku licenčních čísel/klíčů, prokázané analýzy plné verze za účelem prolomení a podobně. Každopádně je třeba zmínit všechny případné důsledky běhu systémů ochrany, jako např. automatické rušení platnosti (třeba v aktualizačních balíčcích) uniklých čísel apod. Pak již nemá smysl strašit vysokými finančními a právními postihy, jelikož tak akorát odradíte uživatele začátečníky, kteří si produkt nekoupí už ze strachu, že by snad náhodou smlouvu porušili, a ti, kteří jej používají nelegálně úmyslně, si z těchto varování stejně nic nedělají.

Klíče

A nyní už přejděme ke konkrétním ideám a technikám. Pokud tedy implementujeme ochranný mechanizmus, potřebujeme kontrolovat nějakou sekvenci, jejíž platnost řekne programu, že může pokračovat, resp. že může povolit placené vlastnosti v případě shareware. Tuto sekvenci budeme nazývat klíčem, ať už se jedná o krátký, uživatelem zapisovaný řetězec, či samostatný souborový klíč. Tento klíč je zpravidla distribuován samostatně konkrétnímu uživateli a vztahuje se na něj požadavek na utajení.

Návrh dobrého klíče ale není triviální záležitost.

  1. Už pro komfort uživatelů je vhodné nesenou informaci převést na snadno zadatelný řetězec. Je vhodné se vyvarovat jakýchkoliv speciálních znaků, případně snadno zaměnitelných (I a jednička, O a nula, apod.). Pro jednoduchost ale může být dobrým nápadem kódování base64, některá z mnoha mutací UUE, či třeba jen hexadecimální notace apod.
  2. Vlastní data ukrytá pod tímto řetězcem musí být určité složitosti. Příliš složitý klíč nebudou mít rádi uživatelé, nebude se jim dařit zadávat jej korektně a díky naší neprozřetelnosti budeme mít plné ruce práce s technickou podporou, zatímco bychom se mohli věnovat vylepšování produktu. Na druhou stranu příliš jednoduchý klíč bude terčem brute-force útoků (brutální síla, metoda postupného zkoušení všech možností či možných kombinací), a bude mnohem snadno odhalitelný, i třeba namátkovými pokusy.
  3. Je velmi důležité udržet poměr platných klíčů mezi všemi možnými permutacemi znaků minimálně pod jednou promilí, nejlépe mnohem níže. Platné klíče také musí splňovat požadavek rovnoměrné distribuce, a to minimálně tak, aby u dvou po sobě jdoucích klíčů, vždy alespoň jeden byl neplatný.

Dalším důležitým prvkem může být vazba na nějaká stabilní data. Těmi pravděpodobně bude jméno uživatele či firmy, která si produkt pořídila. Odvíjí-li se platnost klíče od potřeby zadání korektních uživatelských informací, síla klíče roste, zvláště jedná-li se o citlivé informace, které žádný klient není ochoten zveřejňovat na warez serverech, přičemž bez této informace je klíč bezcenný.

Co do implementace se můžeme rozhodnout mezi reverzibilním, polorevrezibilním a ireverzibilním klíčem. Jaký je zde rozdíl? U reverzibilních klíčů je třeba si připravit generátor platných klíčů a samotný program pak tento klíč kompletně dekóduje k získání potřebných informací. Problém zde je ale ten, že pokud cracker prolomí dekódovací algoritmus, bývá pro něj velmi snadné vyvinout algoritmus ke generování nových klíčů (keygen). Na druhou stranu dekódovací algoritmy často dosáhnou takového stupně složitosti, že je jejich zpětná analýza příliš obtížná. Výhodou poloreverzibilních klíčů je to, že není možné je naprosto dekódovat, pouze kontrolní algoritmus do určité míry zkontroluje jejich platnost, v mnoha ohledech jsou pak podobné reverzibilním. Pod pojmem ireverzibilní klíč si můžeme přestavit klasický jednocestný kontrolní součet nebo hash typu CRC32, MD5, TTH, apod. Je-li použit ireverzibilní klíč, je třeba, na rozdíl od předchozích, známá data znova zakódovat, a porovnat je s klíčem, který dodal uživatel. Algoritmy pro ireverzibilní klíče je snadné a rychlé vyvinout, jejich použití je ale v mnoha směrech omezené, zvláště potřebujeme-li aby klíč nesl i jiné informace, než pouze o své platnosti či neplatnosti.

Také je samozřejmě možné tyto techniky kombinovat. Část klíče může být kontrolována pouze pro platnost, jiná část nese informace pro další ověřování a podobně.

Kapitolou samou pro sebe by byly asymetrické klíče. PGP, DEC, RSA, Blowfish, Twofishes, a mnoho dalších, prolomených či nikoliv, se slabinami již nalezenými či ještě nikoliv. Princip je postaven na existenci privátního a veřejného klíče. Jedním je možné data zakódovat, druhým pak dekódovat či ověřit jejich platnost. Implementace těchto technologií je náročná a mnohdy je třeba si požadovanou technologii licencovat. Často však veškerá námaha vést ochranu tímto směrem vyjde v niveč, jelikož málokdy bude cracker zkoumat a rekonstruovat takto složité kontrolní algoritmy. Tak se děje pouze v případech, že se jedná o triviality, či známý prolomený klíč, …nebo mu jiný způsob nedovolíme.

Návrh interface pro zadávání klíče

Možná budete překvapeni, ale část sil je třeba věnovat také správné implementaci okénka pro zadávání klíče. Konkrétně je třeba se zaměřit na dvě velmi důležité vlastnosti.

  1. Určitě znáte dialogy, kdy zadáváte sériové číslo do několika stejně či různě velkých políček, často mezi nimi kurzor přeskakuje automaticky, často se malá písmenka sama mění na velká a podobně. Jenže to je špatně, přestože to vypadá dobře a komfortně. Každý, kdo zadá náhodnou změť znaků, na první pohled zjistí délku klíče, že klíč neobsahuje malá písmenka, a další informace, které mu následně mohou hodně pomoci při odhalování našeho algoritmu. A my „jim“ přece pomáhat nechceme!
  2. Drobná pauza po zadání klíče. Zavolat po kontrole nějakou tu funkci sleep, či krátkou smyčku se stejným účelem je triviální záležitost, a přitom jsme opět crackerovi znemožnili jeden způsob útoku. Za účelem prolomení klíče jsou často používány prográmky, jimž se přezdívá „makro playery“, které simulují uživatelské zadávání různých klíčů. Jelikož to ale provádí algoritmicky a mnohokrát rychleji než uživatel, mohou brzy přijít na platný klíč jen náhodně či inkrementálně. Tímto zpomalením pak významně zvýšíme čas získání nějakého platného klíče, ne-li znemožníme v případě kvalitního klíče. Podobně je možné po třetím neúspěšném pokusu okno pro zadávání prostě zavřít. Je sice snadné onen „makro player“ instruovat aby jej opět vyvolal, tak jak by to udělal uživatel, ale pokud záměrně použijeme některé méně efektivní způsoby ruční tvorby tohoto okna, zavedeme několik zdržovacích algoritmů apod. pak pokusy o průlom tímto způsobem prakticky znemožníme. I mechanizmus zpoždění je třeba důkladně promyslet, jelikož je více než snadné volání takové funkce v programu vyhledat a vyrušit (přepsat NOPy). Tento problém se ale týká všech částí kontrolního mechanizmu a bude rozebrán později.

Algoritmy kontroly klíče

Nyní se vrhněme na to nejzajímavější a to vlastní kontrolu klíče. Vezměme ji po vrstvách.

První vrstva nad celou kontrolou je převod z řetězce na binární (dekódovatelnou) informaci. Ta by měla být stručná jelikož, rozhodne-li se cracker jít cestou prolomení klíče, tato mu určitě nebude dělat žádné problémy, případně se jí věnovat nebude vůbec. Ale i tak není špatný nápad zapojit do ní jednoduchý kontrolní součet. Tento třeba může posloužit jako první úroveň technik odvádějících pozornost od hlavního kódu, které jsou popsány dále.

Implementace dalších vrstev, které již provádí samotnou kontrolu již zjevně bude promyšlenější. Jaký algoritmus použít vám radit nebudu, ale poradím vám jak jej zamaskovat tak, aby to útočník měl zatraceně složité.

Zopakujme si nyní jak vlastně cracker postupuje. Cracker vidí kód našeho programu v podobě jednotlivých instrukcí a na této úrovni se snaží vyhledat to, co by mohl přepsat NOPy (prázdnými operacemi) či jak tyto instrukce nebo jejich operandy změnit tak, aby program zpřístupnil placené funkce, případně jinak fungoval bez omezení. S vědomím tohoto se můžeme pustit do následujících úprav algoritmů.

inline

Toto kouzelné slovíčko (C++) je příhodné použít všude, kde je to možné, minimálně pak pro celou kontrolní funkci. Pokud váš kompilátor nabízí ještě jiné možnosti jak vynutit rozvinutí funkcí inline, pak je více než vhodné je také použít. Ono je totiž velmi snadné vyrušit instrukci CALL či JMP několika NOPy, případně nasimulováním nějaké návratové hodnoty. Pokud to cracker udělá, celá naše kontrolní procedura se jednoduše nevyvolá, a co se bude dít dál závisí na tom, jak dobře jsme náš mechanizmus navrhli. Pokud ale nejméně hlavní kontrolní funkci (nejlépe všechny a všude) rozvineme inline, žádná instrukce skoku se v kódu neobjeví a namísto ní tam bude několik tisíc jiných instrukcí. Když už toto crackera nijak neodradí, pak jej to minimálně na pořádnou chvíli zdrží.

Poznámka: Studovali jste už někdy, co mnohé crackovací prográmky vlastně dělají, a jaký je rozdíl mezi cracknutým a necracknutým exe souborem? Často byste našli na jednom nebo více místech jen několik změněných bajtů. Kterépak to asi budou instrukce?

Zpoždění hlubších testů

Když jsem zmiňoval vhodnost víceúrovňových testů, napadlo vás zpozdit provedení hlubších testů? Žádných pár sekund, co takhle dva nebo tři dny. Jedná se o to, že při zadání klíče provedeme jen základní kontrolu jeho integrity. Určitě nemůžeme tuto kontrolu zpracovat ledabyle, svůj účinek by také měla mít. Jen počítáme s tím, že se crackerovi podaří tento test odhalit či vyřadit. Jelikož najednou cracknutý program akceptuje jakýkoliv klíč, má onen útočník pocit vítězství, se kterým se jistě okamžitě pochlubí na internetu ve formě cracku. Jaké je ale překvapení, když si po pár dnech program (druhá, skrytá vrstva kontrolního mechanizmu) onen klíč zkontroluje znova, hlouběji, a odmítne spolupracovat. Crackeři tohle přímo nenávidí! Rozveďme tuto techniku do tří nebo čtyř úrovní a přivedeme jej do psychiatrické léčebny.

Random

Jelikož se může zdát, že náhodné čísla nemají v exaktní problematice jako je kontrola klíčů co dělat, je tomu právě naopak. Téměř každý netriviální algoritmus je možné zpracovat mnoha různými způsoby a přece bude stále ve výsledku řešit stejný problém. Pokud si dáme tu práci, a celý nebo části kontroly klíče napíšeme ve více verzích, pak je více než snadné vždy náhodně zvolit jednu z nich. V duchu předchozího tématu můžeme volbu algoritmu odvodit také od aktuálního data a času nebo třeba netradičně podle množství volné paměti.

Čas potřebný k úplnému prolomení kontrolního algoritmu se tak násobí počtem jeho verzí, plus čas, po který si cracker tohoto nevšiml.

Použití pokročilých konstrukcí jazyka

Aby se člověk v disassemblovaném kódu rychle orientoval, musí znát typickou podobu do níž kompilátor sestavuje klasické konstrukce jazyka. Zjevné je, že klasický if, klasický for či while bude mít signaturu rozpoznatelnou snadno pro kohokoliv, kdo se v assembleru nějakou dobu orientuje. Složitější konstrukce se však neobjevují tak často, a tudíž můžeme doufat, že si s nimi bude cracker nějakou chvíli lámat hlavu.

Vlastní implementace standardních funkcí

Když řešíme trochu komplexnější algoritmus, určitě se v něm odvoláváme na mnohé vnější funkce runtime knihoven, pro kopírování, jednoduché převody apod. Při vývoji algoritmu samozřejmě používáme odladěné funkce ze standardních knihoven, nejsme sebevrazi. Ale po odladění vlastního algoritmu stojí reimplementace některých, ne-li všech, použitých funkcí za zvážení. Zvýší to složitost a velikost kódu, který bude muset potenciální útočník analyzovat. Zde, na rozdíl od ostatních částí programu, platí, že čím složitější a pomalejší každá z těchto funkcí bude, tím více práce bude s jeho analýzou bude. Každá špetka invence či netradiční přístup k problému je na místě. Funkce pro kopírování paměti by např. mohla zkopírovat nejprve jeden bajt ze začátku, pak z konce, pak druhý ze začátku, a tak dále. Nebo může „omylem“ zkopírovat o několik bajtů více či méně…

Sdílené mechanizmy

Co když cracker onen kontrolní mechanizmus prostě celý vyřadí a žádná kontrola se provádět nebude? Tomu se můžeme pokusit zabránit různými způsoby, kdy provedení algoritmu kontroly klíče přímo či nepřímo ovlivní i jiné části programu. Nabízí se během dekódování inicializovat datové struktury vybraných funkcí programu, které pak jednoduše nebudou fungovat pokud se kontrola klíče neprovede. Jsou-li některé části algoritmu umístěny do ne-inline funkcí, pak, je-li to jen trochu možné, je vhodné tyto funkce použít i jinde. Vymazané funkce nebudou dělat to co se čeká a společně s ochranou vezme za své i funkčnost programu.

Co ale může pomoci nejvíce, je ovlivnění výstupu neprovedením této kontroly či chybným klíčem. Dejme tomu, že náš program generuje (ukládá, tiskne, …) důležitá čísla (statistická, jakákoliv…). Dejme tomu, že platné klíče mají všechny stejný kontrolní součet. Tedy upravíme algoritmus generování těchto čísel tak, aby všechny hodnoty byly ovlivněny tímto kontrolním součtem. Před samotným výpisem pak spočítáme kontrolní součet znova a čísla ovlivníme opačným směrem. Co se stane, když je klíč neplatný? Je-li ovlivnění dostatečně subtilní, cracker si této změny nevšimne, jelikož nejspíš není expertem na danou oblast, pro ony experty je naopak výstup z takového programu naprosto nepoužitelný. Samozřejmě jsou na místě mnohem složitější variace této techniky, třeba i za použití generátorů náhodných čísel apod.

Frekventované kontroly

V souvislosti s tipem o vkládání maxima kódu inline se nabízí i tento. Vymazat kód jedné kontroly klíče je snadné, avšak zavoláme-li kontrolu (či její část) při každé složitější operaci (práce se soubory, zpracování dat, apod.), pak se její kód rozvine na všechna tato místa a minimálně s jejich mazáním bude mnohem více práce. Zde platí čím častěji, tím lépe, avšak nemůžeme si dovolit zbytečně významně brzdit celé provádění.

Tímto končíme základy kontroly klíčů. Tento seznam by určitě bylo možné rozšířit o mnohé další zajímavé techniky, ale přejděme k dalším tématům, jelikož samotná kontrola klíče celý kontrolní mechanizmus nedělá.

Kontrola integrity programu

Dalším krokem k zabezpečení programu jsou mechanizmy sebekontroly. Implementovat minimálně kontrolní součet pro každý samostatný soubor, jehož modifikace by byť jen zdánlivě, mohla vést ke cracknutí celého programu je nutnost. Pro kontrolní algoritmy je samozřejmě více než vhodné použít podobné techniky, které jsme si popsali u algoritmů kontroly klíče. Přece nechceme zažít situaci, kdy program nepozná, že je cracknutý, právě proto že tomu tak je.

Kontrolní součty

Píšeme-li pro Windows, hned ze začátku udělejme to nejjednodušší co jde. Zapněme v nastavení našeho kompilátoru generování kontrolních součtů pro všechny DLL knihovny, které s sebou program ponese. Tyto lze zkontrolovat velmi snadno funkcí CheckSumMappedFile, a to nejen pro korektnost ale i vůči známým hodnotám, které by měl program znát. Můžeme to pojmout víceúrovňově podobně jako kontrolu klíče, kde toto bude první, nejjednodušší, kontrola, ale skončit zde určitě nemůžeme. Vyberme si dále jeden ze známých kontrolních algoritmů (CRC32, MD5) a některý méně známý (Adler32, …) a nejlépe si ještě jeden navíc vymysleme. S implementací by problémy být neměly, jelikož mnohé algoritmy jsou volně k dispozici na internetu. A zase obecně platí, čím více kontrol a kontroluje-li jedna vrstva druhou (včetně jejího klíče), tím snadněji odhalíme změny a dle nich se můžeme zařídit, viz dále.

Velikost

Zkontrolovat velikost souboru je trivialita, nemyslíte? Tak proč to žádný program nedělá? Ano, je to i velmi snadně prolomitelné, a je-li zaveden kontrolní součet, pak i téměř nepotřebné, ale jsou-li jednou kontrolní součty vyřazeny crackerem, pak jej i tato velmi slabá vrstva na nějakou chvíli pozdrží.

Datum a čas

Kontrola data a času je velmi diskutabilní. Na jednu stranu zde máme možnost porovnávat čas vytvoření a posledního zápisu souboru a zjistit tak, zda někdo ze souborem nemanipulovat. Na druhou stranu datum a čas souboru se může změnit při některých výjimečných operacích, jako obnovení ze zálohy apod. Je třeba tedy dobře zvážit kdy kontrolovat datum a čas souboru, případně tuto změnu nepovažovat za napadení ale pouze jako signál k provedení celé série kontrol sebe sama. Také můžeme kontrolovat počet změn v určitém časovém intervalu a až překročení určitého limitu (např. max 1 změna za den) považovat za útok.

Vzájemná kontrola součástí

Obsahuje-li program více součástí, knihoven, pak je určitě na místě zabudovat ověřování integrity souborů i mimo hlavní spustitelný soubor, a nechat součásti provádět kontroly navzájem.

Pokročilé techniky

Dejme tomu, že se programu podařilo zjistit pokus o napadení. On to nemusel být ani pokus o crack, třeba jej „jen“ napadl virus nebo se poškodil soubor. V takovém případě musíme zajistit aby náš program nemohl dál ohrožovat uživatele a to vše ještě před skončením. Ne vždy je vhodné na systém vznést požadavek k odstranění souboru po restartu. Zobrazení varovného hlášení o změně může sice nadchnout uživatele ale potenciálního crackera jen upozorní na to, že program takovou ochranu obsahuje. To je vhodné zobrazit v případě detekce problému po spuštění, ale jelikož trasování a úprava kódu za účelem prolomení často probíhá za běhu, pak je v případě náhlé změny vhodné reagovat jinak.

Reakce na rozpoznaný pokus o napadení

Jelikož zobrazení hexeditorů či debugerů z převážné většiny okamžitě nereflektuje změnu skutečného kódu, pak změnou vlastního kódu program docílíme toho, že útočník už prakticky luští kód, který již dávno není platný. V podstatě můžeme postupovat následujícími dvěma způsoby.

Sebepoškození

Detekuje-li kontrolní algoritmus napadení, cílené poškození zbylých částí kontrolních kódů, třeba náhodnými daty, povede k nepředvídatelnému chování programu, při troše štěstí pobláznění debuggeru a velmi brzy k havárii programu. Základním vodítkem ke kódu, který bude třeba poškodit nám můžou být adresy funkcí (nebo návěští -- GCC), které máme v plánu vyvolat. Toto ovšem nebude fungovat v případě, že je funkce rozvita inline, S trochou úsilí tedy za pomocí vloženého bloku assembleru můžeme získat z registrů procesoru adresu aktuální instrukce (IP) nebo vrcholu zásobníku (SP), a ono poškození iniciovat z těchto adres.

Co se týče poškození samotného, mnohem lépe než přepsání nějakého celistvého bloku kódu poslouží řídké a nahodilé změny instrukcí či jejich operandů, které povedou k mnohem delšímu laděni jelikož mnohdy nezpůsobí havárii okamžitě. Vyznáme-li se trochu v assembleru, můžeme zapisovat i celé korektní instrukce a tak zmást útočníka ještě více. Nebo kód který zapisujeme, zkopírovat z jiné části programu. Při notné dávce škodolibosti může náš program nést i maligní blok kódu za tímto účelem.

Samoopravné kódy

Tématika samoopravných kódů je velice rozsáhlá a úspěšně se využívá v mnoha oblastech zejména telekomunikací. Samoopravné kódy říkají CD mechanice, jak dopočítat zvuková data z oblastí, které nelze přečíst pro drobné škrábance. A podobně může dopočítat a opravit crackerem modifikované bajty za běhu programu. Vyžaduje to sice nést s programem větší množství nadbytečných kontrolních dat, ale správná implementace může v důsledku opravdu významně pomoci při ochraně kódu.

Jeden triviální příklad za všechny. Rozdělme si kód programu na bloky po 64 bajtech a tyto si představme jako matice 8x8. Pokud pro každý řádek a každý sloupec sečteme jednotlivé bajty a uložíme si je, za běhu je možné tyto součty ověřovat, a v případě změny snadno nalézt modifikovaný bajt a dopočítat jeho původní hodnotu. Budeme-li kód v případě podezření kontrolovat každých 20ms z jiného vlákna, dle vytížení počítače nikdo nic nepozná, a opravný algoritmus bude zvládat opravovat i vícenásobné modifikace stejného bloku. Tento algoritmus lze samozřejmě mnoha způsoby obměňovat, rozšiřovat schopnost opravy a sílu kontrolních součtů, atd.

Šifrování kontrolních kódů

Abychom zamezili okamžité viditelnosti ochranných kódů v souboru, můžeme je zašifrovat. Dešifrování provedeme krátce před provedením samotného kódu a poté kód zpětně přepíšeme původními daty, či odstraníme nebude-li již dále třeba. Nejspíš nebude lehké takovéto šifrování implementovat, jelikož vlastně modifikujeme onen soubor a to je to, čemuž chceme předcházet.

Odvádění pozornosti do jiných vláken

Sílu vícevláknového zpracování můžeme využít i zde, jelikož sledování provádění různých vláken současně je velmi náročné, zvláště obsahují-li podobný kód. Navíc mnohé debuggery volně k dispozici, které jsem měl možnost si vyzkoušet, měly s trasováním přepínání vláken nemalé problémy.

Polymorfní algoritmy

O polymorfismu kódu se často mluví v souvislosti s viry. Prakticky to znamená, že kód po provedení deterministicky mění sám sebe tak, aby následně provedl jinou nebo podobnou akci. Implementace takovéto hard-core techniky, podobně jako šifrování kódu samotného, vyžaduje velké znalosti samotného strojového kódu, a uvádím ji tedy spíše pro úplnost. Z pohledu programátora ve vyšším jazyce bychom nejspíše implementovali kódy samostatně, a následně odečetli série, které je třeba k jednomu kódu přičíst tak, aby se z něj stal kód druhý, a tyto série přenášeli v programu. Samozřejmě nejsme omezeni jen na přičítání ale nabízí se i mnoho logických operací jako and, or, xor, apod.

Shareware

Pro programy distribuované jako shareware nebo podobně je možné využít dalších metod ke zvýšení ochrany. Několik nejužitečnějších tipů a rad je zde.

Omezení distribuce plné verze

Do mnoha programů distribuovaných jako shareware stačí zadat korektní klíč a program najednou plně funguje. Tedy je jasné, že i placený kód je v programu k dispozici, i když je zablokovaný. To umožňuje crackerovi pokusit se, namísto prolamování klíče, jeho kontrolu přemostit či vyřadit. Finta spočívá v tom, že do volné verze programu placené vlastnosti vůbec nezkompilujeme. Až uživateli, který si produkt zakoupil, poskytneme plnou verzi. Ta samozřejmě musí být také důkladně chráněna, protože nikdy nevíme, zda si program nekupuje nějaká warez skupina. Takto, minimálně ze začátku distribuce programu, je možné výrazně zpomalit vývoj cracku na náš program.

Zašedlá tlačítka či položky menu klidně ve volné verzi ponechat můžeme. Dokonce pro ně můžeme přidat i nějaký jednoduchý kód, a to jen proto, abychom crackera zkoumajícího náš program okradli o nějaký ten čas.

Kontrola času

Zjišťování data a času je často kamenem úrazu časově omezených aplikací. Pokud i naše shareware aplikace má mít nějakou trial lhůtu (vlastně trialware), pak je třeba zvážit implementaci následujících dvou kontrol.

Kontrola reálného času.

Jelikož s hodinami je možné v PC libovolně manipulovat, pak nemá cenu pracovat se systémovým časem. I když jako slabá ochranná zídka může odvádět pozornost od zbytku algoritmu. Jak ale získat alespoň trochu reálný pojem o čase? Třeba ze systémových souborů. Zjistit čas poslední změny souboru registru aktuálního uživatele by mělo být možné i z účtu s omezeným oprávněním. Také se můžeme pokusit zjistit toto z některého z naposledy uživatelem otevřených souborů, o nichž si Windows vedou záznamy. Pokud je některý z těchto časů v budoucnosti, je třeba začít být podezřívavý. Je-li uživatelův počítač zrovna připojen k internetu, pak je více než snadné požádat o aktuální čas některý veřejný timeserver.

Ověření intervalu od posledního ukončení.

Máme-li jakousi představu o aktuálním čase, je třeba jej využít i jinak než pro omezení funkčnosti programu na trial lhůtu. Základem by mělo být odmítnout spuštění programu v případě, že je aktuální čas dříve než čas posledního vypnutí programu. To je signál výraznější manipulace s časem, velmi pravděpodobně za účelem vynucení delší funkčnosti programu, než jsme uživateli chtěli umožnit.

Interface programu

Ovládací prostředí programu bývá často prvním vodítkem ke kódu, který o omezeních rozhoduje. Proto je vhodné vyvarovat se generování znakových řetězců s texty „unregistered“ a „registered to …“, apod. Není-li program registrován, pak prostě není registrován, a není důvod to někam psát. I statický prostor pro text s informacemi o registraci má svůj identifikátor či název, a reference na něj se dají v kódu snadno vyhledat. Textové pole s informacemi o registraci v „about“ dialogu, kam tyto informace ostatně patří (a nikam jinam), můžeme vytvořit dynamicky v případě pozitivní kontroly klíče, pokud možno co „nejdále“ od vlastního kontrolního algoritmu. Ono vytvářet všechna menu či dialogy dynamicky vůbec není špatný nápad, jelikož povolit zašedlou položku v prostředcích je trivialita.

Psychologie kódu

V rámci vývoje obranného mechanizmu můžeme do kódu zapracovat části, které mají odvést pozornost potenciálního crackera od důležitých míst. Zde můžeme jít několika cestami, zde jsou nezajímavější z nich.

Falešné instrukce

Zde je zapotřebí dobrá znalost použitého kompilátoru a instrukční sady. Technika obecně spočívá v zavedení sekvence bajtů mezi data s jiným významem, za účelem zmást debugger či disassembler a tudíž i útočníka.

Neproveditelné skoky

Do kódu lze umístit číselnou hodnotu odpovídající některým instrukcím (typicky JMP, CALL, apod), přičemž s ní stále pracujeme jako s daty. Nejlepší je, má-li toto číslo užitečný význam, nebo lze-li z něj tento význam dekódovat, pak se totiž nemusíme obávat jeho vyoptimalizování kompilátorem.
Budeme-li se touto problematikou zabývat hlouběji, pak je velmi vhodné zavést falešné skoky na globální funkce API. Jejich adresy jsou globálně známé, debugger je tudíž pravděpodobně zvýrazní a pro čtenáře kódu tak budou mnohem lákavější.

Spustitelná data

Také opačně lze umístit třeba celou funkci do datových oblastí programu. Několik instrukčních bajtů mezi mnoha řetězci a číselnými hodnotami nebude nápadné, a při méně častém volání existuje mnohem větší šance, že tento kód zůstane skryt. Jedná-li se o inline funkci, pak takto schovaný kód můžeme použít jako referenční při kontrole korektnosti jednotlivých výskytů.
Co se týče konkrétní implementace umístění dat, pak je zapotřebí si prostudovat manuál k vašemu kompilátoru. Např. GCC dokáže umisťovat data i kód do libovolných sekcí pomocí extenze (atributy).

Prolomené klíče

Vězte, že ke koupi i volně k dispozici je množství kontrolních algoritmů a ochranných systémů, které již dávno byly prolomeny, mají známe signatury a je možné je snadno algoritmicky odstranit. Máme-li dostatek prostředků a času, pak se zavedení některého z nich může fungovat jako další z metod odvádění pozornosti. Cílem je, aby útočník strávil nějaký čas vyhledáváním a rutinním prolamováním známé kontroly, která ve výsledku vůbec nemusí mít na provádění programu efekt. Na místě jsou i drobné modifikace, které znepříjemní vyhledávání známých vzorů kódu.

Zapracování takovýchto algoritmů je vhodné třeba jako první úroveň doporučených víceúrovňových ochranných mechanizmů.

Vlastní crack

Jelikož mnoho z crackerů vytváří svá dílka především ze soutěživosti a proto, aby se mohli pochlubit v komunitě. Existence cracku od jiného autora může pro mnoho z nich znamenat nezajímavost našeho programu, a to je také náš cíl. Snadnou cestou k tomuto stavu je vytvoření vlastního cracku. Protože ale většinou nejsme crackeři, pak je třeba si v programu ponechat polootevřená vrátka v podobě známé sekvence bajtů (nebo offsetu), kterou je třeba změnit a zpřístupnit tak funkčnost programu.

Pointou je, že náš crack nebude tak úplně korektní. Modifikace programu sice způsobí zpřístupnění placených funkcí, avšak ne úplně korektně a za vědomí programu samotného. Ten pak na tuto situaci může reagovat různými způsoby, nejčastěji se doporučuje náhodně chybné chování nejdůležitějších placených funkcí (jako třeba ukládání dat na disk), nebo náhodné pády celého programu po delší době práce. Každý typ programu nabízí jinou funkčnost a dle toho se budou lišit i tyto reakce.

Zde je třeba ale pečlivě zvážit, zda implementace této techniky samotné, nepředstavuje riziko nebo bezpečnostní trhlinu.

Co nemá smysl

Minimálně dvěma směrům při vývoji ochrany je záhodno se vyhnout, jelikož stojí programátora čas a ve výsledku se neprojeví.

Anti–debug a anti–disassemble techniky

Implementace jakýchkoliv ochran proti debuggerům či disassemblerům je vyložená ztráta času, nejedná-li se o jednořádkový příkaz, třeba IsDebuggerPresent z Windows API. Všechny debuggery používané dnes crackery se dokáží velmi důmyslně maskovat. Ochranu proti jednomu lze snadno vyřadit jiným a jakkoliv náročná práce má nakonec jen minimální efekt.

Naopak je tomu u antidekompilačních technik. Některé kompilátory generují snadno rozpoznatelné sekvence pro jednotlivé konstrukce jazyka a díky tomu je možné je někdy dokonce dekompilovat. Jelikož kód ve vyšším jazyce je nepoměrně snazší číst než kód v jazyce symbolických adres, zpracování programu některým z procesů zabraňujících dekompilaci je více než vhodné.

Neefektivní jazykové konstrukce

V jednom z předchozích tipů doporučuji používat pokročilé a složitější konstrukce poskytované zvoleným programovacím jazykem. Tím ovšem nejsou myšleny neefektivní a zbytečné konstrukce, které by měly jen „zvětšit“ množství kódu. Vězte, že víceprůchodové optimalizéry používané v moderních kompilátorech jsou docela důkladné při vyhledávání neefektivních bloků kódu.

Ať už napíšete kolik neefektivního kódu chcete, nakonec program zůstane holý a průhledný. Je třeba snahu zaměřit jiným směrem.

Aktivní boj

Napsáním programu a jeho vypuštěním zdaleka vše nekončí. S crackery lze bojovat i během distribuce programu. Nejčastěji používané cesty jsou následující:

Různé mutace programu

Pro programy jejichž ukázkové verze jsou distribuovány pomocí různých download serverů, je vhodné vytvořit různé mutace, minimálně ochranných kódů, pro každý z těchto serverů. Podobně můžeme poskytnout každému uživateli jinak upravenou verzi, jedná-li se o program s menším rozšířením. Této mutace můžeme dosáhnout nejen vlastním programováním jednotlivých verzí ale i různým nastavením optimalizací.

Cílem tohoto snažení je omezit okruh uživatelů, kterým bude případný crack fungovat. A samozřejmě znesnadnit život crackerům, kteří tak budou zavaleni zprávami o nefunkčnosti jejich dílka.

Aktualizace databáze uniklých klíčů

Je klasický přístup při vydávání nových verzí programu. Je zapotřebí občas procházet servery sdružující klíče (tzv. serialz/cerealz) k programům, vyhledávat náš program, a tyto uniklé klíče neakceptovat v nové verzi programu.

Samozřejmě i pro tuto část programu platí požadavek na bezpečnost a propracovanost kódu, jelikož i na tyto části lze vést pokus o crack.

Nátlak na správce warez serverů

A pokud se už nějaký ten crack objeví, nemusí to nutně znamenat, že jsme prohráli. Je třeba jen akumulovat všechny naše schopnosti co se týče anglického jazyka, sepsat profesionálně znějící e-mail informující o znepokojení naší firmy, o ilegálnosti onoho cracku a o možných sankcích, a odeslat jej provozovateli warez serveru a provozovateli jeho hostingu. Při troše štěstí pro jistotu onen crack stáhnou, a našemu programu tak dají nějaký ten čas navíc.

A když nic nepomůže…

…sedneme si pohodlně do křesla se sklenkou vína a doutníkem, a budeme pyšní na to, že jsme odvedli na programu kvalitní práci, když si někdo dal tu práci s prolamováním všech ochran.

exertion of the imagination, portfolio library and software repository