Дэвид Лебланк - 19 смертных грехов, угрожающих безопасности программ
Ниже приводится криптографически стойкая версия:
...static string IterateHashAppendSalt(string uid, string pwd, UInt32 iter)
{
// границы числа итераций
const UInt32 MIN_ITERATIONS = 1024;
const UInt32 MAX_ITERATIONS = 32768;
// ограничить переданное значение параметра для безопасности
if (iter < MIN_ITERATIONS) iter = MIN_ITERATIONS;
if (iter > MAX_ITERATIONS) iter = MAX_ITERATIONS;
// получить 24-байтовое начальное значение (затравку)
const UInt32 SALT_BYTE_COUNT = 24;
byte[] salt = new byte[SALT_BYTE_COUNT];
new RNGCryptoServiceProvider().GetBytes(salt);
// закодировать имя и пароль
byte[] uidBytes = UTF8Encoding.UTF8.GetBytes(uid);
byte[] pwdBytes = UTF8Encoding.UTF8.GetBytes(pwd);
UInt32 uidLen = (UInt32)uidBytes.Length;
UInt32 pwdLen = (UInt32)pwdBytes.Length;
// скопировать uid, pwd и salt в буфер (массив байтов)
byte[] input = new byte[SALT_BYTE_COUNT + uidLen + pwdLen];
Array.Copy(uidBytes, 0, input, 0, uidLen);
Array.Copy(pwdBytes, 0, input, uidLen, pwdLen);
Array.Copy(salt, 0, input, uidLen + pwdLen, SALT_BYTE_COUNT);
// вычислить хэш uid, pwd и salt
// H(uid || pwd || salt)
HashAlgorithm sha = HashAlgorithm.Create("SHA256");
byte[] hash = sha.ComputeHash(input);
// вычислить хэш от результата первого хэширования, начального
// значения и номера итерации N раз
// R0 = H(uid || pwd || salt)
// Rn = H(Rn-1 || R0 || salt || i) ... N
const UInt32 UINT32_BYTE_COUNT = 32/8;
byte[] buff = new byte[hash.Length +
hash.Length +
SALT_BYTE_COUNT +
UINT32_BYTE_COUNT];
Array.Copy(salt, 0, buff, hash.Length + hash.Length, SALT_BYTE_COUNT);
Array.Copy(salt, 0, buff, hash.Length, hash.Length);
for (UInt32 i = 0; i < iter; i++) {
Array.Copy(hash, 0, buff, 0, hash.Length);
Array.Copy(BitConverter.GetBytes(i), 0, buff,
hash.Length + hash.Length + SALT_BYTE_COUNT,
UINT32_BYTE_COUNT);
hash = sha.ComputeHash(buff);
}
// построить строку вида base64(hash) : base64(salt)
string result = Convert.ToBase64String(hash) + ":" +
Convert.ToBase64String(salt);
return result;
}
Но даже эта версия уязвима для атаки! В чем же уязвимость? Пусть, например, имя и пароль пользователя сворачиваются в строку «xE/fl/XKonG+/XFyq+ Pg4FXjo 7g=», и вы включаете ее в состав URL в качестве доказательства того, что верительные грамоты были проверены. Противнику нужно лишь увидеть эту свертку и воспроизвести ее. Пароль ему знать вовсе необязательно! Вся эта «навороченная» криптография оказалась не стоящей и ломаного гроша! Исправить это упущение помогут такие технологии шифрования канала, как SSL, TLS или IPSec.
Противник предсказывает данные
В этом случае пользователь заходит на сайт, вводя свое имя и пароль по шифрованному соединению (SSL/TLS), сервер проверяет их и генерирует автоинкрементное значение для представления данного пользователя. В ходе дальнейшего взаимодействия с пользователем используется именно это значение, чтобы не проходить каждый раз всю процедуру аутентификации. Такую схему легко атаковать даже при наличии SSL/TLS. И вот как это делается. Настоящий, хотя и злонамеренный, пользователь соединяется с сервером и предъявляет свои верительные грамоты. Он получает от сервера идентификатор 7625. Затем он закрывает браузер, открывает его снова и входит с тем же именем и паролем. На этот раз он получает идентификатор 7267. Похоже, что для каждого нового пользователя идентификатор увеличивается на единицу, причем между двумя его заходами вошел кто–то еще. Чтобы перехватить чужой сеанс (защищенный протоколом SSL/ TLS!), противнику остается лишь задать идентификатор равным 7266. Технологии шифрования не защищают от такого рода предсказаний. Но вы можете установить идентификатор соединения равным криптографически случайному числу. На языке JavaScript для этого можно воспользоваться СОМ–объектом CAPICOM:
...var oRNG = new ActiveXObject(«CAPICOM.Utilities»);
var rng = oRNG.GetRandom(32, 0);
Примечание. CAPICOM вызывает функцию Windows CryptGenRandom.
При использовании PHP в ОС Linux или UNIX (в предположении, что система поддерживает специальное устройство /dev/random или /dev/urandom) можно написать такой код:
...// наличие @ перед fopen не дает fopen вывести излишне много
// информации пользователю
$hrng = @fopen("/dev/random", "r");
if ($hrng) {
$rng = base64_encode(fread($hrng,32));
fclose($hrng);
}
И на языке Java:
...try {
SecureRandom rng = SecureRandom.getInstance("SHA1PRNG");
byte b[] = new byte[32];
rng.nextBytes(b);
} catch (NoSuchAlgorithmException e) {
// Обработать исключение
}
Примечание. Стандартная реализация класса SecureRandom в Java обладает очень небольшим энтропийным пулом. Для управления сеансами и опознанием пользователей в Web–приложениях этого, может быть, и достаточно, но для генерирования долгосрочных ключей маловато.
Но с непредсказуемыми случайными числами связана одна потенциальная проблема: если противник может увидеть данные, то ему достаточно сохранить случайное число и воспроизвести его! Чтобы предотвратить такую возможность, можете зашифровать канал по протоколу SSL/TLS. Но опять же это зависит от конкретной угрозы.
Противник изменяет данные
И наконец, предположим, что вам наплевать на то, что противник может увидеть данные, но изменять их он не должен. Это как раз проблема «цены в скрытом поле». Вообще–то вы должны избегать подобных решений, но если по какой–то странной причине другого выхода нет, то можете поместить в форму код аутентификации сообщения (message authentication code – MAC). Если МАС–код, возвращенный браузером, отличается от того, что вы послали, или вообще отсутствует, то данные были изменены. Можете рассматривать МАС–код как свертку секретного ключа и данных. Чаще всего для вычисления свертки применяется алгоритм хэширования НМАС. Вам нужно лишь конкатенировать значения всех скрытых полей формы (или любых полей, которые вы хотите защитить) и свернуть результат с ключом, хранящимся на сервере. На С# код выглядит так:...HMACSHA1 hmac = new HMACSHA1(key);
byte[] data = UTF8Encoding.UTF8.GetBytes(formdata);
string result = Convert.ToBase64String(hmac.ComputeHash(data));
А на Perl – так:
...use strict;
use Digest::HMAC_SHA1;
my $hmac = Digest::HMAC_SHA1->new($key);
$hmac->add($formdata);
my $result = $hmac->b64digest;
В PHP функции HMAC нет, но в архиве PHP Extension and Application Repository (PEAR) она имеется. (См. ссылку в разделе «Другие ресурсы».) Результат вычисления МАС–кода можно включить в скрытое поле формы:
...<INPUT TYPE = HIDDEN NAME = «HMAC» VALUE = «X8lbKBNG9cVVeF9+9rtB7ewRMbs»>
Прочитав значение из поля HMAC, сервер может проверить, были ли изменены скрытые поля, для чего достаточно повторить операции конкатенирования и сворачивания.
Не используйте для этой цели функции хэширования. Применяйте МАС–коды, поскольку противник может повторить вычисление хэша. Заново же вычислить НМАС, не зная секретного ключа, невозможно.
Дополнительные защитные меры
Никаких дополнительных защитных мер не требуется.
Другие ресурсы
□ Раздел о скрытых полях в спецификации W3C HTML: www.w3.org/TR/ REC–html32#fields
□ « Practical Cryptography» by Niels Ferguson and Bruce Schneier (Wiley, 2003), §6.3 «Weaknesses of Hash Functions»
□ PEAR HMAC: http://pear.php.net/package/Crypt_HMAC
□ «Hold Your Sessions: An Attack on Java Session–Id Generation» by Zvi Gutter–man and Dahlia Malkhi: http://research.microsoft.com/~dalia/pubs/GM05.pdf
Резюме
Рекомендуется
□ Проверяйте все данные, поступающие из Web, в том числе и посредством форм, на признаки злоумышленности.
□ Изучите сильные и слабые стороны применяемого вами подхода, если вы не пользуетесь криптографическими примитивами.
Не рекомендуется
□ Не встраивайте конфиденциальные данные в HTTP–заголовки и в HTML–страницы, в том числе в URL, куки и поля форм, если канал не шифруется с помощью таких технологий, как SSL, TLS или IPSec, или данные не защищены криптографическими средствами.
□ Не доверяйте никаким данным в Web–форме, поскольку злоумышленник может легко подставить любые значения вне зависимости от того, используется SSL или нет.
□ Не думайте, что приложение защищено, коль скоро вы применяете криптографию: противник может атаковать систему другими способами. Например, он не станет угадывать случайные числа криптографического качества, а просто попытается подсмотреть их.
Грех 10. Неправильное применение SSL и TLS
В чем состоит грех
Протокол Secure Sockets Layer (SSL – протокол защищенных сокетов), а равно пришедший ему на смену Transport Layer Security (TLS – протокол защиты транспортного уровня) – это два наиболее популярных в мире протокола защиты сетевых соединений. SSL широко используется в браузерах для обеспечения безопасности электронной торговли. Он применяется для защиты сети и во многих приложениях, не предназначенных для работы в Web. На самом деле, говоря о безопасности, программисты часто имеют в виду именно протокол SSL.
Примечание. Для краткости мы будем считать, что аббревиатура SSL обозначает оба протокола: SSL и TLS.
Программные API, поддерживающие SSL, обычно заменяют традиционную абстракцию ТСР–сокета, соединяющего две точки, понятием «защищенного соке–та» (отсюда и название протокола). Это означает, что SSL шифрует трафик, проверяет целостность сообщений и предоставляет каждой стороне возможность аутентифицировать партнера.
SSL, на первый взгляд, прост. Для большинства программистов он выглядит как прозрачная замена одних сокетов другими. При этом еще надо добавить простую начальную аутентификацию, работающую по защищенному соединению, и вроде бы все. Однако за этой кажущейся простой скрывается несколько проблем, и некоторые из них весьма серьезны. Самое главное – это то, что надлежащая аутентификация сервера обычно не выполняется автоматически. Для этого нужно написать довольно много кода.