C, PHP, VB, .NET

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


* Offline bruteforce атаки – как да се справим с тях?

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

Обновено 26.07.2019 г. с добавени примери за хеширане с Argon2id

Досега в статиите съм коментирал главно начини за справяне с атаки чрез rainbow таблици в допълнение с малко obscurity. Демонстрираната техника най-схематично изглеждаше така:

  1. Клиентът изпраща паролата си в чист текст към сървъра през POST по SSL криптирана връзка;
  2. Сървърът получава паролата и я криптира използвайки статично записания много дълъг низ SALT1. Това е слаба защита и е по-скоро obscurity, отколкото security. Криптирането наприрмер може да е хеш алгоритъм като SHA256 или симетрично криптиране като AES256-cbc – няма особено значение;
  3. В базата от данни сме записали крайния хеш на паролата (нека го кръстим REALHASH) и token – SALT2, който е дълъг и по възможност произволен низ със силна ентропия (в показаните примери досега това не е така – просто генерирам 10 цифрено число с RAND(), което въобще не е нещо с голяма ентропия – ще бъде потенциален проблем ако имаме милиони потребители). Взимаме криптирания низ от т.2, добавяме SALT2 до него и цялото това нещо го минаваме през хеш алгоритъма (например SHA256), с което получаваме хеша, който ще се тества – да му дадем име COMPUTEDHASH. Него сравняваме с REALHASH и ако съвпадат – паролата е вярна.

Схематично за проверка на паролата имаме сравнение COMPUTEDHASН == REALHASH, където например:

COMPUTEDHASH = SHA256(SHA256(SALT1+password)+SALT2)

Горната последователност прецаква напълно rainbow таблиците, т.е. с предварително записани бази с hash-reduction вериги от пароли и хешове няма да могат да ви атакуват. При това няма да има смисъл създадват нови – дори да знаят SALT1 (нашето obscurity, което ако го нямат, например ако се пази на отделна машина до която не са добили достъп, първо трябва да го намерят с brute force) нашият SALT2 гарантира, че няма да има универсална таблица с хешове, с която да се атакуват всички потребители. Ще трябва да се прави отделна за всеки потребител, а това е безсмислено – ще е по-добре да се прави brute force атака.

Така, че дотук с тези SALT1 и SALT2 (които обикновено се наричат „salt and pepper“) сложихме край на атаките с rainbow таблиците. Лошото е, че не сме сложили край на проблемите.

Проблем 1. Ниската ентропия на RAND()

В MySQL функцията RAND() генерира псевдослучайно число с плаваща запетая и ентропията оптимистично е 32 бита. Или 1 колизия при 232 = 4294967296 случайни числа. Тоест вместо 10 е било по-хубаво да отделя 11 цифри за произволното число. С малко математически сметки свързани с „birthday paradox“ от теория на вероятностите ще открием, че това означава приблизително 50% шанс за колизия при 77429 потребителя. Това не е много голямо число (сравнено например с предполагаемата 256 битова ентропия на SHA256). И освен това стъпваме на оптимизма, че ентропията наистина е 32 бита. Не знам как точно е реализирана функцията RAND, но се съмнявам, че е толкова. За да потвърдя съмненията си направих следния тест:

DELIMITER |
CREATE PROCEDURE RandCollisionsCheck()
BEGIN
   DECLARE calculations BIGINT;
   DECLARE randnum INT(10);
   DECLARE hit INT(10);

   SET calculations = 0;

   CREATE TEMPORARY TABLE tmptbl(
      num INT(10) NOT NULL
   )ENGINE=Memory;

   label1: LOOP
    SET randnum = FLOOR(RAND()*999999999);
    SELECT num INTO hit FROM tmptbl WHERE num = randnum;
    IF (hit IS NULL)
    THEN
      INSERT INTO tmptbl(num) VALUES(randnum);
      SET calculations = calculations + 1;
      ITERATE label1;
    ELSE
      SELECT "Collision!", calculations;
      LEAVE label1;
    END IF;
END LOOP label1;
END
|
DELIMITER ;

Колкото и пъти да изпълнявах процедурата, винаги даваше колизия при под 10 000 опита. Това е доста слаба ентропия – доста под тази, която очаквахме, ако функцията беше написана идеално.

Доколко това е проблем? За малки бази от данни (малко потребители) не е проблем. Ако обаче имате много потребители (Фейсбук, Туитър, Амазон, ЕBay и т.н.) това определено Е проблем. Причината е, че отново rainbow таблиците се връщат в играта. Ако имате например един милион потребителя, то (ако моя benchmark е верен) на всеки 100 ще има колизия на SALT2. Ако сте хакер, може би вече си заслужава да правите rainbow таблица – с една таблица ще „убивате по 100 заека“ и като цяло може би вече ще е по-ефективно от brute force на всеки един поотделно (зависи от големината на таблицата разбира се) :)

