20
Большинство пользователей до сих пор ставят себе на аккаунты простые пароли — комбинации даты рождения, имени любимого футболиста, клички питомца, не подозревая, что вся эта информация находится в открытом доступе. Используя ее, брутфорсер может значительно облегчить себе задачу угона аккаунтов. Чтобы этому противодействовать, вводят антибрутфорс (АБФ). Как оценить качество его работы? Как вычислить взломщика-брутфорсера и отделить его вредоносные запросы от легитимных? В этой статье я расскажу, каким образом эти задачи были решены у нас в компании.
Очень часто, вместо того чтобы ставить в систему сложный АБФ, админы ограничиваются простым правилом: если количество неудачных запросов за определенный период превышает заданное значение, то надо заблокировать все запросы с этого IP на некоторое время или вывести капчу. Возможно, для каких-нибудь довольно простых сайтов такое решение действительно будет работать, но, когда речь заходит о проектах с большим числом пользователей, могут возникнуть проблемы.
Выставлять длительные блокировки по IP-адресам не будет хорошим решением: взломщик может этим воспользоваться и заблокировать авторизацию сразу для большого числа пользователей, просто меняя IP-адрес пакетов.
Использовать капчу — вариант не для всех: чем чаще ее показываешь, тем больше затрудняешь пользование сайтом. Поэтому на некоторые запросы, например в мобильных приложениях, ее могут просто не ставить. И, на мой взгляд, очень важно пресекать попытку брутфорса именно на этапе обращения к серверу авторизации.
Если решишь использовать у себя стороннее, продвинутое решение, то надо убедиться, что оно правильно сконфигурировано для работы именно с твоей системой. Если этого не сделать, результат может получиться не лучше, чем у простого правила с рейт-лимитами, а ресурсов для своего содержания такой АБФ будет требовать в разы больше.
Для АБФ очень важно найти компромисс между простотой, качеством и ограничениями, накладываемыми на удобство пользования.
Приведу пример одной типичной проблемы, с которой можно столкнуться при конфигурации. Рассмотрим следующее правило: если за определенное время t отношение числа успешных авторизаций к их общему числу меньше, чем q, а общее число запросов с этого IP больше, чем p, считаем, что с этого адреса идет брутфорс. Если посмотреть внимательнее, оно представляет собой простое дерево решений (рис. 1).
Рис. 1. Дерево решений
Здесь каждому IP-адресу соответствует точка на плоскости, а описанное выше правило будет разбивать саму плоскость таким образом, как показано на рис. 2.
Рис. 2. Разбиение пространства признаков
Основанный на таком правиле АБФ будет отлавливать лишь тех пользователей, что попадут в правый нижний угол. Опытным путем можно определить границу решающих правил: находясь на ней, получится брутить более эффективно, чем внутри зеленой области.
Если быть более точным, то оптимально такое соотношение n_requests / success rate, которое попадает в вершины красной области (рис. 2). Чем ближе к этим вершинам будет работать брутфорсер, тем эффективнее получатся его результаты.
Поэтому при конфигурации такого АБФ параметром оптимизации будет центральная вершина квадрата. Ее расположение определяет работу всей системы. Если она окажется слишком низко, то мы дадим большую свободу брутфорсерам; если же слишком высоко, то вполне можем урезать нормальных пользователей. Такой трейдоф типичен для систем АБФ.
При неправильной настройке он практически не мешает брутить. Здесь, например, брутфорсер может использовать несколько своих аккаунтов для успешной авторизации, чтобы поднять свой success rate до нужного уровня, что в итоге позволит ему увеличить общее число запросов.
Рис. 3. Выбор разделяющей границы
Конечной целью конфигурации системы АБФ будет построение такой разделяющей границы, которая пройдет как можно ближе к нормальным пользователям и при этом сделает саму попытку брутфорса как можно более бессмысленной (затратной по времени и ресурсам).
Если с этим примером все довольно просто, то как в общем случае понять, какие значения параметров оптимальны? Какой процент брутфорсеров пропускает система и сколько нормальных пользователей она блокирует? Если каждый день к тебе поступают несколько миллионов запросов, которые выливаются в сотни мегабайт логов, разобраться с этим вручную невозможно. Придется либо использовать готовое решение, либо писать свое, но полностью исключить участие человека в анализе будет непросто — в результате может получиться аналогичная система АБФ, может, чуть лучше или хуже.
Так как мало что сравнится по эффективности с ручным анализом, я предлагаю от него не отказываться, а сделать более удобным с помощью некоторых алгоритмов машинного обучения. Можно заставить программу выполнить большую часть вычислений, аналогичных тем, что проводит АБФ, выделить группы похожих между собой пользователей, а потом для каждой такой группы вручную расставить метки.
Если быть более точным, я предлагаю провести анализ в несколько этапов:
- собрать данные, доступные о пользователях системы за некоторый промежуток времени;
- провести кластеризацию пользователей на основе этих данных (выделить группы наиболее похожих по поведению пользователей);
- вручную определить, в какие кластеры попали брутфорсеры;
- сделать выводы на основе полученных результатов.
Есть несколько возможных вариантов использования этих результатов, но о них я расскажу позже, пока перейдем к первому этапу — сбору данных.
Сбор данных о пользователе¶
Конечно, хотелось бы сразу разделить всех пользователей на две группы — плохих и хороших, но зачастую все не так просто. Есть пользователи, которые заходят из-под NAT’а, крупных сетей или пользуются одним прокси-сервером. Общее количество запросов с таких адресов будет намного выше, чем у пользователей с выделенным адресом; соответственно, и число неудачных авторизаций будет больше. Всех их надо отличить от брутфорсеров. Если нет достаточного количества параметров, сделать это довольно непросто: пользователи будут сильно переплетаться между собой и будет сложно отделить одних от других. Поэтому чем больше данных мы соберем о конкретных пользователях, тем лучше.
Посмотрим, что можно узнать из данных, которые есть в любом логе по авторизациям:
- дата и время авторизации;
- ID аккаунта;
- IP-адрес;
- результат авторизации (success/fail).
На их основе для каждого IP-адреса можно посчитать следующую статистику:
- сколько часов в день с конкретного IP было хотя бы несколько попыток авторизации (нужно выбрать небольшой порог, чтобы отбросить стороннюю активность, например пять-десять запросов);
- среднее время между попытками авторизации (у брутфорсеров оно, вероятнее всего, будет меньше, чем у любых других пользователей);
- общее количество попыток авторизации;
- отношение successess / (successess + fails);
- соотношение регулярно используемых аккаунтов — для каждого аккаунта, посещенного с данного IP, сравнить количество успешных авторизаций с количеством неуспешных, и если оно больше, то взять 1, иначе 0. Посчитать отношение суммы к общему числу посещенных аккаунтов. Такой признак должен работать против тех брутфорсеров, которые держат несколько своих аккаунтов и время от времени в них заходят, пытаясь таким образом «очистить» себя от предыдущего показателя.
Далее приведу список параметров, которые также могут быть полезными при анализе, но могут отсутствовать в логах некоторых систем.
- User-Agent пользователя. Очень часто брутфорсеры пользуются готовыми тулзами, поэтому можно детектить запросы тех программ, где этот параметр захардкожен.
- Страна пользователя. Можно посмотреть, насколько отличается целевая аудитория посетителей сайта от того, что есть на самом деле. Любые отклонения от нормы будут служить неплохим признаком.
- Некоторые признаки можно также извлечь из числовых аккаунтов, где в качестве логина используется набор цифр. Такие аккаунты могут быть интересны, если у них красивый номер. Понятие красивости можно посчитать как количество повторяющихся цифр в числе. Для численного выражения можно использовать отношение количества повторяющихся цифр к количеству цифр в номере либо считать энтропию Шеннона — чем она меньше, тем номер красивее.
- Если в запросе можно указывать числовой идентификатор пользователя, то брутфорсер может последовательно перебирать пользователей по их ID, используя при этом какой-нибудь распространенный пароль, в надежде, что он подойдет хоть к какому-нибудь аккаунту. На основе такого поведения можно сделать еще один параметр детекта.
- Любые другие данные тоже могут оказаться кстати. Если найдется еще пара-тройка параметров, их тоже можно добавить. Перед использованием их нужно привести в числовой вид. Например, категориальные параметры, типа языка, страны и подобное можно просто пронумеровать.
Кластеризация¶
INFO¶
Кластеризация — это метод поиска закономерностей в наборе данных, суть которого заключается в разбиении этого набора на однородные группы (кластеры). Внутри таких кластеров данные максимально похожи между собой и значительно отличаются от элементов извне.
Для более точных результатов кластеризации и актуальности результата лучше всего собрать эти данные за довольно небольшой промежуток (семь-десять дней).
Соотношение брутфорсеров в каждый отдельный момент будет примерно одинаково, что позволит понять картину происходящего. Информация о других адресах может со временем зашумляться, поэтому если брать данные за более длительный период, то в результатах кластеризации может оказаться много мусора.
По этой же причине, чтобы убрать лишний шум, перед началом кластеризации лучше провести стандартизацию данных, приводя среднее и дисперсию по каждому параметру к 0 и 1 соответственно. Это сделает данные менее привязанными к длительности промежутка, внутри которого их рассматривали. На Python это можно сделать в две строчки:
scaler = preprocessing.StandardScaler().fit(data_set)
data_set = scaler.transform(data_set)
Здесь и далее я планирую использовать Python 2.7 в качестве основного языка для примеров. Мне он кажется интуитивно понятным, для работы необходимо установить библиотеки numpy и sklearn. Полный код примера можно посмотреть на github.com.
Для кластеризации будем использовать алгоритм DBSCAN. С хорошими примерами и аналогиями сам алгоритм описан тут. Если вкратце, то он основан на распределении точек в пространстве: чем плотнее друг к другу они расположены, тем вероятнее, что они попадут в один кластер. Ключевыми параметрами алгоритма являются eps и min_pts.
Принцип работы алгоритма DBSCAN
Для каждой точки пространства алгоритм вычисляет ее соседей — точки, находящиеся от нее на расстоянии не больше, чем eps. Если таких точек больше, чем min_pts, то ее считают корневой (core). Если же точка — сосед корневой, но в ее окрестности меньше, чем min_pts, соседей, то ее будем считать граничной (border). Две точки p1 и p2 будем считать достижимыми, если найдется такая последовательность корневых точек, которая их соединяет. Каждая точка в такой последовательности должна быть соседом предыдущей.
Таким образом, все точки пространства распадаются на группы достижимых точек, внутри которых любые две точки будут достижимы. Все точки, которые не попали ни в одну группу, будем считать шумом (noise).
Для выбора значений min_pts и eps нет готовых рецептов. При выборе их значений я ориентировался на количество нераспределенных точек, стараясь его уменьшить, при этом получив умеренное количество кластеров.
На данный момент существует много более продвинутых алгоритмов кластеризации, таких как OPTICS, однако здесь я решил использовать именно DBSCAN из-за его простоты, хороших практических результатов и приемлемой реализации на Python. В принципе, его можно заменить на любой другой алгоритм, который больше подходит для твоей конкретной задачи, это не должно никак повлиять на другие этапы анализа.
При большом объеме данных для их кластеризации может потребоваться много оперативной памяти. В этом случае можно попытаться использовать KD-деревья в качестве структуры для хранения данных. Это можно сделать, просто задав нужный параметр в алгоритме:
clusterer = DBSCAN(eps=0.4, min_samples=30,algorithm='kd_tree')
clusterer.fit(data_set)
Если это не помогло, можно взять рандомную подвыборку данных, то есть выбрать какое-то случайное подмножество данных, например 30–40% от их общего количества, полагая, что от этого конечные результаты не сильно изменятся.
n = len(data_set)
reduction_rate = 0.4
data_indexes = [i for i in xrange(0, n) if rnd.random() < reduction_rate]
reduced_data = data_set[data_indexes]
Если ты использовал лишь часть данных, то имеет смысл каким-то образом распределить оставшиеся. Для этой цели можно взять какой-нибудь классификатор, например Random Forest. Обучить его на уже размеченных данных, а потом предсказать результат для оставшихся.
rf_clf = RandomForestClassifier(n_estimators=100, n_jobs=8)
rf_clf.fit(reduced_data,reduced_data_labels)
all_labels = rf_clf.predict(data_set)
Для визуализации результатов кластеризации многомерных данных можно использовать алгоритм TSNE. С его помощью можно сократить размерность данных до двух, сохранив при этом относительное расположение кластеров. Подробное описание этого алгоритма читай здесь.
Результаты кластеризации, визуализированные с помощью TSNE
Анализ полученных результатов¶
На основе полученной кластеризации определим, в какие из кластеров могли попасть брутфорсеры. Для этого можно сделать частичную выборку из исходных данных, соответствующих каждому полученному кластеру. Такой набор данных уже можно легко обработать вручную — кластеров, по идее, будет не так много, а просмотреть по 10–15 экземпляров каждого вида не должно занять много времени.
Проведя такого рода анализ, можно установить, есть ли на сайте брутфорсеры, какую долю пользователей они составляют, какое количество запросов производят.
Используя Random Forest, можно сразу после обучения выделить Feature Importance — набор признаков, которые наиболее важны для разделения групп пользователей. Обучив RF в режиме один ко многим, когда брутфорсеров ставят в противовес всем остальным пользователям, можно получить тот набор признаков, которые хорошо детектят брутфорс.
## Метки кластеров, в которые попали брутфорсеры
bruteforce_labels = set([0,5])
train_y = []
for i in xrange(0, n):
if all_labels[i] in bruteforce_labels:
train_y.append(0.0)
else:
train_y.append(1.0)
rf_clf = RandomForestClassifier(n_estimators=100, n_jobs=8)
rf_clf.fit(data_set,train_y)
feature_importances = sorted(zip(feature_names,rf_clf.feature_importances_),
key=lambda x: x[1], reverse=True)
for feature, importance in feature_importances:
print "%s имеет значимость в %f процентов" %(feature,100*importance)
Чем больше значимость признака, тем он более важен для отделения брутфорсеров от остальных пользователей. Топовые признаки позволяют выявить наиболее проблемные места в текущем АБФ. Не все признаки, которые здесь использовались, легко вычисляются в режиме онлайн, к тому же некоторые из них могут не учитываться вашим АБФ. Это не всегда будет проблемой — один раз выделив брутфорсеров, можно обучить классификатор на меньшем числе параметров, и, если информации, содержащейся в них, будет достаточно для определения брутфорсеров, можно брать значимость уже оттуда.
На основе полученных данных также можно выделить аккаунты, которые были угнаны: для этого нужно посмотреть, в какие аккаунты успешно авторизовались и обычные пользователи, и брутфорсеры. Здесь будет два варианта:
- «Обычными пользователями» окажутся брутфорсеры, которые зашли под другим IP.
- Аккаунт действительно был взломан.
В любом случае можно принять некоторые меры для повышения безопасности — отправить пользователям письмо с просьбой сменить пароль либо заблокировать подозрительные аккаунты.
В результате мы смогли определить, есть ли в системе брутфорсеры, выявить самые слабые места АБФ, составить список аккаунтов, которые могли быть сбручены.
Конечно, предложенный подход не отличается простотой или изяществом, однако мне он кажется довольно гибким. Тут нет какого-то черного ящика, на каждом этапе ясно, что и как ты делаешь с данными, и это позволяет легко интерпретировать результаты. Можно добавлять и убирать некоторые этапы анализа, менять используемые алгоритмы и смотреть, что получается. В этом же заключается и главный минус — его нельзя просто использовать «из коробки», придется потратить время, подбирая нужные параметры, которые дадут оптимальный результат. Это больше исследовательская работа. Но, если в результате получится улучшить работу своего АБФ и сделать сайт более защищенным, она определенно стоит своего времени.