C, PHP, VB, .NET

Дневникът на Филип Петров


* Някои допълнителни препоръки за запазване на пароли

Публикувано на 25 март 2012 в раздел ОСУП.

За кода в предишната статия, в която се демонстрира "завършената" форма за автентикация с поддръжка на автоматично влизане с cookie, има допълнителни фактори, на които би трябвало да обърнем внимание при запазването на паролите. Нека припомним как записвахме паролите в базата данни:

hashedpass = HASHCODE ( 'SALT1passwordSALT2' )

В тази формула "hashedpass" е защитената парола (записана в базата данни), "SALT1" трябва да е много дълъг произволен низ, който записваме в кода на приложението и до който правим всичко възможно да няма външен (та дори и вътрешнофирмен) достъп, "SALT2" беше псевдослучайно число или низ, записан за всеки отделен акаунт в базата данни, "password" е паролата на клиента, а "HASHCODE" e хеш функция, която приемаме за сигурна и непробиваема (т.е. възползваме се от цялата ѝ ентропия). Нека припомним предимствата и недостатъците на двата вида salt:

  • SALT1
    • Предимства: Подсигурява хешовете записани в базата данни от съществуващи rainbow таблици. Низът е записан в приложението и се предполага, че не е наличен за хакер, който има достъп само до базата данни.
    • Недостатъци: Достъп до сорс кода на приложението излага секретния низ на показ, а от там хакера може да създаде нова rainbow таблица спрямо него. Освен това две еднакви пароли на различни потребители ще имат един и същи хеш код.
  • SALT2
    • Предимства: Подсигурява хешовете записани в базата данни от съществуващи rainbow таблици, като при това долепеният низ е различен за всеки един потребител (т.е. хакерът не може да се възползва от създаването на нова rainbow таблица, която да използва за всички потребители, а ще му се наложи да атакува всеки един поотделно). Потребители с еднакви пароли ще имат различни хеш кодове.
    • Недостатъци: Низовете са налични в базата данни, до която се предполага, че хакерът има достъп по принцип (той е откраднал хеш кодовете именно от нея). Затова и offline brute force атака срещу конкретен потребител остава като наличен проблем.
  • SALT1+SALT2
    • Предимства: Всички предимства на SALT2, като в допълнение на това хакерът евентуално няма достъп до SALT1, което не му позволява да прави дори offline brute force атаки към паролата (трябва да ги прави към паролата И SALT1 едновременно).
    • Недостатъци: Достъп до сорс кода на приложението излага секретния низ SALT1 на показ и от там нататък защитата се свежда до нивото на тази на използването на SALT2.

Как би процедирал хакер, който е добил достъп до базата ни от данни? За него ще са налични хеш кода и SALT2. Естествено предполагаме, че самия той има акаунт в системата ни, следоветелно за един от откраднатите хеш кодове той знае паролата. Или от примерната формула горе следва, че хакерът знае hashedpassword, password и SALT2 за един от акаунтите. За останалите акаунти знае само hashedpassword и SALT2. За да започне да атакува други акаунти с offline brute force атака той все още трябва да намери SALT1. Следователно сведохме защитата ни до:

  1. Хакерът трябва да направи offline brute force атака върху своя собствен акаунт, с цел намирането на SALT1. Ако този низ е много дълъг и използва много специални символи извън ASCII таблицата, то това може да отнеме огромно количество време. Нека това време е t1.
  2. След като евентуално е намерил SALT1, хакерът трябва да генерира нова offline brute force атака за всеки един потребител отделно. Това също отнема голямо количество от време (в зависимост от дължината и сложността на конкретната парола), което ще отбележим средно като t2 за акаунт.
  3. Ако паролите на нашите потребители са "сигурни" (достатъчно дълги и използващи специални символи), а HASHCODE е достатъчно сигурен хеш алгоритъм, то практически увеличаваме драстично времето t2 и така свеждаме вероятността хакера да може да атакува даден акаунт до "практически невъзможно".