В MySQL има функция RANDOM_BYTES за генериране на произволни поредици с голяма ентропия, но в MariaDB (до 10.4 със сигурност) няма. Най-лесното решение е да спрем да генерираме произволния SALT2 в MySQL и да се преместим в PHP, където има доста повече възможности и различни библиотеки.

Първото, за което можем да се сетим, е че да използваме аналогичната функция mt_rand() няма да е от кой знае каква полза. Тя пак е с оптимистична ентропия от 32 бита и пак сигурно е доста по-малка (да не пиша код сега, но можете да си направите сами тестови сценарии за проверка чрез експеримент).

Решението е естествено да се използва значително по-сигурна библиотека. Аз например винаги съм използвал openssl_random_pseudo_bytes – функция, която генерира X на брой случайни байта. Ако например искаме да създадем SALT2 с (оптимистична) ентропия например 32 байта (предишните използвани бяха в „бита“!), ще направим следното:

<?php
echo bin2hex(openssl_random_pseudo_bytes(32));
?>

Използването на bin2hex е с цел да направим генерираната поредица от битове „SQL safe“. Ще записваме полученото в колона от тип BINARY(32) като ще го подаваме в SQL функция UNHEX(…).

Проблем 2. Бързината на алгоритъма

Дотук категорично се справихме с атаките с rainbow таблици. Нищо не казахме обаче за добрия стар „brute force“. Идеята за него е ясна – хакерът е добил нашата база от данни и започва да налучква. Ако нашето obscurity е сработило (SALT1 не е налично за лошия човек), хакерът ще трябва да има свой потребител в базата и да приложи „грубата сила“ първо върху него. Целта му е да налучка произволния стринг и евентуално начинът, по който сме миксирали паролата с него (от примера по-горе SHA256(SALT1+password)). Той знае своята собствена парола, знае и SALT2, т.е. от тук нататък времето за намиране на SALT1 е пред него. Ако пък хакерът е пробил достатъчно добре системата и е взел и SALT1 наготово, направо прескачаме към следващия параграф.

След като знае всички SALT-ове и знае алгоритъма за тяхното миксиране, за хакера следва класическата проба/грешка. Взима криптираната парола на избран потребител и знаейки неговия SALT2, той започва да пробва една по една пароли – било то bruteforce, бито то dictionary attack – в общи линии трябва да се съобрази с т.нар. „password policy“ на системата, която атакува (какви пароли от колко до колко и какви символи позволява тя). Тази поредица от проба/грешка рано или късно ще му даде желания резултат.

И сега да си зададем въпроса – това, което направихме достатъчно сигурно ли е, за да не може хакера да разбие паролата ни в обозримо бъдеще? Отговорът е категорично НЕ. Причината за това е… прекалено бързото криптиране, което правихме досега. SHA256 е един доста бърз алгоритъм. И особено с видео карти като ето тези:

cards-2или цели „ферми“ от видео карти като ето тази:

cards
разбиването на така запазената от нас парола може да се окаже, че хич не е сложно и не отнема много време. В едно изследване на tarsnap от 2009г. с тогавашния моделен хардуер се получават следните резултати:

hardware-cost

Доколко достоверна е таблицата няма да се заема с твърдение, но е факт, че модерен процесор към днешна дата може да генерира десетки хиляди MD5 хешове в секунда, а видео карта – и много повече (някой ако е правил тест, ще помоля да даде съвременни данни :P). Ще кажете „да, ама примера е с MD5 – алгоритъм от 1991г., а ние използваме SHA256 – доста по-модерен“. И ще сбъркате. SHA256 не е значително по-бавен от MD5. Горе долу 4 пъти по-бавен е. И паролите ви много бързо ще бъдат разбити на съвременна „ферма“ от видео карти. Да не говорим за специализиран хардуер – например тези, които произвеждат за mining на bitcoins :)

Първото нещо, което можем да направим, е да потърсим още „по-тежък“ алгоритъм за хеширане. Например SHA384/SHA512 са значително по-тежки за видео карти, защото използват 64 битови int операции (съответно към този момент 32 битовите видео карти извършват повече действия за да изпълнят операциите). Новият SHA3 (във всичките си варианти) също използва 64 битови операции и е добър избор. При CPU обаче значима разлика в скоростта няма. А и при GPU все още става достатъчно бързо. Тоест простото еднократно хеширане не е достатъчно независимо от алгоритъма, който използвате (поне от добре познатите такива)!

