- PVSM.RU - https://www.pvsm.ru -
Совсем недавно в публичный доступ попали базы паролей популярных почтовых сервисов [1 [1],2 [2],3 [3]] и сегодня мы их проанализируем и ответим на ряд вопросов о качестве паролей и возможном источнике (или источниках). Так же мы обсудим метрики качества отдельных паролей и всей выборки.
Не менее интересными являются некоторые аномалии и закономерности баз паролей, возможно, они смогут пролить свет на то, что могло служить источником данных и насколько данная выборка является опасной с точки зрения обычного пользователя.
Формально, мы рассмотрим следующие вопросы: насколько надежными являются пароли в базе и могли ли они быть собраны словарной атакой? Есть ли признаки фишинговых атак? Могла ли «утечка» данных быть единственным источником данных? Могла ли данная база быть аккумулирована в течении длительного периода или данные исключительно «свежие»?
Структура статьи:
Данные из всех трех баз представляют собой набор пар адрес-пароль, разделенные двоеточием. Никаких других «мета-данных» недоступно. Однако данные достаточно зашумленные т.е. в них присутствуют строки не являющимися ни адресами почты, ни допустимыми паролями.

Если мы исследуем особенности данных, то сможем выдвинуть (или опровергнуть) гипотезу о том, в результате какого процесса пароли могли быть получены.
Самые простой критерий невалидности пароля — несоответствие длины пароля требованиям почтовых сервисов.

Полученные данные говорят, что пароли из выборки не могли быть получены в результате «внутренней» утечки, так как несколько тысяч паролей не являются валидными паролями в принципе из-за ограничений на длину пароля в шесть символов (а для современных паролей gmail в восемь символов).
Рассмотрим эти аномально длинные (более 60) и короткие пароли (менее 6) в деталях.
Длинные пароли представляют собой куски HTML-кода, один из репрезентативных примеров:

Подобные примеры указывают, что одним из источников паролей мог быть фишинг. Запись в базе явно не была проверена человеком и получена автоматически, на фишинг так же указывает тот факт, что в пароле присутствует html-разметка, что довольно нетипично для кражи пароля через заражение.
Краткая выборка слишком коротких паролей:

Еще один индикатор того, что одним из источников мог быть фишинг — отсутствие логина и пароля в записях. Особенно интересно выглядит апостроф без указания пароля. Возможно, потенциальная жертва догадалась о фишинговой форме и попыталась проверить наличие SQL инъекции.
Что можно однозначно утверждать по проверенным данным? Автоматической валидации базы не происходило. Наиболее вероятные гипотезы: фишинг и заражение вирусом.
Для того, чтобы оценить качество всей выборки, мы удалим из неё заведомо неверные пароли длины меньше 6 и больше 60 и рассмотрим всё распределение в целом по нескольким параметрам.
Как видно из графика ниже, большая часть паролей имеет длину в 8 или менее символов. Что может указывать на то, что существенный пласт паролей потенциально неустойчив к различному виду атак переборных атак.

Для того, чтобы проверить эту гипотезу, рассмотрим простую метрику надежности пароля основанную на
стандарте PCI [14].
Пусть за удовлетворение одного из следующих условий пароль получает условный балл:
Если пароль получает 4/5, то мы называем его надежным (очень надежным за 5/5), соответственно 3/5 назовем средним, а 2/5 слабым (0 или 1 балл назовем очень слабым). Код на языке R приведен ниже.
library("Hmisc")
strength <- function(password){
# must contain at least 7 characters
score = 0
if (nchar(password) >= 7){
inc(score) <- 1
}
# at least one digit
if(grepl("[[:digit:]]", password)){
inc(score) <- 1
}
# at least one lowercase letter
if(grepl("[[:lower:]]", password)){
inc(score) <- 1
}
# at least one uppercase letter
if(grepl("[[:upper:]]", password)){
inc(score) <- 1
}
# at least one special symbol
if(grepl("[#!?^@*+&%]", password)){
inc(score) <- 1
}
# 0-1 very weak
# 2 - weak
# 3 - medium
# 4 - strong
# 5 - very strong
return(score)
}
Тогда распределение надёжности имеет вид:

Как видно из графика большинство паролей попадают в категорию не-надежные. В качестве примера рассмотрим пароли нулевой надёжности, так как скорее всего это ещё один репрезентативный пример невалидных паролей.

Как видно из примеров выше, данные пароли не являются валидными (и с точки зрения человека выглядят скорее ошибкой ввода, чем действительным паролем), так как почтовые сервисы не дают зарегистрировать ящик, если считают пароль слишком простым, например, повторением одного и того же символа шесть раз. А значит, что возможно ещё больший пласт паролей не является валидным согласно современным требованиям.
Возможно, что существенная часть базы собрана в течении длительного периода времени, когда требования к паролям были мягче? Иначе довольно сложно объяснить столь большую группу паролей, не соответствующих требованиям современных почтовых систем.
В качестве дополнительного аргумента проведем следующий эксперимент: возьмём выборку релевантных словарей паролей из общего доступа, проведем атаку на доступные пароли по этим словарям и оценим какой процент паролей содержится в этой выборке словарей (автор буквально не уходил дальше первых трёх ссылок гугла по запросу [password dictionary]).