Виждаме, че за хакването на един акаунт ще отнеме средно време t1 + t2, а на N акаунта t1 + N.t2. Дали обаче това е реално така и дали така записаната защита е достатъчна? Всъщност има допълнителни фактори, които досега въобще не отчитахме:

  • Наистина ли е "непробиваем" хеш алгоритъма? Какво ни гарантира, че в бъдеще няма да бъде открит пробив в него? И ако поддържаме голяма система с много потребители, то какво ще правим, ако евентуално е открит пробив в хеш алгоритъма? Смяната му в "production" система не винаги е лесно действие.
  • Вътрешнофирмена сигурност - невероятно ли е нашият администратор на базата данни да компрометира сигурността на системата и да изнесе данни? Или някой от локалната мрежа просто е успял да подслуша трафика между уеб сървъра и базата данни (а ние не сме го криптирали с SSL, за да спестим натоварване на сървърите, нещо което всъщност е нормална практика)? Изобщо защитена ли е системата ни срещу изнасяне на информация отвътре?

А сега си представете какво ще стане при комбинация от двете? Естествено едва ли можем да приемем, че може да се направи система за сигурност, която теоретично би устояла на всичко възможно на този свят, но със сигурност примерът от предишната статия е силно уязвим от която и да е от тези два споменати пробиви. Нека ги разгледаме поотделно.

Всъщност в примера от предишната статия е допусната една огромна грешка, а именно: там изпращахме низа SALT1password директно в чист вид чрез SQL заявката към сървъра. Кой гарантира, че човекът добил достъп до базата данни няма и достъп до трафичните данни, които СУБД приема от приложението? Например тривиално това е нашият администратор! Тоест ние излагаме нашият хеш код SALT1 свободен за четене извън рамките на зоната на сигурност на сорс кода на приложението!

Сега набързо ще разгледаме поотделно двата проблема.

Хеш алгоритъмът е "пробит"

Всъщност няма нищо ново под слънцето - това се е случвало с известни и популярни в миналото алгоритми и нищо не предвещава, че няма да продължава да се случва. Алгоритъм, който днес смятаме за "най-доброто" може още утре да се окаже "напълно ненадеждно". И засега никой не може да ни гарантира, че това няма да е така.

Ето ви примерен сценарии - хакер е успял да източи всички хеш кодове на пароли от базата ни данни. В същия момент се оказва, че за хеш алгоритъма, който сме използвали е намерена ужасяваща (за нас) уязвимост и хакера спокойно започва да открива колизии на нашите хеш кодове за доста по-кратко време, отколкото сме предполагали, че ще може (ентропията е намалена). На този етап все още сме спокойни, защото колизиите сами по себе си не вършат работа - на хакера му трябва оригиналния низ, а не негова колизия. А нашият SALT1 е бил толкова дълъг, че предполагаме, че трудно ще стигне до оригиналния низ. Само, че "времето е пред него". Рано или късно ще го открие и при достатъчно "пробит" хеш алгоритъм може да се окаже така, че търсенето на колизии да отнема време доста по-малко от очакваното преди това от нас t1. И понеже алгоритъмът е един и същ - t2 също ще е драстично намалено. С други думи цялата ни защита е провалена от евентуалния провал на хеш алгоритъма.

Администраторът на базата данни ни краде / някой подслушва трафика между приложението и базата данни

Тук предполагаме, че хеш алгоритъмът НЕ е "пробит". Но за сметка на това най-доверените ни хора крадат информация от нас. По-горе споменах, че сме допуснали голяма грешка като предаваме SALT1password директно в заявката. Да припомним как правихме това:

...
$pass = SALT.$pass;
...
$sql = "SELECT id FROM users WHERE user = '$user' ";
$sql .= "AND pass = SHA2(CONCAT('$pass',CAST(token AS CHAR)),256)";
$result = mysql_query($sql);
...

Ако потебителят е "pencho", нашият SALT1 например е "sdagfdjkgf24243" (бел.ред. това е доста къс salt!), паролата на потребителя е "Iv@ansSecPa$$", а token (SALT2) в базата данни е "0.2579033621663531", то ние най-официално изпратихме заявка до базата данни:

SELECT id FROM users
WHERE user = 'pencho'
AND pass = SHA2(CONCAT('sdagfdjkgf24243Iv@ansSecPa$$',
CAST(token AS CHAR)),256)";

Тъй като хакера знае token за потребител pencho, то възпроизвеждането на низа става елементарно:
sdagfdjkgf24243Iv@ansSecPa$$0.2579033621663531
Така виждаме, че човек с достъп до СУБД всъщност има достъп до низа SALT1. А това е напълно против нашата философия за SALT1! Със сигурност загубихме времето t1 и сведохме атаката до стандартната защита със SALT2.