Решението на този проблем е т.нар. „key stretching“. Идеята е да усложним алгоритъма за хеширане до много бавен такъв, така че „фермата от видео карти“ на хакера да не може да се справя за секунди/минути/часове, а да ѝ трябват поне години, десетки години или най-добре – поне столетие.

Най-простият метод за постигане на key stretching е множествено хеширане. Ако например искаме да забавим хакерът 40 000 пъти, то можем да направим следното (PHP-подобен псевдокод):

$password = ...; // паролата в plain text
$salt1 = ...; // нашето obscurity
$salt2 = ...; // истинският salt срещу rainbow таблици
$computedhash = hash('sha256', $salt1.$password.$salt2);
for ($i = 0, $i< 40000, $i++){
  $computedhash = hash('sha256', $computedhash.$password.$salt2);
}

Тук веднага можем да си кажем – дайте да затрудним генерирането на хеш милиарди пъти! Тук ще достигнем до неприятен проблем – нашият сървър също ще бъде натоварен милиарди пъти пъвече! От едно място нататък ние не можем да го правим, защото системата или ще стане много бавна за потребителите или пък ще стане уязвима към Denial Of Service атаки.

Друго нещо, което можем да направим, е да изберем алгоритъм, който е принципно по-бавен и най-добре по възможност – неудобен за съвремения хардуер (пиша го визирайки видео картите). Пример за неудобен за съвременните видео карти алгоритъм е Whirlpool заради 64 битовите си операции. Sha384 и sha512 също са добър (вероятно даже по-добър) избор.

В PHP може да се възползвате да постигнете горното и чрез готовата функция crypt (тя обаче генерира до 128 битови хешове (ако това е от значение за вас – колизиите все пак се увеличават) по следния начин:

$password = "fd";
$salt1 = ...; // Нашето obscurity
$salt2 = ...; // уникален SALT за потребителя
$computedhash = crypt($salt1.$password, '$5$rounds=40000$'.$salt2.'$');

Генерираният хеш код все пак ще показва някаква информация за алгоритъма – ще даде нещо като следното:

$5$rounds=40000$YpxfOqqcAIYxQSnP$.H1O37nImPiK1Tv4NHPFNpr0vsVJzA2fq0ZG88Txji5

където 5 означава SHA256, 40000 е броя итерации, YpxfOqqcAIYxQSnP в моя случай е SALT2 (генериран по подобие от по-горе, но взети точно 16 символа, което е и максимума за crypt функцията) и края на този голям низ е самият хеш (забележете, че е по-къс от стандартното SHA256 – просто са отрязани първите 128 бита). Аз лично предпочитам сам да си правя цикъла и да си пазя ентропията на хеш алгоритмите :)

Горните примери имат и още един проблем – итерационното забавяне чрез множествено криптиране е уязвимо от т.нар. „transferable state attack“. При тази атака се прескачат много от изчисленията в хеш алгоритъма (прескача се от изхода на предишната операция директно в „transform“ метода на следващата) и се получава между 10 и 20% забързване. Такава атака впрочем е демонстрирана при SHA256. Въпреки това от практиката ще забележите, че техниката с итерационния метод все пак се използва често. Проблемът с тази атака, освен ако не се намерят още по-тежки оптимизации, засега не е голям.

Готови алгоритми – пример с BCrypt

Може да подходите и по-традиционен начин и да се доверите на нарочно направени за целта алгоритми като PBKDF2, BCrypt или (казват) най-добре SCrypt. В практиката най-често се използват PBKDF2 и BCrypt. PBKDF2 като цяло се счита за по-слабо, защото използва стандартен алгоритъм за хеширане, което като правило е бързо на GPU – ако го използвате, наблегнете на SHA384/SHA512, което както писах по-горе използва операции върху 64 битови цели числа и е неприятно за сегашните GPU. Bcrypt се възползва от друга техника, за да „прецаква“ видео картите – използва променлив 4KB масив в паметта – а това все пак е някакъв проблем за споделения кеш между ядрата на видеокартите, т.е. bcrypt ефективно затруднява разпаралеляването на изчисленията. Най-модерният алгоритъм от 2009 г. SCrypt добавя екстрата, че освен възможност за добавяне на итерации, позволява чрез параметър да се увеличи и заеманото количество памет (което към този момент това обезсмисля видео картите, които се продават на пазара и също така ограничава използването на евтини хардуерни устройства оптимизирани за операции свързани с генериране на хешове). Но като цяло идеята и на трите алгоритъма е, че те са адаптивни. Това означава, че с добавянето на параметър вие можете да ги забързвате или забавяте (нещо аналогично на увеличаване или намаляване на броя итерации в key stretching от по-горе).

