Изучаем параллельные вычисления с OpenMPI и суперкомпьютером на примере взлома соседского WiFi |
||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2018-03-13 23:47 Во время написания диссертации одним из направлением исследований было распараллеливание поиска в пространстве состояний на вычислительных кластерах. У меня был доступ к вычислительному кластеру, но не было практики в программировании для кластеров (или HPC — High Performance Computing). Поэтому прежде чем переходить к боевой задаче, я хотел поупражняться на чем-то простом. Но я не любитель абстрактных hello world без реальных практических задач, поэтому такая задача быстро нашлась.
Всем известно, что полный перебор является самым низкоэффективным способом подбора паролей. Однако с появлением суперкомпьютеров появилась возможность существенно ускорить данный процесс, поскольку, как правило, перебор параллелится практически без накладных расходов. Поэтому, теоретически, на кластере можно ускорить процесс с линейным коэффициентом, т.е. имея 100 ядер — ускорить процесс в 1000*k раз (где 0.0 < k <= 1.0). Так ли это на практике? Поэтому в качестве учебного упражнения и была поставлена задача это проверить. В качестве практической задачи выступила организация перебора паролей к WPA2 на вычислительном кластере. Поэтому в этой статье я опишу использованную методику, результаты экспериментов и выложу написанные для этого исходные коды. Стоит понимать, что практическая польза решаемой задачи стремится к нулю, поскольку реально никто не будет гонять огромные кластеры, прожигая электроэнергию стоимостью в сотни раз превышающую стоимость роутера и годовую абонентскую плату за интернет, ради того чтобы подобрать пароль к этому жалкому роутеру. Но если закрыть на это глаза и посмотреть на рейтинг суперкомпьютеров, то видно что топовые кластеры включают до 6 миллионов ядер. Это значит, что если на одноядерной машине некоторый пароль будет подбираться 10 лет, то такой кластер в случае сферического коня в вакууме разберется с ним за 10 365 24 60 60 / 6000000 = 53 секунды. Как работает WPA2 авторизация На эту тему в просторах сети существует достаточно ресурсов, поэтому я поясню совсем кратко. Предположим имеется точка доступа и клиентское устройство, находящееся рядом. Если клиент хочет установить соединение, то он инициирует последовательность обмена пакетами, в ходе которого он сообщает пароль точке доступа, а точка решает — предоставлять ему доступ или нет. Если они "договариваются", то соединение считается установленным. Чтобы злоумышленнику получить пароль авторизации, необходимо полностью прослушать обмен сообщениями между точкой доступа и клиентом, и далее подобрать хэш от определенного поля определенного пакета (так называемый handshake). Именно в этом пакете клиент и сообщает свой пароль. Но как застать процесс авторизации? Если предположить, что мы ломаем соседский WiFi, то надо либо ждать пока сосед выйдет с телефоном и вернется обратно, либо принудительно разорвать соединение. И такая возможность есть. Для этого нужно отправить в эфир запрос разрыва соединения, на которое в конце концов устройства отреагирует, и произойдет повторное подключение. Для пользователя этот процесс будет незаметен, а мы застанем процесс авторизации. Инструменты Сами пакеты можно поймать "особыми" WiFi точками, драйвера на которые есть в специализированном дистрибутиве Kali Linux, как в общем-то и все необходимые инструменты. На ebay или aliexpress можно найти сотни подходящих точек, а совместимость следует проверить на сайте ОС заранее (совместимость нужно проверять с использующимся чипом, на котором основана точка). Однако для данной работы наибольший интерес представляют собой инструменты для обработки handshake пакета и подбора пароля. Наиболее известным и продвинутым инструментом является aircrack-ng, у которого к тому же открыт исходный код. Нам он еще понадобится, но это позже. Однако все они рассчитаны на использование локального процессора либо видеокарты. Я не нашел подобного инструмента для запуска на суперкомпьютере, а значит велосипед мы не изобретаем и самое время написать его самому. Перебор по алфавиту Прежде чем что-то параллелить, нужно сделать смысловую часть — механизм перебора. Для этого введем понятия "алфавита", на основе которого осуществляется перебор. Алфавит представляет собой неповторяющееся множество всех символов, из которых может складывается пароль. Поэтому, если в алфавите N символов, то любой пароль может быть представлен как число в N-ной системе счисления, где каждая цифра соответствует символу с идентичным порядковым номером.
Поэтому создадим класс key_manager, который будет инициализироваться строковым алфавитом.
В нем понадобятся методы для конверсии от внутреннего представления (в числовой форме) к строковой форме и в обратную сторону. В обратную сторону потребуется конвертировать, если возникнет необходимость начать с какого-то заданного пароля, а не нулевого, например если необходимо продолжить поиск, а не начать сначала. При этом сами числе будем хранить в специальном классе, так называемом внутреннем представлении. Не хочется терять читаемость и делать его грамотно, поэтому сделаем его в виде вектора, где каждый элемент соответствует "цифре".
Порядковым идентификатором будет выступать целое число, возьмем в качестве примера 128-битный unsigned int.
Конечно, если делать совсем по-умному, то следует написать свой класс big_integer, но в рамках моих экспериментов в 128-битное целое все пароли укладывались, поэтому не будем тратить силы на то, что никогда не понадобится. Архитектура поискового движка Задачей поискового движка является перебрать диапазон ключей, сообщить найден ли правильный, и если да — вернуть найденный ключ. Но движку же все равно, для чего подбирать пароли — для WiFi или md5, поэтому спрячем детали реализации внутрь шаблона.
Внунтри данного метода напишем просто цикл, который линейно пройдется от first до last и если найдется подходящий ключ, то вернет true и запишет в correctKeyId найденный идентификатор.
Таким образом, становится ясно, каким должен быть интерфейс для класса, который уже "знает" для чего выполняется подбор пароля. В моем варианте я отлаживал этот класс на md5, прежде чем переходить на wpa2, поэтому в репозитории можно найти классы под обе задачи. Далее, сделаем, собственно сам checker. Проверяем пароль Начнем тоже с простой версии для подбора пароля под md5-хэши. Соответствуюший класс будет максимально простым, в нем нужен всего лишь один метод, в котором собственно и происходит проверка:
Для этого используется функция std::string md5(std::string), которая на основе заданной строки возвращает ее md5. Все максимально просто, поэтому теперь займемся подключением фрагментов aircrack-ng. Подключаем aircrack Первые сложности наступают в том, чтобы получить файл с handshake пакетом. Здесь я затрудняюсь вспомнить, как именно я его получал, но, кажется патчил airodump или aircrack, чтобы он сохранял нужный фрагмент. А возможно это делалось штатными средствами. В любом случае — в репозитории лежит пример такого пакета. Откинув все лишнее, для работы нам нужна следующая структура:
Которую из соответвтующего файла считать можно чтением нескольких фрагментов handshake-файла:
Далее, нужно выполнить некую предобработку этих данных, чтобы не повторять вычисления на каждый пароль (в конструкторе wpa2_crypter), но здесь можно особо не вдумываться, а просто перенести из aircrack. Самое интересное находится в aircrack/sha1-sse2.h, в которой есть функции calc_pmk и calc_4pmk, которые и выполняют полезные вычисления. Более того, calc_4pmk — это версия calc_pmk, которая при наличии в процессоре SSE2 позволяет посчитать за один этап аж 4 ключа, соответственно ускоряя процесс в 4 раза. Учитывая, что сейчас такое расширение есть почти на всех процессорах, его обязательно надо использовать, хоть это и добавляет небольшие сложности в реализацию. Для этого в наш класс wpa2_crypter мы добавляем буферизацию — поскольку brute_forcer будет запрашивать по одному ключу, то вычисления будут запускаться только на каждый 4й раз. А логику обработки данных, опять же, аккуратно переносим из aircrack, ничего не меняя. В итоге функция проверки получается следующей:
Для всех некратных 4м запросов — говорим, что ключ не подходит. А на 4м, если ключ найден — то возвращаем и true, и сам ключ. Буфер накапливается в массив из 4х элементов следующего типа:
Собственно говоря, переборщик готов. Для того, чтобы им воспользоваться, выполняем следующие действия:
Однако, в один поток считать умеет и сам aircrack, но нам же не это нужно? Архитектура параллельности После изучения существующих фреймворков и ПО для организации параллельных вычислений, установленных на кластере, к которому у меня был доступ, я решил остановиться на Open MPI. Работа с ним начинается со строчек:
На вызове Init() произойдет разделение процессов, после которого в rank будет стоять порядковый номер вычислителя, а в size — общее количество вычислителей. Процессы могут быть запущены на разных машинах, из которых складывается кластер, поэтому про shared memory легким путем придется забыть — общение между процессами идет с помощью двух функций: MPI::COMM_WORLD.Recv(&res_task, sizeof(res_task), MPI::CHAR, MPI_ANY_SOURCE, 0, status);
Подробнее про взаимодействие можно прочитать здесь. А теперь придумаем архитектуру параллельного поискового движка. По хорошему, учитывая специфику задачи, стоило бы сделать следующую архитектуру. Если всего в кластере N ядер, то каждое i-е ядро должно проверять ключи с идентификаторами i + N*k, k = 0,1,2..., никак не взаимодействуя друг с другом. Тогда производительность будет максимальной. Но, как я сказал в самом начале, основная задача — это освоение технологии, поэтому необходимо освоить общение между вычислителями. Поэтому я выбрал другую архитектуру, где процессы делятся на два типа, и здесь я опишу о максимально понятный вариант, в репозитории реализован вариант чуть-чуть по-сложнее и побыстрее, но все равно это далеко не самый быстрый вариант. Для этого условно разделим процессы на 2 типа — управляющие и рабочие. Управляющий будет рассылать рабочим задания, а рабочие будут, собственно, считать хэши. Для простоты обмениваться между процессами будем POD struct-ами. Схематично можно представить процессы в виде следующего рисунка:
Создадим соответственно классы controller и searcher, которые инстанциируем после идентификации процесса:
Объекты searcher-ы будут ждать сообщений-заданий, обрабатывать их и отправлять обратно сообщения-отчеты. Контроллер будет заниматься только рассылкой заданий и проверкой результатов. Если делать серьезно, то контроллер тоже может заниматься вычислениями во время простоев, но для нашего примера не будем усложнять этим архитектуру. Более того, необходимо избежать ситуаций, когда рабочий поток досчитал задание, и не успел получить новое и простаивает. Это достигается за счет использования очередей, и разнесения транспортного и вычислительного потока в простейшем случае с мьютексами, хотя по хорошему надо делать на conditional_variable. Поэтому схематично обмен данными между контроллером и рабочим потоком может быть представлен в следующем виде:
Вместо того, чтобы приводить здесь фрагменты кода с синхронизацией сошлюсь на свой же репозиторий. А мы переходим к завершающей части — экспериментам. Эксперименты Казалось бы, это самая ожидаемая часть статьи, но в силу простоты решаемой задачи результаты полностью совпадают с ожидаемыми. Для экспериментов был взят handshake пакет, который я достал из своей точки, а также пары соседских. Кстати говоря — процесс не очень приятный и детерминированный, и на это ушло больше часа. На своих пакетах я убедился в правильности работы разработанного ПО, а на соседских уже пробовал технологию в реальных условиях. В приведенных исходниках я сделал периодический вывод отладочной информации о текущей скорости, кол-ве просмотренных ключей и ожидаемом времени на проверку ключей текущей длины. В моем распоряжении был небольшой суперкомпьютер со 128 ядрами суммарно. В нем, конечно же, было SSE, хотя для чистоты эксперимента я его отключал — и получал скорость в 4 раза меньше. Сама динамика работы тоже вполне ожидаема — несколько секунд уходит на разгон и сбор статистики, после чего движок показывает стабильную скорость перебора. В зависимости от количества ядер достигается примерно линейный рост скорости (что очевидно), однако простой ядра контроллера и небрежная синхронизация потоков дают о себе знать — константа роста получилась в районе 0.8-0.9. Но самое интересное ждало меня при запуске движка на соседском ключе — не успев разогнать все ядра, пароль был уже найден… это оказалась дата рождения кого-то из семьи соседа. С одной стороны я разочаровался, с другой — я все таки решил первоначальную задачу. Приводить абсолютные значения скоростей я смысла не вижу, т.к. вы можете попробовать это на своих доступных машинах — все исходники приводятся в конце статьи. А зная коэффициент параллельности и характеристики машины, можно достаточно точно посчитать, какой скорости можно достигнуть. Исходники Исходники к описанной реализации можно найти в моем репозитории на гитхабе. Код был полностью рабочим, собиралось под Linux и Mac OS. Но… прошло больше 2х лет, не знаю, как сильно поменялось API того же Open MPI. В папке test/ можно найти пример handshake пакета, собранного под используемый формат. Сам код достаточно грязный, но в силу отсутствия практической ценности решаемой задачи, причесывать его смысла тоже не было. Проект тоже не развивается ввиду бессмысленности, но если кому-то понравилась идея, или он увидел зачем его можно использовать… то берите и пользуйтесь. В строке запуска указывается handshake-файл и опционально стартовый ключ, если требуется начать не с нулевого элемента. brute_wpa2 k aaa123aaab ap_Dom.hshk Сам парсинг параметров находится в src/brute_wpa2.cpp, из него также можно понять как задавать идентификатор первого ключа числом, а также задавать размер чанка. Заключение Данная статья дает небольшой экскурс в параллельное программирование на примере простейшей практической задачи и ее достаточно простого решения. Поставленную задачу я выполнил — не только освоив современные технологии параллельного программирования, получил навыки для реализации боевой задачи в диссертации, но и также подобрал пароль от соседского WiFi. Правда, им я воспользовался всего лишь один раз — чтобы проверить правильность, но все равно было приятно. Но возвращаясь к практической пользе проделанной работы, в связи с последними событиями хотелось бы отметить ажиотаж вокруг биткойнов. Учитывая, что в основе взлома WPA2 используется вычисление SHA хэшей, как и при майнинге биткойнов, то открывается новое направление для развития данной работы. Поскольку для майнинга было разработано специальное оборудование, которое умеет только считать нужные хэши, то интересно проверить, насколько оно адаптируемо и применимо для взлома WPA2. Безусловно, подобные чипы значительно эффективней последних процессоров общего назначения в вопросах подборах хэшей, поэтому, возможно, здесь можно добиться интересных результатов. Источник: habrahabr.ru Комментарии: |
|