Biztonságosabb jelszó tárolás


Ryan Malesevich írt a blogjában az MD5-ként tárolt jelszavak sebezhetőségéről. Az onnan merített – általam ott már egyszer leírt ötletet – közölném itt, hogy egy kicsit biztonságosabban tároljuk felhasználóink jelszavait.

Alapvető feltevésünk: az egyirányú kódolások (CRC32, MD5, SHA, …) biztonságosabbak a kódalatlan jelszó-tárolásnál, azonban nem elég biztonságosak. Számoljunk egy kicsit, most az MD5 példájánál maradva!

Az MD5 ellenőrző számok az angol ábc betűiből (26) és számokból (10) állnak. Ez ugye 32 felvehető érték, 32 karakternél (32 karakteresek az MD5 ellenőrző számok). Tehát összesen 3632 [kb. 1,46 * 1048] lehetséges MD5 ellenőrző szám van. Ha ASCII 256 karakterével számolunk tovább legelőször a 21 karakter hosszú sztringeknél kell, hogy legyen MD5 ellenőrző-szám ismétlődés, mivel ekkor már a lehetséges kombinációk száma 25621 [kb. 3.44*1050] (20 karakternél ez "még" csak 1.35 * 1048).

Az előzőből gondolom már jól látszik, hogy nem igazán nehéz azonos MD5 hasheket találni. SHA-nál is hasonló a helyzet, csak ott a 40 karakteres ellenőrző szám miatt az ismétlődések később válnak szükségessé, elkerülhetetlenné. Tehát ez lesz első védelmi vonalunk: SHA-1 kódolással tároljuk a jelszavakat. (Ryan ezt a SHA-1 visszakereső adatbázisok hiányával támasztja alá, ez is egy jó érv :) )

Ryan szintén ír a "sózásról" (salting). Szép és jó dolog egy fix karakterlánccal megfűszerezni a jelszavakat, mivel így – a használt karakterlánc ismeretének hiányában – a támadó még mindig nem tud mit kezdeni a megszerzett-visszfejtett jelszavakkal. Úgyhogy ezt is bevetjük majd a használt megoldásunkban, ez lesz a második védvonalunk: salting

Mint korábban írtam még a visszafejtett jelszavak között is lehet, sőt kell, hogy legyen ismétlődés. Azonban a valószínűsége, hogy az ismétlődő MD5 hashű karakterláncok hossza megegyezik egy kicsit alacony: ezt is be fogjuk vetni, javíthat valamennyit az esélyeinken. Eltároljuk a jelszavak hosszát is, és összevetjük a megadott jelszó hosszával. Ha nem egyezik a két érték biztosak lehetünk abban, hogy téves a jelszó.

Fontos azonban megjegyezni, hogy ehhez természetesen ismerni kell az eredeti jelszót, így MD5 átalakítónkban ez nem fog szerepelni, viszont írok erre is egy példát, egy újonnan induló oldalnál már fel lehet használni. (Ryan megoldása (két külön tárolt jelszó) használható lenne, azonban felesleges adatot nem tárolunk)

1. menet: átalakítások

Ebben a szakaszban feltételezem, hogy jelenleg a rendszerünk MD5 jelszavakat használ, és erre építek, amennyiben nem, a példák apróbb módosításra szorulnak.
Valószínűleg – teljesen jogosan – jelszó mezőink 32 byte hosszú VARCHAR típusúak. Megérthető, csak nekünk ez most nem lesz jó, úgyhogy gyorsan alakítsuk át őket 40 karakter hosszúra:

ALTER TABLE `táblaneve` CHANGE `jelszó mező neve` `jelszó mező neve` VARCHAR(40)

Ezzel a jelszó mezőnk már a kívánalmaknak megfelel, azonban még nem teljesen jó nekünk, mivel még a régi 32 byteos ellenőrző kódokat tartalmazza, úgyhogy frissítsük is őket. Az itt használt salt-ot jegyezzük meg, enélkül később adataink használhatatlanná válnak! Lássuk be, ez is egyik célunk…

UPDATE `táblaneve` SET `jelszó mező neve`=SHA1(CONCAT(`jelszó mező neve`, "Salt"));

(A Salt helyére persze írjuk saját fűszerünket!)

Ezzel máris megvan az összes jelszavunk. Azonban itt fontos két – valószínűtlen – dolgot megjegyezni: azokban a sorokban, ahol eddig Null érték volt a jelszónál továbbra is null lesz (a CONCAT Null-t ad vissza, ha Null érték is szerepel paraméterként, és a Null SHA1 hashe Null), az üres sztringnél (aminél értelemszerűen a felhasználó nem tud bejelentkezni) a SHA1 ellenőrző-számmal visszafejthetővé válik a saltunk, mivel csak azt tartalmazza. Éppen ezért érdemes minél hosszabbat választani (mondjuk 20 karakter…)