Да "разбъркаме" ли SALT1 с password по различен начин? - не е добро решение

При всички положения "SALT1password" НЕ е правилният начин за добавяне на "сол от първи тип" към паролите. Конкатенацията в началото или в края не води до значителна защита на данните - много лесно се изолира кое е "солта" и кое е паролата.

Някои хора се досещат, че може не просто да долепят низовете един до друг, но и да ги "разбъркват", да изместват символите в ASCII таблицата или изобщо да се опитат да скрият чрез бърз, но наивен алгоритъм начинът, по който са "смесени" SALT1 и password. За съжаление дори да направим това ние не правим много - това, което му трябва на "хакера" получил този низ е да провери как са миксирани данните при своя собствен акаунт (за който той знае своята собствена парола). Тоест едва ли ще му е трудно да открие начинът, по който сме го направили, ако сме подходили толкова елементарно.

Развивайки идеята си по-напред можем да достигнем до написването на наш собствен алгоритъм за симетрично криптиране или хеш алгоритъм. А защо? Дали той ще е по-труден за пробиване отколкото наличните? Малко вероятно е. Затова - НЕ, "разбъркването" на SALT1 с паролата с някой наш собствен алгоритъм НЕ е добро решение, освен ако не сме наистина гениални математици и програмисти!

Да "разбъркаме" и SALT2? - не по-добро от лошото решение

Преминаваме "една идея напред" и решаваме да разбъркаме SALT1 със SALT2 и с паролата, но пак няма да имаме голям успех. Той знае SALT2 и паролата за своя си акаунт, следователно задачата му не е много по-трудна от тази в миналата точка... Наистина дали ще търси начина за миксиране между SALT1 с един низ или SALT1 и два познати низа реално се свежда до една и съща задача.

Изобщо смисълът на SALT2 е съвсем друг - да принудим хакера да прави отделни brute force атаки към всеки отделен потребител и да не може да се възползва от готова rainbow таблица, т.е. по този начин увеличаваме драстично необходимото време за атака. SALT2 не може да участва в защитата на SALT1, защото е видим и е наличен от страната на вече приетата за компрометирана база от данни.

Да хешираме паролата и SALT преди да ги изпратим към базата данни - добро решение

С други думи вместо

$pass = SALT.$pass;

да ги хешираме:

$pass = hash('ripemd160', SALT.$pass);

и така полученият хеш код да се изпрати към втория хеш алгоритъм към базата данни (с долепен SALT2 към него естествено):

$sql = "SELECT id FROM users WHERE user = '$user' ";
$sql .= "AND pass = SHA2(CONCAT('$pass',CAST(token AS CHAR)),256)";
$result = mysqli_query($link, $sql);

По този начин реално заявката придоби следният вид:

hashedpass = HASHCODE1 ( HASHCODE2('SALT1password' + 'SALT2')

където последователността и мястото на извършване на операциите за хеширане е много важна! Трябва да отбележим следното:

  • HASHCODE1 и HASHCODE2 е добре да са два различни хеш алгоритъма. По този начин разчитаме на сигурността на два различни алгоритъма, вместо на един единствен, т.е. "пробиването" на единия не компрометира цялата система.
  • HASHCODE2 се прилага от приложението, а НЕ от СУБД. Към СУБД предаваме само хешираните от HASHCODE2 данни, а SALT1 не го предаваме в чист вид към никакви външни канали.

Така постигнахме относително доста по-високо ниво на защита. За съжаление продължаваме да имаме недостатъка на SALT1, който имахме в началото - прочитането на сорс кода на програмата разкрива SALT1 в чист вид, което обезсмисля направеното действие с допълнителното хеширане. В случаят спечелихме само в това, че от страната на базата данни SALT1 не е наличен и задължихме хакера да го търси с offline brute force атака (от примерния код - срещу ripemd 160 хеш алгоритъм).

Да хешираме паролата и SALT с HMAC преди да ги изпратим към базата данни - също добро решение

Идеята е много сходна с предишната - вместо

$pass = SALT.$pass;

да използваме някой хеш алгоритъм с hmac с ключ SALT, например:

$pass = hash_hmac('ripemd160',$pass, SALT);

и така полученият хеш код да предадем към SQL сървъра, както го правихме преди:

$sql = "SELECT id FROM users WHERE user = '$user' ";
$sql .= "AND pass = SHA2(CONCAT('$pass',CAST(token AS CHAR)),256)";
$result = mysqli_query($link, $sql);

Този подход е много сходен с предшния, да не кажем направо, че е същия. В случая от използването на HMAC не печелим нищо от гледна точка на сигурността. Даваме този пример, защото често се дава в примерни кодове по различни сайтове. HMAC алгоритмите имат съвсем различно предназначение. Повече за HMAC прочетете тук.

При този метод продължаваме да имаме недостатъка при пробив с прочитане на сорс кода на приложението.

Симетричното криптиране - също добро решение

Вместо хеш алгоритъм можем да използваме (задължително силен) алгоритъм за симетрично криптиране - например AES256. Ето как трябва да променим примерния сорс код:

Файл salt.php (намира се в директория /…/htdocs/includes/):

<?php
	define('SALT', 'adsa34343SFhsjfdsF');
	// Допълнителен произволен низ за initialization vector
	define('IV', 'p9erRikVm123');
?>

Файл checklogin.php (намира се в директория /…/htdocs/includes/):

...
// Вместо "$pass = SALT.$pass;"
$pass = openssl_encrypt($pass,'aes-256-cbc',SALT,IV);
...

След което, както преди, правим конкатенацията със SALT2 и хешираме с SHA256 със заявка към базата от данни, по същия начин както го правихме преди. Естествено трябва да сме записали паролата в базата данни криптирана и хеширана по същия начин (това важеше и за предишните два метода). С този метод продължаваме да имаме недостатъка, че SALT1 ще стане известен при прочитане на сорс кода на приложението.

Резултати:

Доколко си помогнахме с използването на един от последните три метода? При "пробит" хеш алгоритъм може евентуално да се изолира хешираният/криптираният $pass от SALT2 за някоя парола. Същото важи и при подслушването на трафика или поемането на контрол над СУБД - там тривиално ще се изолира хешираният/криптираният $pass от SALT2. Тази хеширана/криптирана парола обаче все още не върши никаква работа! Тя трябва да бъде декриптирана, за да може да бъде използвана. И по-точно трябва да бъде налучкан (предполагаемо възможно само чрез bruteforce) SALT1, за да може впоследствие да се правят нови brute force атаки и то за всеки един потребител поотделно (заради наличието на SALT2). В крайна сметка добавихме следните ползи към приложението:

  • Защитата ни не зависи от един единствен алгоритъм за хеширане, а вече зависи от два различни хеш алгоритъма или от един хеш алгоритъм и един за симетрично криптиране. По този начин частично се подсигуряваме от евентуални открити сериозни проблеми с използвания основен хеш алгоритъм в бъдеще.
  • Каквото и да става със сигурността на основния хеш алгоритъм, ние категорично си гарантирахме нуждата от минимално време t1 за brute force атака срещу SALT1.

Или по друг начин казано - ако хакерът има достъп само до базата данни (включително до цялото СУБД), то ние си гарантираме, че той ще трябва минимално да отдели време t1, за да пробие паролите ни. Все още не сме решили проблема със защитата на SALT1, който стои явно в сорс кода на приложението. Какво да направим ако друг вътрешен човек, като например програмист работил по приложението, еветуално се сдобие с ценния низ SALT1? Тогава ние продължаваме да разчитаме на основната защита, а именно хеш алгоритъм приложен върху паролата с конкатениран SALT2, което би отнело време t2 за акаунт. В крайна сметка си гарантирахме времето t1 + N.t2 за "хакване" на N акаунта, като вече t1 не зависи от t2 и обратно. А нека припомним, че и двете сами по себе са си дълги времеви интервали, особено ако се използва множествено криптиране (т.е. допълнително забавяме всяка една операция от brute force атаката и така правим хеш алгоритмите бавни)!

Виждате, че вътрешнофирмената сигурност на една подобна система силно зависи от явното разделение и стриктен контрол на достъп между сорс кода на приложението и базата от данни. Двете трябва да си комуникират чрез сигурен и строго контролиран интерфейс.

Наистина "мерките за сигурност" дискутирани в настоящата статия са малко "параноични". Истината в реална практическа ситуация е, че е напълно достатъчно да подсигурим низовете си само със SALT2 и да разчитаме на времето t2 средно за всеки един акаунт. Тази допълнителна защита със SALT1 обаче не отнема кой знае колко процесорно време, а добавя още значително голямо допълнително време t1. Нищо не пречи да се възползваме, но трябва да го правим така, че да има смисъл да го правим, а не да се "самокомпрометираме" още в самото начало, както беше в примера от предишната статия.

 



5 коментара


  1. Мен също много ме вълнуват въпросите със сигурността на уеб приложенията. За себе си не съм намерил достатъчно надежден метод, да се защитят паролите на потребителите. Ето какви са слабите места, които могат да компрометират паролите:

    1. http комуникация - прихващане на паролата от клиента към сървъра през интернет. Тук помага https, но не винаги е възможен.

    2. Прихващане на комуникацията между приложението и сървъра за базата данни;

    3. Не-оторизиран достъп до базата данни;

    Утежняващо обстоятелство е, че напоследък работя само по проекти с отворен код, което означава, че кода и заложените в него алгоритми са достъпни на всички.

    Солта, заложена в конфигурационните файлове, е недостъпна за повечето хора, но администратора и евентуално хакер осигурил си шел достъп я знаят.

    Според мен (без да съм сигурен, че това е най-правилния начин) трябва да направим следното:

    1. От клиента към сървъра паролите да не се предават в чист вид, а да се предава хешът им със:
    - сол предадена от сървъра;
    - таймстамп предаден от сървъра и поддържан от клиента;
    - хеша трябва да е поне 1000 пъти приложена функция от вида на MD5 или SHA-1;

    2. Да не се използват функциите на SQL сървъра за хеш/криптиране. Всичко да става в приложението.

    3. В базата данни да се записват хешове на хешовете получени от клиента и конкатинирани със сол от конфигурацията + сол пер потребител. Хеширането тук трябва да бъде приложено поне 10 000 пъти.

    Многократното хеширане има смисъл на нова хеш-функция, която се изпълнява по-бавно от обичайната хеш функция. Понеже това се прави еднократно при логване, забавяне от около 0.5 секунди е приемливо, а в същото време, забавя атаките, разчитащи на брутална сила.

    До сега не съм намерил приемлива схема за защита на системата от администратор, преминал на тъмната страна. Ако той има достъп до SQL сървъра, той би могъл да прочете данните, (например цените на ключовите клиенти) без да се налага да разбива някаква парола. Понеже в реалния живот подобна защита има смисъл, ще се радвам, ако някои сподели идеи, как да защитим базата данни по такъв начин, че хем всичко да работи, хем потребител с достъп до нея, да не може да дешифрира информацията, която тя съдържа и още по-малко пък - да я модифицира. Разбира се, в подобна база трябва да вървят SQL заявките, за да работи системата.

    P.S. Чета блога и се възхищавам на един друг БГ програмист, който разбива пароли, като прави милиони хешове в секундата, чрез програмиране на ниско ниво и подбор на хардуер: http://www.gat3way.eu/index.php

  2. Здравей Милен,

    Проблемът с предаването на паролите от клиента към сървъра на този етап е решен фундаментално с SSL.

    Това, което предлагаш в точка 1 може да се направи именно с HMAC алгоритъм за хеширане (key се предава от сървъра към клиента, а клиента хешира паролата, предава я хеширана към сървъра, а сървъра имайки оригиналния ключ може да генерира същия хеш при себе си и да ги сравни). Това обаче не решава Man in The middle атаките, подслушването на трафик (съответно session hijacking) и т.н. Изобщо напълно безсмислено е да слагаш бронирана входна врата с множество сложни ключалки, ако се окаже, че някой може елементарно да влезе през прозореца.

    Относно многократното хеширане - да, това е често използвана практика и трябва да се прави. Много популярни приложения се възползват от нея. Трябва да се използва внимателно и прецизно - наистина увеличавате драстично необходимото време за bruteforce, но също така драстично увеличавате необходимото време за изпълнение на собствения си сървър. Ресурсите на системата не са нещо, което трябва да бъде за пренебрегване. Да, бихте могли да защитите данните в базата данни като направите 8000+ поредни криптирания (т.е. brute force атаката ще изисква 8000+ пъти извикване на хеш функцията за всеки опит), но какво ще направите срещу DDoS атака срещу вашия сървър? Имам предвид такъв "denial of system", който претоварва процесора на системата ви, а не толкова запълване на трафичните данни. Ако в един момент се опитат да се логнат 100 души, то ние ще изпълняваме 800 000+ хеш функции едновременно, вместо 100. Неприятно, нали? Така, че тук вече набъркваме много други фактори и в общи линии отиваме към цяло ново поле от проблеми.

    Не, че "не трябва да се ползва тази техника", напротив - трябва. Но винаги много внимателно и с добро планиране и оптимизиране.

    Това "да не използваме SQL функцията на базата данни, а само на приложението" е важно дотолкова, доколкото да не допуснете грешката, която дискутирах в тази статия. А именно - да не изпращате данни към базата от данни, които не трябва да изпращате. Иначе няма нищо лошо в тези функции. Когато хеширам със SALT2 даже ще предпочета да го правя чрез СУБД вместо от приложението (иначе трябва първо да чета token, след това да хеширам, след това да правя SQL заявка за login - 2 SQL заявки вместо 1).

    Накрая за "администраторите на тъмната страна" - мноооого, даже най-сложния въпрос. Досега и аз не съм имал досег до система, която да е решила този проблем напълно, но съм чувал легенди, че евентуално имало някакви такива :)

    Най-простият (може би) метод е цялата комуникация между приложение и база от данни да преминава през строго контролиран интерфейс с много силно ограничен достъп, като само той се грижи за криптиране и декриптиране на информацията (вкл. на важната информация от базата данни) - така СУБД администраторите и програмистите нямат достъп до "тайните" и щетите се свеждат до минимум. Отделно от това много тежък quality control, "разделяй и владей" за всички служители и т.н. и т.н. Изобщо "беля работа".

    И накрая, но не на последно място, в горната статия е даден пример с дооооста ниска ентропия на SALT2. Доколко това е проблем - за това ще пиша по-късно.

  3. Здравей Филип,
    Благодаря за отговора. Аз също смятам, че SSL е единственото масово достъпно решение в момента, което на теория и на практика подсигурява комуникацията между клиент и сървър. Достъпността обаче изисква приложенията да могат да работят и без https поддръжка. Man in The middle е много неприятно нещо, но за щастие изисква "Man", а то няма много такива;) По-честия случай, който наблюдавам е "Bot in The middle", като този сценарии има известни разлики. Бота прихваща комуникацията, като прави или автоматичен анализ (но органичен) в момента или я съхранява за по-късен анализ от човек. При това положение, доброто кодиране на паролите може да е полезно, понеже от прихваната информация, най-лошото което може да се случи е да бъдат възстановени паролите.

    Относно: многократния хеш
    Като гледам разни системи, той е почти стандарт при криптиране на пароли. Напълно съм съгласен, че не трябва да се харчат ресурсите на сървъра, но логването не е чак толкова честа операция.
    Според мен, всичко трябва да се оразмери спрямо хардуера и потребителите. Ако в една бизнес организация има до 500 потребителя, спокойно можем да режем всеки опит над 5-тия за логване, в рамките на 3 секунди, като можем да очакваме, че няма да създадем негативни емоции в потребителите.

  4. Аз не знам да съществува съвременен интернет браузър, който да НЕ поддържа SSL... Може би такива се срещат по разни мобилни телефони, но в модерните пак - едва ли. Разбира се, че част от "frontend" на приложенията няма смисъл да се криптира - публичната информация винаги се предоставя некриптирана. Но личните данни - задължително трябва да се защитават. Освен ако приложението е такова и пазените данни са тикива, че "да не ни пука".

    Това, което наричаш "Bot in the middle" всъщност е финалният етап на автоматизиране след извършен анализ от "Man". Да, вярно е, че атаката трябва да е целенасочена. И, че само за стандартните приложения си има стандартни ботове. Но кой каза, че нашето приложение няма да бъде атакувано целенасочено? Например от "админчето", което ти доставя интернета вкъщи (а ти си администратор на важно приложение).

Добави коментар

Адресът на електронната поща няма да се публикува


*