Could you hire me? Contact me if you like what I’ve done in this article and think I can create value for your company with my skills.

February 11, 2015 / by Zsolt Soczó

.NET fejtörő 6.

Miért fogják feltörni könnyen az alábbi jelszó ellenőrzőt? Ne csak a nyilvánvaló okokat írjátok meg.

private static bool PassValidator(string pass)
{
    string passInDb = "password";   //az eredeti implementációban valahonnan memória cacheből jön az adat

    int i;
    for (i = 0; i < pass.Length; i++)
    {
        if (pass[i] != passInDb[i])
        {
            break;
        }
    }
    return i == passInDb.Length;
}

Could you hire me? Contact me if you like what I’ve done in this article and think I can create value for your company with my skills.

LEAVE A COMMENT

7 COMMENTS

  • Meister February 11, 2015

    Plain text manapság már nem illik tárolni a jelszót, hanem valamiféle hash-ét, sokszor salt-ozva.
    Így ki is húzzuk azt a méregfogat, hogy az ellemőrzésénél mérhető az idő, ami után hibát ad. Ennek a hosszából ki lehet találni a jelszót, hiszen hamar megvan, hogy az első karakter mi, aztán a második, és így tovább. (Timing attack).

  • Tamás February 18, 2015

    0) A triviális hibák: nem jelszó hash-t használ a jelszó ellenőrzésére. Túl gyorsan lefut, brute-force kísérletezésre nagyon is alkalmas.

    1) Nem teljesen nyilvánvaló, de könnyen észrevehető hiba, hogy a valódi jelszót tetszőleges stringgel kiegészítve a kód IndexOutOfRangeException-t dob, ebből a jelszóra rájönni már csak legfeljebb annyi vizsgálat, ahány karakter a tesztelt jelszójelölt.

    2) A nagy baj, hogy az ellenőrző kód futásideje függ a jelszó első karakterei közül “eltalált” karakterek számától, azaz pusztán a függvény futásidejéből következtetéseket levonva karakterenként felépíthető a jelszó (a memória cache-ről feltételezve a viszonylag kis késleltetés-ingadozást).
    Készítettem egy egyszerű kódot, ami egy ma már átlagosnak mondható notebookon (Core i5) egy percen belül visszafejti az így ellenőrzött jelszót még akkor is, ha az 12-karakteres és három karakterosztályból tartalmaz karaktereket.
    A kód lényege a futásidő mérése a PassValidator függvény futtatásakor:

    var watch = Stopwatch.StartNew();
    // azert sokszor hivjuk meg, hogy merheto legyen a kulonbseg, gyors gepen lehet, hogy novelni kell, a kivanatos idokulonbseg legalabb 3-4 tick eltalalt karakterenkent
    for (int i = 0; i < 100; ++i)
    {
    PassValidator(guess);
    }
    watch.Stop();

    Ahhoz, hogy a fenti hatékonysággal lehessen jelszót kitalálni persze még kellenek trükkök a tippek kiválasztásakor, ezeket kérésre kifejtem bővebben.

    +1) A jelszó nem biztonságosan tárolódik a memóriában, swapelhető területen, a DPAPI használatával ez elkerülhető, bár ez már inkább szőrszálhasogatás, mint valós probléma a kóddal.

  • Soczó Zsolt February 26, 2015

    Köszi Tamás, ez egy alapos elemzés, nem is vártam ilyen részletes munkát. :)

  • Soczó Zsolt February 26, 2015

    Ja, és érdekelnének a részletek. A kódod feltételezem nem nyilvános, pedig érdekelne pl. az általam prezentált szándékosan buta kóddal mit kezdene.

  • Tamás February 26, 2015

    A következő trükköket érdemes bevetni a teszteléskor:
    1) Azon alapul az első trükk, hogy kitűzendő célunk, hogy a jelszóellenőrző algoritmus minél hosszabb ideig fusson. Ha a jelszó a példa szerinti password, és a pass stringig eljutottunk valamilyen módszerrel, akkor további egy karakterrel kiegészítve a jelszójelöltet találgatunk tovább. Jelöljük ezt a kiegészítő karaktert az aláhúzás karakterrel: _!
    Ekkor, ha _ a jelszó következő karaktere (példánkban w), illetve ha _ nem megfelelő, a futás annyiban különbözik, hogy a ciklusból normálisan vagy break-kel lépünk ki, ez nem nagy differencia, ennek detektálása nehéz. Ezért a pass_ stringhez a jelölt mellé még egy további karaktert fűzünk, mivel ebben az esetben _ helyessége esetén egy további karakter-összehasonlítás történik, ezzel meg tudtuk növelni a jó és rossz választás esetén tapasztalt futásidőbeli különbséget. Ezt a karaktert minden jelölttel való időmérési kísérlet után eldobjuk.
    1a) Szerencsés, ha a “további karakter” egy érvénytelen karakter, mivel a második pontban leírtakban bezavarhat, ha véletlenül pont eltaláljuk a _ után következő karaktert.
    2) Mivel a kód vélhetően többfeladatos környezetben fog futni, lesznek ingadozások a futásidőben, előfordulhat, hogy tévútra jut az algoritmus (az összes lehetőséget tartalmazó fa egy rossz ágára téved). Ezt detektálhatjuk, ha nyilvántartjuk az egy karakterrel rövidebb jelszójelölthöz tartozó ellenőrzési időt, és ha azt tapasztaljuk, hogy csökken az idő, feladjuk a jelszókeresést, majd újrakezdjük az elejéről egy üres stringgel. Ez nem tűnik nagy dolognak, de ez a trükk félelmetesen sokat gyorsít a jelszó visszafejtésén. Lehet persze, hogy akkor vágunk, amikor nem kellett volna, de talán a következő futásnál nagyobb szerencsénk lesz.
    3) Nyilvánvaló dolog: ha túl gyors a gépünk, a jelszóellenőrzést többször futtatjuk az időmérés során, hogy az időkülönbségek mérhetők legyenek.

    Az emlegetetett kódot egyébként konkrétan erre a feladatra írtam, mert érdekelt, hogy vajon mennyire könnyű a valóságban belefutni ebbe az alapvetően elméleti természetűnek tűnő problémába. Nem is annyira elméleti… Ez egyben azt is jelenti, hogy az általad prezentált kóddal kapcsolatban írtam azt, hogy 12-karakteres, három karakterosztályból karaktereket tartalmazó jelszóra jön rá egy percnél rövidebb idő alatt (ez általában 15 másodpercnél is rövidebb idő).
    Ami érdekes, hogy bár 20-karakterig elég megbízhatóan működik, és többnyire tartja az egy perc alatti futásidőt, 21-karakteres jelszavakkal már nagyon meggyűlik a baja, vélhetően a 2) pontban említett favágó-algoritmus finomhangolásával lehetne ezt javítani, de nem vagyok benne biztos. Egyelőre nem látom, hogy mi a drasztikus különbség oka, de elképzelhető az is, hogy az operációs rendszer ütemezője áll a háttérben.

  • Soczó Zsolt February 27, 2015

    Köszi megint a részletes leírást, nagyon tetszik, hogy rászántad az időt, és megnézted a gyakorlatban is a kérdést, bizonyítva, hogy ez nem csak elméleti okoskodás.