Ezután persze a felhasználó-kezelésünkön is módosítani kell egy kicsit, itt most csak a lehetséges megoldásokat írom le, mivel ahány felhasználó-kezelő rendszer annyiféle megoldás létezik. Az egyik megoldás a MySQL-re bízza az ellenőrző-szám elkészítését, ehhez azonban – a függvény felvitelének idejére mindenképp – szükségünk van egy MySQL acc-ra, amivel írhatunk a MySQL adatbázisba!

CREATE FUNCTION `createHash`(p_string VARCHAR(255), p_salt VARCHAR(20)) RETURNS VARCHAR(40)
RETURN SHA1(CONCAT(MD5(p_string), p_salt));

Ezzel létrehozunk egy új MySQL függvényt, ami két paramétert fogad el: az első a kódolatlan jelszó, a második a saltunk. Így a felhasználó-ellenőrző kérésünket át kell írnunk egy kicsit, én valami ilyesmivel ellenőrizném a felhasználónév-jelszó páros helyességét:

SELECT COUNT(`id`) AS `auth` FROM `táblaneve` WHERE `felhasznalonev`='felhasználónév' AND `jelszo`=createHash('Megadott jelszó', 'Salt') LIMIT 1;

Ezután a begyűjtött tömb auth elemét ellenőrizve (0 – nincs ilyen felhasználó, 1 – létezik) meg is történt az ellenőrzés. Ha ez adat-betöltéssel is együtt kell, hogy járjon, akkor a Count(`id`) AS `auth` helyére egy * kerül.

Fontosnak tartom kiemelni, hogy miért kell a függvénynek megadni a Salt-ot: amennyiben ezt a függvényünk kódjába beírjuk egy támadó – az adatbázis megszerzése után – egyszerűbben vissza tudná alakítani a jelszavunkat. (Így mondjuk az első megfejtett jelszónál már ismeri azt, mivel a jelszó mindig 32 karakter. Persze ehhez tisztában kell lennie a tárolás módszerével…)

Felhasználó felvitelekor és módosításakor (jelszó-csere, ilyenek) úgyanígy a jelszó mezőnek a createHash() függvénnyel adhatunk megfelelő értéket, mindenhol megadva a salt-ot.

Kiütés az első menet elején

Sajnos előfordulhat – valószínű, hogy elő is fordul -, hogy nincs hozzáférésünk a MySQL adatbázishoz, így nem tudunk új függvényt felvinni. Ilyen esetben használhatunk egy ilyesmi megkerülő megoldást php-ban:

<?php
function createHash($string)
    {
    return SHA1(MD5($string) . 'Salt');
    }
</code]
<p>Itt is a Salt-ot cseréljük le sajátunkra, értelemszerűen. Ugyanazt csinálja, az egyetlen különbség, hogy itt a Null jelszónál is kapunk egy értéket, mivel az üres karakterláncként értékelődik ki. Valójában lényegtelen...</p>
 
<p>Ekkor egy felhasználó-ellenőrző függvény például így nézhet ki:</p>
<pre lang="php">
<?php
function auth()
    {
     return (current(mysql_fetch_assoc(mysql_query("SELECT Count(`id`) FROM `felhasznalok` WHERE `felhasznalonev`='" . mysql_escape_string($_SESSION['username']) . "' AND `jelszo`='" . createHash($_SESSION['password']) . "' LIMIT 1"))) == 1 ? true : false);
     }

Ebben természetesen hibakezelés nem sok van, de egy sorban azt nehéz összehozni, szemléltetésre pedig elég tűrhető.

Finomhangolás

Mint korábban említettem hozzájárulhat a biztonsághoz a jelszóra vonatkozó adatok tárolása – kódolt formában. Nem kell bonyolult dolgokra gondolni, kezdőbetű, utolsó betű, bizonyos helyen álló betűk kis és nagybetűs állapota, a jelszó hossza, akármi. Most a jelszó hosszának felvitelével fogjuk egy kicsit erősíteni a védelmet.

CREATE FUNCTION `createHash`(p_string VARCHAR(255), p_salt VARCHAR(20)) RETURNS VARCHAR(40)
RETURN SHA1(CONCAT(CHAR_LENGTH(p_string), MD5(p_string), p_salt));

Használni ugyanúgy tudjuk, mint az előző példáknál, csak egy plusz védelemmel láttuk el az egészet. Ráadásul 1-3 [kevés ember használ 999 karakternél hosszabb jelszót] karakterrel hosszabb lett a kódolt (SHA1) jelszavunk eredetije, így még valószínűtlenebbé vált annak feltörése. Ha nulláról indulunk, szerintem, megéri.

Ugyanennek PHP megfelelője:

function createHash($string)
    {
    return SHA1(strlen($string) . md5($string) . 'Salt');
    }

Amennyiben valami hasonló védelmet felépítünk oldalunkra (hasonlót, mert az elvek ismeretében máris több esélye van egy támadónak), máris hozzájárultunk felhasználóink – általában több oldalon is használt – jelszavainak biztonságához…