Из таблицы выше видно, что существенная доля паролей содержится в словарях, что так же указывает на то, что часть паролей могла быть получена в результате словарной атаки (или какой-то модификации перебора).
Приведем подборку наиболее популярных паролей и заметим, что большая часть сейчас не является допустимыми паролями.

Действия и данные, описанные и полученные в данной и следующей части, были произведены и переданы другом моего друга, пожелавшего остаться неизвестным.
Задача: проверить валидность (т.е. что пароль действительно подходит) паролей. Действие: по небольшой выборке из ~150-200 попробовать получить доступ к ящикам. Из всей выборки в принципе валидными являются ~2-3% (через несколько часов появления данных в открытом доступе), и фактически все являются деактивированными на момент проверки. Реально действующими являлись менее 1% ящиков и те заброшены владельцами по крайней мере в течении года.
Несложно обнаружить в сети списки «действительно валидных» адресов, составленных широким кругом заинтересованных лиц (ака кулхацкеры).

Что интересно, среди них довольно большой процент адресов рамблера.
Rambler был предупрежден за несколько дней до публикации и был получен ответ, что необходимые меры безопасности будут приняты в ближайшее время.
<юмор>
</юмор>
Что интересно, процент валидных паролей существенно выше и до последнего времени rambler был вне медийного поля событий и не активизировал дополнительных систем безопасности.
Это позволило неизвестному антропологу утечки оценить последние моменты жизни почтовых ящиков. Несмотря на валидность паролей, все ящики являлись заброшенными в течении долгого времени (~1-1.5 года) и заканчивались одним из подобных писем:

Что является еще одним подтверждением гипотезы о фишинге и кумулятивной природе базы.
Вернемся к рассмотрению открытых источников. Активный поиск по паролям-логинам, привел нас к ряду раздач с геймерских форумов:

Оказывается, что часть списка уже в какой-то форме гуляла по сети.
Таким образом данные позволяют отвергнуть гипотезу о единственном источнике данных таком как «внутренняя утечка».
Основная часть используемого кода:
source("multiplot.R")
source("password_strength.R")
library("ggplot2")
print("loading yandex data")
yandex <- read.csv("yandex.txt", header = FALSE, sep = ":", quote = "", stringsAsFactors = FALSE)
print("loading mailru data")
mailru <- read.csv("mail.txt", header = FALSE, sep = ":", quote = "", stringsAsFactors = FALSE)
print("loading gmail data")
gmail <- read.csv("gmail.txt", header = FALSE, sep = ":", quote = "", stringsAsFactors = FALSE)
##testing if data loaded correctly
print("testing, if loaded correctly")
print(head(yandex))
print(head(mailru))
print(head(gmail))
##changing names
names(yandex) <- c("email", "password")
names(mailru) <- c("email", "password")
names(gmail) <- c("email", "password")
print("computing lengths of passwords and adding to the datasets")
yandex$pass_length <- sapply(yandex$password, nchar)
mailru$pass_length <- sapply(mailru$password, nchar)
gmail$pass_length <- sapply(gmail$password, nchar)
print("number of invalid passwords by length")
print(nrow(yandex[yandex$pass_length < 6,]))
print(nrow(yandex[yandex$pass_length > 60,]))
print(nrow(mailru[mailru$pass_length < 6,]))
print(nrow(mailru[mailru$pass_length > 60,]))
print(nrow(gmail[gmail$pass_length < 6,]))
print(nrow(gmail[gmail$pass_length > 60,]))
print("removing invalid passwords by length")
yandex <- subset(yandex, pass_length >= 6 & pass_length <= 60)
mailru <- subset(mailru, pass_length >= 6 & pass_length <= 60)
gmail <- subset(gmail , pass_length >= 6 & pass_length <= 60)
#print("checking that they are removed")
print(nrow(yandex[yandex$pass_length < 6,]))
print(nrow(yandex[yandex$pass_length > 60,]))
print(nrow(mailru[mailru$pass_length < 6,]))
print(nrow(mailru[mailru$pass_length > 60,]))
print(nrow(gmail[gmail$pass_length < 6,]))
print(nrow(gmail[gmail$pass_length > 60,]))
print("visualizing distribution of password lenghts by provider")
gmailcolor <- "deepskyblue"
yandexcolor <- "orangered1"
mailrucolor <- "limegreen"
pgmail <- ggplot(data=gmail, aes(x=pass_length)) + scale_x_discrete(limits=seq(6, 20, 1), breaks=seq(6, 20, 1), drop=TRUE) + geom_histogram(colour="black", fill=gmailcolor, aes(y=..density..)) + coord_cartesian(xlim=c(5,21.5)) + xlab(expression("Длина пароля"))+ ylab(expression("Доля"))+ggtitle("Gmail")
pyandex <- ggplot(data=yandex, aes(x=pass_length)) + scale_x_discrete(limits=seq(6, 21, 1), breaks=seq(6, 21, 1), drop=TRUE) + geom_histogram(colour="black", fill=yandexcolor, aes(y=..density..)) + coord_cartesian(xlim=c(5,21.5)) + xlab(expression("Длина пароля"))+ ylab(expression("Доля"))+ggtitle("Yandex")
pmailru <- ggplot(data=mailru, aes(x=pass_length)) + scale_x_discrete(limits=seq(6, 20, 1), breaks=seq(6, 20, 1), drop=TRUE) + geom_histogram(colour="black", fill=mailrucolor, aes(y=..density..)) + coord_cartesian(xlim=c(5,20.5)) + xlab(expression("Длина пароля"))+ ylab(expression("Доля"))+ggtitle("Mail.ru")
multiplot(pgmail, pyandex, pmailru, cols=3)
print("computing strength of the passwords")
yandex$strength <- sapply(yandex$password, strength)
mailru$strength <- sapply(mailru$password, strength)
gmail$strength <- sapply(gmail$password, strength)
print(head(yandex))
print(head(mailru))
print(head(gmail))
scale <- scale_x_discrete(limits=c(1,2,3,4,5), breaks=c(1,2,3,4,5), drop=TRUE, labels=c("Оченьnслабый", "Слабый", "Средний", "Надежный", "Оченьnнадежный"))
pgmail <- ggplot(data=gmail , aes(factor(strength))) + geom_bar(colour="black", fill=gmailcolor) + xlab(expression("Надежность"))+ coord + ylab(expression("Доля"))+ggtitle("Gmail") + scale
pyandex <- ggplot(data=yandex, aes(factor(strength))) + geom_bar(colour="black", fill=yandexcolor, binwidth=0.5) + xlab(expression("Надежность"))+ coord + ylab(expression("Доля"))+ggtitle("Yandex") + scale
pmailru <- ggplot(data=mailru, aes(factor(strength))) + geom_bar(colour="black", fill=mailrucolor, binwidth=0.5) + xlab(expression("Надежность"))+ coord + ylab(expression("Доля"))+ggtitle("Mail.ru") + scale
multiplot(pgmail, pyandex, pmailru, cols=3)
print("Zero strength passwords")
print("GMAIL")
print(head(gmail[gmail$strength == 0,]))
print("YANDEX")
print(head(yandex[yandex$strength == 0,]))
print("MAILRU")
print(head(mailru[mailru$strength == 0,]))
table_gmail <- sort(table(gmail$password) , TRUE)
table_yandex <- sort(table(yandex$password), TRUE)
table_mailru <- sort(table(mailru$password), TRUE)
print("gmail most frequent")
print(head(table_gmail, 100))
print("yandex most frequent")
print(head(table_yandex,100))
print("mailru most frequent")
print(head(table_mailru,100))
only_pass_gmail <- gmail[ ,2]
write.csv(only_pass_gmail, "only_pass_gmail", row.names = FALSE)
only_pass_yandex <- yandex[,2]
write.csv(only_pass_yandex, "only_pass_yandex", row.names = FALSE)
only_pass_mailru <- mailru[,2]
write.csv(only_pass_mailru, "only_pass_mailru", row.names = FALSE)
#!/bin/bash
data=sample_mailru
dict=saved_dict_mailru
> $dict
j=0
while read p; do
((j++))
echo -n $j
if grep -q "^$p$" dictionary/*; then
echo " in "
echo $p >> $dict
else
echo " out "
fi
if (("$j" > 10000)); then
break
fi
done <$data
Таким образом наиболее вероятной выглядит гипотеза, что данная выборка — компиляция различных источников (фишинг, заражение, словарно-переборные атаки, собрание популярных подборок) в течении длительного периода времени. Достаточная часть данных в принципе не является валидными паролями по формальным синтаксическим критериям, что также подтвердила экспериментальная проверка.
С точки зрения пользователя данное событие не несет существенной опасности и скорее выглядит попыткой создания инфоповода.
Автор: varagian
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/analiz-danny-h/69546
Ссылки в тексте:
[1] 1: http://habrahabr.ru/post/235949/
[2] 2: http://habrahabr.ru/post/236077/
[3] 3: http://habrahabr.ru/post/236283/
[4] Описание данных: #data
[5] Невалидные пароли и не-пароли: #valid
[6] Распределение длины паролей: #length
[7] Распределение надёжности паролей: #stength
[8] Словарная атака: #dictionary
[9] Топ паролей: #top
[10] Выборка Gmail: #sampleGmail
[11] Выборка Rambler: #sampleRambler
[12] Анализ открытых источников: #sources
[13] Заключение: #conclusions
[14] стандарте PCI: https://limoanywhere.uservoice.com/knowledgebase/articles/170461-what-are-the-password-requirements-for-pci-complia
[15] Источник: http://habrahabr.ru/post/236759/
Нажмите здесь для печати.