Тук ще разгледам отгоре-отгоре BCrypt. Той вътрешно използва алгоритъм за симетрично криптиране BlowFish (много стар, а досега не е открита уязвимост, което е добър признак, че няма да бъде пробит). Ключът, който се генерира, е на базата на паролата и се добавя salt, който допълнително маскира нещата (може би в предишна статия писах няколко думи за AES256-cbc).

В PHP (съвременните версии след 5.5) bcrypt може да се имплементира съвсем лесно чрез функцията password_hash. Тя приема три параметъра – парола, алгоритъм и масив с настройки. Паролата разбира се е обикновен текст – това, което е подал потребителя, – но не трябва да е над 72 символа. Ако паролата е над 72 символа, тя се отрязва и се използват само първите 72 (връща ме във времената, когато в хостинг компанията APlus режеха паролите на SquirrelMail до 8мия символ и се получаваха интересни моменти, като например потребител с парола „passwordFDSKfsdgfdihIUHGRU“, която се свежда до… „password“). Внимавайте с това, защото ако използвате SALT1 в комбинация с паролата и SALT1 е прекалено дълго, можете да се озовете в капан. Ако използвате хеширане като например SHA256 (64 символа) пък набърквате колизиите в играта на паролите (не е сериозен проблем, но все пак го има). Затова ограничете максималната дължина на паролите на потребителите си например да не е повече от 40 символа  и добавете 32 символен SALT1 за да сте „on the safe side“. Или най-добре просто добавяйте SALT1 след, а не преди паролата :)

Алгоритъмът за криптиране може да е PASSWORD_DEFAULT или PASSWORD_BCRYPT… което е едно и също – на този етап няма други алгоритми в PHP. Не ви препоръчвам да използвате PASSWORD_DEFAULT, защото може в бъдещи версии на PHP да го сменят.

Колкото до асоциативният масив с опциите – той може да има два елемента: salt (ако не желаете автоматично генерирания) и cost (каква да бъде тежестта на криптирането). Много е удобно да използвате автоматично генерирания salt (което ще е еквивалент на нашия SALT2), защото е достатъчно сигурен.

В докемунтацията на PHP е даден следния пример за тестване на вашата система:

<?php
$timeTarget = 0.2; 
$cost = 9;
do {
    $cost++;
    $start = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
} while (($end - $start) < $timeTarget);
echo "Appropriate Cost Found: ".$cost;
?>

В променливата $timeTarget задавате за колко „микросекунди“ желаете да се генерира хеша. Можете да увеличите по ваше желание тази стойност (дадената е доста малка) и този benchmark ще ви покаже удачния „cost“ спрямо вашия хардуер. Не желаете прекалено бавно генериране на хеш (примерно от секунда), защото това ще натовари сървъра ви драстично и ще го направи уязвим към DoS атаки.

След като веднъж сте нагодили каква „цена“ може да отделите (по подразбиране 10), започвате да използвате функцията. Хешовете вече ще ги генерираме по следния начин (това е с автоматично генериран SALT2 по аналогията от по-горе):

password_hash("password".SALT1, PASSWORD_BCRYPT, ["cost" => 11]);

Както казах автоматично генерирания salt се съдържа вътре в генерирания изходен низ, който сам по себе си прилича на ето това:

$2y$11$QjSH496pcT5CEbzjD/vtVeH03tfHKFy36d4J0Ltp3lRtee9HDxY3K

„2y“ означава BlowFish, а „11“ е cost. Това вече го записвате в базата от данни. От тук нататък може да използвате функцията password_verify, за да проверявате парола за вярност:

$hash = ...; // четете го от базата от данни
if (password_verify("password".SALT1, $hash)) {
    echo 'Валидна парола!';
} else {
    echo 'Невалидна парола.';
}

Понеже $hash съдържа алгоритъма, cost и salt2, значи има всичко необходимо за функцията да направи повторно хеширане на паролата по същия начин както е била записана преди. Почти съм убеден, че в повечето (ако не и всички) примери свързани с BCrypt, които ще разгледате в други източници, не се прави стъпката с конкатениране на паролата със SALT1, а направо се подава паролата в чист вид. Причината е, че се смята, че това не е security, а както писах по-нагоре е obscurity. И познатата фраза е „do not use security through obscurity“. Аз обаче лично не съм съгласен с нея и по-точно – не съм напълно съгласен с нея. Правилната фраза трябва да е „do not use ONLY security through obscurity“. Добавянето на допълнителни (дори и да са дребни) пречки за хакера въобще не е лошо нещо. Важното е да не разчитате само и единствено на тях.

