Аналіз витекли паролів Gmail, Yandex і Mail.ru

Зовсім недавно в публічний доступ потрапили бази паролів популярних поштових сервісів [1,2,3] і сьогодні ми їх проаналізуємо і відповімо на ряд питань про якість паролів і можливе джерело (або джерелах). Так само ми обговоримо метрики якості окремих паролів і всієї вибірки.

Не менш цікавими є деякі аномалії і закономірності баз паролів, можливо, вони зможуть пролити світло на те, що могло служити джерелом даних та наскільки дана вибірка є небезпечною з точки зору звичайного користувача.

Формально, ми розглянемо такі питання: наскільки надійними є паролі в базі і чи могли вони бути зібрані словникової атакою? Є ознаки фішингових атак? Чи Могла «витік» даних бути єдиним джерелом даних? Чи Могла ця база бути акумульована протягом тривалого періоду або дані виключно «свіжі»?

Структура статті
  1. Опис даних
  2. Невалідні паролі і не-паролі
  3. Розподіл довжини паролів
  4. Розподіл надійності паролів
  5. Словникова атака
  6. Топ паролів
  7. Вибірка Gmail
  8. Вибірка Rambler
  9. Аналіз відкритих джерел
  10. Висновок


Опис даних


Дані з усіх трьох баз являють собою набір пар адреса-пароль, розділені двокрапкою. Ніяких інших «мета-даних» недоступно. Однак дані досить зашумлені тобто в них присутні рядки не є адресами пошти, ні допустимими паролями.


Якщо ми досліджуємо особливості даних, то зможемо висунути (або спростувати) гіпотезу про те, в результаті якого процесу паролі могли бути отримані.

Невалідні паролі і не-паролі


Найбільш простий критерій невалидности пароля — невідповідність довжини пароля вимогам поштових сервісів.

Отримані дані говорять, що паролі з вибірки не могли бути отримані в результаті «внутрішній» витік, так як кілька тисяч паролів не є валідними паролями в принципі з-за обмежень на довжину пароля в шість символів (а для сучасних паролів gmail вісім символів).

Розглянемо ці аномально довгі (понад 60) і короткі паролі (менше 6) в деталях.

Приклади
Довгі паролі являють собою шматки HTML-коду, один з репрезентативних прикладів:

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

Коротка вибірка надто коротких паролів:


Ще один індикатор того, що одним з джерел міг бути фішинг — відсутність логіна і пароля в записах. Особливо цікаво виглядає апостроф без вказівки пароля. Можливо, потенційна жертва здогадалася про фішинг формі та спробувала перевірити наявність SQL ін'єкції.

Що ми можна однозначно стверджувати за перевіреними даними? Автоматичної валідації бази не відбувалося. Найбільш вірогідні гіпотези: фішинг і зараження вірусом.

Для того, щоб оцінити якість всієї вибірки, ми видалимо з неї завідомо невірні паролі довжини менше 6 і більше 60 і розглянемо розподіл в цілому за кількома параметрами.

Розподіл довжини паролів


Як видно з графіка нижче, більша частина паролів має довжину 8 або менше символів. Що може вказувати на те, що суттєвий пласт паролів потенційно нестійкий до різного виду атак переборних атак.


Розподіл надійності паролів


Для того, щоб перевірити цю гіпотезу, розглянемо просту метрику надійності пароля засновану на
стандарті PCI.
Нехай за задоволення однієї з наступних умов пароль отримує умовний бал:
  • пароль містить не менш 7ми символів;
  • пароль містить хоча б одну рядкову букву;
  • пароль містить хоча б одну прописну букву;
  • пароль містить хоча б одну цифру;
  • пароль містить хоча б один символ.
Якщо пароль отримує 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]).

Із таблиці вище видно, що істотна частка паролів міститься у словниках, що так само вказує на те, що частина паролів могла бути отримана в результаті словникової атаки (або якоїсь модифікації перебору).

Топ паролів


Наведемо добірку найбільш популярних паролів і зауважимо, що більша частина зараз не є допустимими паролями.


Вибірка Gmail


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

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

Вибірка Rambler


Нескладно виявити в мережі списки «дійсно валідних» адрес, складених широким колом зацікавлених осіб (ака кулхацкеры).

Що цікаво, серед них досить великий відсоток адрес рамблера.

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("invalid number of 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("Дуже\пслабый", "Слабкий", "Середній", "Надійний", "Дуже\пнадежный"))
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



Висновок


Таким чином найбільш вірогідною виглядає гіпотеза, що дана вибірка — компіляція різних джерел (фішинг, зараженні, словниково-переборні атаки, збори популярних збірок) протягом тривалого періоду часу. Достатня частина даних в принципі не є валідними паролями за формальними синтаксичним критеріями, що також підтвердила експериментальна перевірка.

З точки зору користувача дана подія не несе суттєвої небезпеки і швидше виглядає спробою створення инфоповода.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.