A legjobb megoldás

Ezt a megoldást először elfelejtettem leírni, így utólag szerkesztve írom bele, ezért elnézést. Ebben már arra építünk, hogy a az eredeti jelszó MD5 hashe ne legyen teljtes mértékben visszakereshető, és ténylegesen csak brute-force technikával lehessen egy felhasználónév-jelszó párost megszerezni.

CREATE FUNCTION `createHash`(p_string VARCHAR(255), p_salt VARCHAR(20)) RETURNS VARCHAR(40)
RETURN SHA1(CONCAT(SUBSTRING(MD5(p_string), 1, 32 - CHAR_LENGTH(p_salt)), p_salt));
<?php
function createHash($string)
    {
    return sha1(substr(md5($string), 0, 32 - strlen('Salt')) . 'Salt');
    }

Így már egy 16 karakteres Salt esetében az md5 hash-nek csupán az egyik felét tároljuk, így maximum 3616 lehetséges md5 hash-t kéne végigpróbálni, hogy megtaláljuk az eredeti jelszót…

Kapcsolódó bejegyzések:
  • Gyakran használnak a weboldalon “Emlékezz rám” típusú, süti (cookie) alapú bejelentkezési módot. Ez tényleg nagyon hasznos szolgáltatás, ám legalább ekkora bizto …

  • A tutorial a PHP-n belüli session és MySQL alapú bejelentkezésről szól. Mindent részletesen leírok, nem kell félni! :)Először is: Mi az a SESSION? A s …

  • Mivel egyre gyakrabban fordul elő, hogy sok – felesleges – hozzászólás kerül a fórumba egy-egy egyszerű, csak rosszul kifejtett kérdés megválaszolásához, gondolt …

  • Egy kicsivel összetettebb adatbázis alapú (példában mysql) látogató számlálót fogunk most megnézni. Ami talán kicsit érdekessé vagy különlegessé teszi, hogy a se …

  • Ahogy belépek egy magyar nyelvű fórumba ahol webszerkesztés téma is van, olyan nincs, hogy ne találjak olyan kérdést, hogy “Keresés, hogyan?”. Most erre talász i …

A cikket beküldte: BlackY ()

3 hozzászólás

  1. BlackY says:

    “Az MD5 ellenőrző számok az angol ábc betűiből (26) és számokból (10) állnak. Ez ugye 32 felvehető érték, 32 karakternél (32 karakteresek az MD5 ellenőrző számok). Tehát összesen 3632 [kb. 1,46 * 1048] lehetséges MD5 ellenőrző szám van. Ha ASCII 256 karakterével számolunk tovább legelőször a 21 karakter hosszú sztringeknél kell, hogy legyen MD5 ellenőrző-szám ismétlődés, mivel ekkor már a lehetséges kombinációk száma 25621 [kb. 3.44*1050] (20 karakternél ez “még” csak 1.35 * 1048).”

    Ez a bekezdés egy az egyben marhaság. Az Md5 ellenőrző számok (32 karakterként ábrázolva) 0-f karakterek között lehetnek, így 16^32 különböző md5 hash létezhet. (3,4e+38). Vagyis 256^16 különböző hash, következésképp a 0-16 hosszúságú stringek esetén már biztosan van egyezés. (SHA-1-nél ugyanez 16^40 és 256^20).

    BlackY

  2. Pipis says:

    Az online MD5 kódoló oldalakat ne használjátok, megjegyzik a jelszavakat és abból építik fel a visszafejtő adatbázisaikat. Volt egy kis ráérő időm, teszteltem néhány online encoder-decoder oldalt. A módszer a következő volt: saját gépen java generátorral generáltam 100 jelszót 16 karakteres láncból. Ezeket kódoltam szintén saját gépen MD5-be, majd néhány online visszafejtőben megpróbáltam mindegyiket. Az eredmény 11%-os, vagyis 11-et feltört.
    Második módszerként másik 100 jelszót már online kódoltam, majd azonnali visszafejtés más oldalakon. Az eredmény 13 volt. A furcsaság ezután jött, egy hónap múlva megismételtem az online kódolt jelszavakkal a visszafejtést, kihagyva a már előzőleg megbukottakat. ebből a maradék 87-ből már 53-at felismertek az adatbázisok. Kipróbáltam jó néhány primitív jelszóval is, amelyekről kiderült gyakorlatilag mintha nem is használna az ember semmit, kb annyit érnek.

  3. kukko says:

    Sziasztok!

    Én az utolsó megoldást használtam a weboldalamon és egy olyan kérdésem lenne hogy hogyan tudnám vissza fejteni a jelszavakat, ez megoldható?

Szólj hozzá
a Biztonságosabb jelszó tárolás c. bejegyzéshez

- Engedélyezett HTML elemek: <a> <em> <strong> <ul> <ol> <li>
- Forráskód beküldéséhez tedd a kódot ezek közé: <pre lang="php" line="1">Kódrészlet helye itt</pre>