SCrypt

В много източници ще видите, че SCrypt се препоръчва като може би най-доброто решение. Този алгоритъм обаче се използва от много криптовалути и това впрегна много усилия към неговия анализ. Оказа се, че има сериозна уязвимост, която не прави алгоритъма лош и несигурен, но значително губи неговите предимства (невъзможността да се извършва в паралелни операции използвайки кеша на процесора поради многото заемана памет). С тази атака се прави „CPU-memory tradeoff“ – увеличават се операциите, но се намалява заеманата памет. Това е изгодно при кракването на пароли, защото искаме да се използва кеша на процесора – бърза памет, а не рам паметта – много по-бавна.

Или казано накратко – SCrypt е една чудесна идея, която за съжаление все още няма стабилна реализация. Поне на този етап нямате сериозна полза от него (пред BCrypt например). Може би е по-добре да заложите на сигурното.

Argon2 (i, d, id)

Argon2 е алгоритъм, който поема идеята на SCrypt и я развива до подходящ модел за запазване именно на пароли. При него има три параметъра:

  • Време
  • Памет
  • Степен на паралелизация (брой нишки за криптиране/декриптиране)

Argon2i е имплементация, която е устойчива на „side channel attacks“, Argon2d е устойчив срещу атаки с разпаралеляване, а най-новият Argon2id е опит (за сега считан за успешен) за комбинация от двата метода – наличен е в системните библиотеки на PHP 7.3.

Използването е изключително лесно – подобно на показаното с BCrypt. Можем да хешираме и да проверим хеша по следния начин:

$hash = password_hash('<PASSWORD>', PASSWORD_ARGON2ID, ['memory_cost' => 3907, 'time_cost' => 50, 'threads' => 1]);
if(password_verify('<PASSWORD>', $hash){ ... }

Тук memory_cost е в мерни единици kibibytes (3907 e малко над 4MB), time_cost e броят на итерациите на алгоритъма (колко пъти ще бъде повторен), а threads е броят нишки, които ще бъдат използвани (при уебсайт с много потребители няма смисъл от повече от една нишка – тъй или иначе различните потребители ще използват различни нишки и съответно процесора ще се утилизира достатъчно добре). В общи линии можете да повторите теста на системата спрямо time_cost, както беше показано за BCrypt по-горе. Единствено трябва да пресметнете колко памет може да си позволите да заделите за тези операции – не искате да си докарате потенциална DoS атака.

<?php
	$timeTarget = 0.2; 
	$time_cost = 10; 
	do { 
		$time_cost++; 
		$start = microtime(true); 
		password_hash('somepassword', PASSWORD_ARGON2ID, ['memory_cost' => 3907, 'time_cost' => $time_cost, 'threads' => 1]);
		$end = microtime(true); } 
	while (($end - $start) < $timeTarget); 
	echo "Appropriate Cost Found: ".$time_cost; 
?>

Самият формат на хешовете е подобен на следното:

$argon2id$v=19$m=3907,t=50,p=1$ZU5mUlRZUUk4R1h1WXY0OA$v2mOEhQCF19604GErMwxv3zML3nARQWRBVFo1r15egU

Виждате, че дължината му зависи основно от числата на променливите m, t и p, които подавате вие – хеша след тях ще е с фиксирана дължина. Така можете да подберете коректно и дължината на CHAR колоната в MySQL/MariaDB, в която ще ги запазвате.

Заключение

Дали ще използвате key stretching с хеш алгоритми или готови решения като BCrypt си е ваше решение – трудно е да се каже коя техника е по-добра. Ако ще използвате хеш алгоритъм с key stretching – погрижете се алгоритъма да е такъв, който е тежък за видео картите на пазара. SHA256 например не е добро решение – то използва 32 битови операции и генерирането на хешове на GPU е много бъпзо. По-добре се насочете към SHA384/SHA512, което леко усложнява живота на „кракерите“.

От гледна точка на bcrypt и неговия BlowFish – в алгторитмите за симетрично криптиране от една страна исторически е хвърлено много повече усилие от страна на учените и по презумция имат доста по-малко уязвимости спрямо хеш алгоритмите. Конкретно при този алгоритъм, поне на този етап – с тази техника на пазара, кракването на видео карта е по-бавно, отколкото обикновените хешове с key stretching. Затова ако използвате техника като BCrypt на този етап се предполага, че няма да сбъркате. PBKDF2 или ваш собствен цикъл с множествено хеширане също е достатъчно сигурно, стига да използвате хеш алгоритъм опериращ с 64 битови числа.

 



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

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


*