Чужая информация всегда привлекала к себе внимание, а когда к ней нет прямого доступа, возникает желание прибрести этот доступ контробандным путём. Если социальная инженерия и прочая игра на чувствах не срабатывает, остаётся идти на таран и использовать метод "грубой силы", что в дословном переводе на инглиш звучит как "Brute-Force". За периметром конкретной задачи брутфорс лишён смысла – каждый случай требует индивидуального подхода. А потому предлагаю ознакомиться с алгоритмами его реализации на программном уровне, чтобы самостоятельно решать проблемы по мере их поступления.
Оглавление:
1. Общие сведения о криптологии;
2. Практика – генератор паролей;
3. Практика – брутфорс паролей;
4. Варианты оптимизации кода;
-------------------------------------------------
1. Криптология – общие сведения
Разработкой методов крипта и декрипта информации занимается "криптология", которая пошла по двум направлениям – криптография и криптоанализ. Первая (криптография) – это наука о методах обеспечения конфиденциальности, т.е. изучает криптосистемы и способы шифрования данных. Криптоанализ-же занимается вопросами оценки слабых сторон отдельно взятых методов защиты. Соответственно два эти направления являются враждующими сторонами, между которыми идёт не шуточная война. В рамках представленного на ваш суд материала, нас будет интересовать только криптоанализ в виде атаки на шифр, а его обобщённую схему раскрывает рисунок ниже:
Как видим, вариантов у взломщиков много, на все случаи жизни. Это довольно объёмная тема, и попытки охватить сразу весь материал ни к чему хорошему не приведут. Поэтому будем последовательны и рассмотрим только один из методов атак, а заинтересованный читать всегда может почерпнуть базовые сведения о криптоанализе в соответствующей литературе, или например в
1.1. Атака с известным шифротекстом – "Сiphertext only attack".
В атаках этого класса предполагается, что взломщик знает алгоритм шифрования, но не знает секретный пароль. Это самый распространённый вид атак, внутри которых можно пойти по одной из следующих трёх веток:
Таким образом, даже внутри одного класса атак имеем три совершенно разных подхода, мы-же остановимся только на втором варианте – брутфорс. По своей природе, он подразумевает попытку взлома паролей методом циклического перебора до полного совпадения. Если в качестве алфавита был выбран верхний регистр "A..Z", то на каждой итерации цикла к символу "А" прибавляем 1 и получаем "В". После того-как дойдём до символа "Z" (26 итераций = длина алфавита), то добавляем к паролю второй символ и получаем "АА". Теперь продолжаем перебирать первый символ, пока не получим "ZA", после чего увеличиваем уже второй символ пароля "АВ" и т.д.. На каждой итерации цикла, текущий пароль забрасывается удочкой программе-жертве и если она его не проглотила, значит подставляем следующий, ..и так до второго пришествия архангела-Михаила.
При таких раскладах, противостоять бруту довольно просто – достаточно ввести ограничение на количество попыток-ввода неверного пароля (допустим 3-раза), как перебор отваливается сам по себе. Реализовать это можно проверкой хорошо спрятанного в нёдрах системы счётчика, или установкой аппаратного бряка на обращение к памяти (SEH + отладочные регистры DR0-7). Нужно помнить, что в современных реалиях программы для брутфорса паролей предлагают лишь теоретическое решение проблемы, без каких-либо гарантий по временной шкале. Прибегать к нему стоит лишь в том случае, когда основа пароля известна (например, это какое-то число или дата), и необходимо подобрать к ней комбинацию из дополнительных нескольких цифр/букв.
2. Практика – генератор паролей
Чтобы опробовать свои силы в бруте нам нужен пароль, ..а если готовых паролей нету, то напишем примитивный их генератор. Гугл предлагает нам различные варианты, а мы пополним этот зоопарк ещё одним "зверьком". В масштабах этой задачи, нужно будет сформировать три типа символьных алфавитов, из которых рандомом будем выбирать числа и буквы для пассвордов. С алфавитом проблем нет – его мы честно скомуниздим у кодировок "Base64/32/16", а рандом будем использовать как индекс (порядковый номер), для выборки символов из этих алфавитов. Base64 описывает стандарт
Посмотрите на таблицу символов ASCII – все числа и буквы в ней собраны последовательно с инкрементом +1. К примеру символ верхнего регистра "А" будет лежать в памяти нашей программы как значение
Синтаксис ассемблера FASM имеет интересную директиву "%". Если поместить её внутрь цикла, она возвращает текущий счётчик. Так-же, в наличии имеется директива "times", которая повторяет одну инструкцию указанное кол-во раз. За ней должно следовать значение счётчика, и инструкция, которую нужно повторять (в нашем случае символ). Например, такая конструкция запишет в память все символы кодировки "Base64". Поскольку счётчик(%) начинает считать с единицы, делаем -1:
Будем считать, что забросили алфавиты в память, теперь нужно выбирать из них псевдо-случайные значения для пароля. Как видим, каждый из символов имеет свою позицию в дампе – это "индекс элемента" в массиве. Например по индексу(3) от начала лежит символ "D" со-значением
Если юзер захочет в генераторе паролей использовать символы кодировки "Base32", то и рандом должен быть в пределах 0..31. На практике, загнать значение в диапазон можно взятием остатка от деления. В данном случае, "потолки" всех алфавитов у нас кратны двум: 16,32,64, поэтому находить остаток от деления можно логикой, как в примере ниже (случайное значение лежит в регистре AL):
Теперь в AL лежит число, значение которого мы должны использовать в качестве индекса в алфавите. Для этого, воспользуемся инструкцией ассемблера
Другими словами, эта инструкция требует, чтобы в регистре
Ну и в заключении о самом рандоме..
Сгенерить его можно различными путями – это и счётчик тактов процессора
3. Практика – программа для брутфорса паролей
Не последнюю роль в подборе паролей играет "атака по-словарю". Но процент попадания в орфографический словарь представленных выше паролей ничтожно мал, ведь это не осмысленные слова, а скорее напоминают помещённые в миксер предложения. В таких случаях, брут по-словарю абсолютно не эффективен и заранее обречён на провал. Поэтому приготовившись к длительной осаде бастиона, приходится подбирать пароль последовательным перебором всех возможных вариантов, начиная с первого и до последнего его символа. Рассмотрим, как это можно осуществить на практике.
Авансом скажу, что за разумное время (примерно месяц), брутфорсом на одной машине можно подобрать пароли длинной не более 8-ми символов. На подбор представленных выше 16-значных паролей могут уйти года! Но если учесть, что производительность современных компьютеров сейчас на высоте, то "лобовой перебор" из утопии превратился в реальность – это не брут на третьих пеньках. А если распараллелить вычисления по всем ядрам CPU и собрать в кластер несколько таких машин (что собственно и делают хакерские группировки), то время брутфорса можно уменьшить в тысячи раз.
Ниже приводится не оптимизированный исходный код программы-брутфорсера.
Значит всё-что нам нужно, это запросить у юзера пароль для брута, после чего выбрать алфавит с возможными в пароле символами. Теперь, начиная с первого индекса этого алфавита, в цикле перебирать все его символы. Как только сделаем один круг, добавляем к нашему паролю второй символ, ..следом третий и так, пока не найдём схожую с оригинальным паролем комбинацию. То-есть медленно, но верно будем продвигаться к своей цели.
Соответственно на каждой итерации цикла нужно сравнивать обе строки, а в данном примере я отображаю ещё и процесс брута на консоль – это отнимает львиную долю времени перебора. Чтобы узнать чистый профайл брутфорса, можно закомментировать демонстрацию и оставить только сравнение паролей функцией lstrcmp() и сам полезный код, без SetConsoleCursorPosition() + printf(). Вот пример:
Обратите внимание, что чем длиннее алфавит, тем дольше длиться процесс перебора. Например алфавит "Base64" содержит вдвое больше символов, чем "Base32". Соответственно и обходить его в цикле по-времени будет в два раза накладней. Поэтому если пароль состоит только из цифр типа "12345", то и выбирать нужно самый короткий алфавит "Base16" – это на порядок увеличит скорость брутфорса, чем если-бы мы выбрали только для цифр алфавит "Base64".
4. Варианты оптимизации кода
Время – основной фактор в циклическом инкременте паролей, а потому код должен быть максимально компактным, без избыточных обращений к памяти. Пример выше не может похвастаться этим, хотя и выполняет поставленную задачу. Ныне покойный К.Касперски был увлечён этим направлением и в своих трудах приводил различные фишки для оптимизации кода. Маэстро бесспорно отличался неординарным мышлением и предложил для брута весьма оригинальный вариант – рассмотрим его детали..
Значит по-прежнему имеем в памяти алфавит, из которого последовательно планируем выбирать символы для своего пароля. Здесь, мыщъх посоветовал выстрелить дробью и сразу убить стаю куропаток по такому алго. Мы знаем, что первым символом в алфавите является "А" с кодом
То-есть по адресу(0) в алфавите кладём символ(А) с кодом
Таким способом можно сформировать таблицу буквально всех символов, начиная с пробела с кодом
Невероятно, но буквально в 26-ти байтах мыщъх умудрился закодировать полноценный брутфорс паролей! Вот это действительно мысль достойная аплодисментов. В примере ниже я позаимствовал у него этот алгоритм, и добавил в него счётчик сгенерированных на текущий момент паролей. При каждом обновлении пароля, я увеличиваю регистр
Чтобы посмотреть, с какой скоростью код перебирает пароли, я скомпилировал его в двух вариантах – с выводим процесса на консоль, и без него. Для этого я просто закомментировал следующие две строки кода:
Если клавишей(F7) зайти в эти функции отладчиком, то обнаружим сотни строк кода, при помощи которых система реализует эти функции. Непосредственно полезная нагрузка брутфорса уходит в этом случае на 10-ый план, поэтому и время увеличивается соответственно. Эта разница показана на скринах ниже:
5. Заключение
Здесь я попытался освятить только одну сторону этого интересного направления. За бортом остались "атаки по-словарю", и это чуть другая тема со-своим подходом. Как нибудь в другой раз мы обязательно вернёмся к ней, поскольку в своей массе юзеры используют для паролей именно осмысленные слова, а вот фантазии у них не хватает. Поэтому перебор по-словарю всегда идёт на шаг впереди брутфорса.
В скрепку я положил три исполняемых файла, коды которых мы рассмотрели выше. В версии(1) имеется возможность выбрать алфавит для паролей "Base16/32/64", зато сам алгоритм перебора оставляет желать лучшего. Во-второй версии алго оптимизирован, но алфавит включает в себя все печатные символы, из-за чего процесс перебора может длиться дольше. Без проблем можно было заточить версию(2) и на выбор алфавита, с построением соответствующих таблиц. Но оставлю это вам в качестве дз. Здесь ставлю точку, и до скорого!
Оглавление:
1. Общие сведения о криптологии;
2. Практика – генератор паролей;
3. Практика – брутфорс паролей;
4. Варианты оптимизации кода;
• выбор по индексу,
• распределение по потокам Thread.
5. Заключение.-------------------------------------------------
1. Криптология – общие сведения
Разработкой методов крипта и декрипта информации занимается "криптология", которая пошла по двум направлениям – криптография и криптоанализ. Первая (криптография) – это наука о методах обеспечения конфиденциальности, т.е. изучает криптосистемы и способы шифрования данных. Криптоанализ-же занимается вопросами оценки слабых сторон отдельно взятых методов защиты. Соответственно два эти направления являются враждующими сторонами, между которыми идёт не шуточная война. В рамках представленного на ваш суд материала, нас будет интересовать только криптоанализ в виде атаки на шифр, а его обобщённую схему раскрывает рисунок ниже:
Как видим, вариантов у взломщиков много, на все случаи жизни. Это довольно объёмная тема, и попытки охватить сразу весь материал ни к чему хорошему не приведут. Поэтому будем последовательны и рассмотрим только один из методов атак, а заинтересованный читать всегда может почерпнуть базовые сведения о криптоанализе в соответствующей литературе, или например в
Ссылка скрыта от гостей
. Чтобы понять, о чём пойдёт речь конкретно в этом треде, рассмотрим определение атак, которые выделены на рисунке выше коричневым цветом.1.1. Атака с известным шифротекстом – "Сiphertext only attack".
В атаках этого класса предполагается, что взломщик знает алгоритм шифрования, но не знает секретный пароль. Это самый распространённый вид атак, внутри которых можно пойти по одной из следующих трёх веток:
1. Перебор ключей по словарю. Способ требует наличия "файла-словаря" – этакой базы, со списком всевозможных паролей. Словари лежат в свободном доступе, например
Ссылка скрыта от гостей
. Встречаются базы размером 30 и более гигаБайт, которые обычный блокнот мастдая уже не в силах открыть и нужно искать подходящий софт. В атаках по словарю, мы берём из него очередной пароль и подсовываем его жертве. Если облом, то парсим следующую запись и т.д. Если пароль криптостойкий и его нет в словаре, то данный подход вообще не даст результата, хотя в зависимости от размера используемой базы, процесс перебора может длиться довольно долго.
2. Полный перебор ключей – классический "брутфорс", или слепая атака в лоб. Отличается от метода со-словарём тем, что пароли в явном виде уже нигде не хранятся – их порождает заброшенная в цикл матка. В осуществляющем брут софте должна быть предусмотрена опция, которая позволяла-бы выбирать "алфавит" для паролей. На практике применяются следующие типы алфавитов: (1)только цифры 0..9, (2)только заглавные символы "A..Z", (3)пароли только из прописных символов "a..z", (4)гремучая смесь всех печатных символов, включая знаки препинания. Если брутить пароли с полным алфавитом(4), то перебор обязательно даст положительный результат и это придаёт надежды. Однако затраченное на поиск время может в несколько раз превысить возраст вселенной.
3. Частотный криптоанализ. Метод основывается на частоте повторения букв в пароле. Например, если пароль представляет собой осмысленное слово, то в англоязычных текстах много букв "e", и артиклей "the". Частотный криптоанализ – это любая атака, которая использует данный факт. Даже если пароль закодирован (к примеру подстановочным шифром Цезаря), то по частоте встречающихся символов можно сделать вывод, что это именно символ(е).
Таким образом, даже внутри одного класса атак имеем три совершенно разных подхода, мы-же остановимся только на втором варианте – брутфорс. По своей природе, он подразумевает попытку взлома паролей методом циклического перебора до полного совпадения. Если в качестве алфавита был выбран верхний регистр "A..Z", то на каждой итерации цикла к символу "А" прибавляем 1 и получаем "В". После того-как дойдём до символа "Z" (26 итераций = длина алфавита), то добавляем к паролю второй символ и получаем "АА". Теперь продолжаем перебирать первый символ, пока не получим "ZA", после чего увеличиваем уже второй символ пароля "АВ" и т.д.. На каждой итерации цикла, текущий пароль забрасывается удочкой программе-жертве и если она его не проглотила, значит подставляем следующий, ..и так до второго пришествия архангела-Михаила.
При таких раскладах, противостоять бруту довольно просто – достаточно ввести ограничение на количество попыток-ввода неверного пароля (допустим 3-раза), как перебор отваливается сам по себе. Реализовать это можно проверкой хорошо спрятанного в нёдрах системы счётчика, или установкой аппаратного бряка на обращение к памяти (SEH + отладочные регистры DR0-7). Нужно помнить, что в современных реалиях программы для брутфорса паролей предлагают лишь теоретическое решение проблемы, без каких-либо гарантий по временной шкале. Прибегать к нему стоит лишь в том случае, когда основа пароля известна (например, это какое-то число или дата), и необходимо подобрать к ней комбинацию из дополнительных нескольких цифр/букв.
2. Практика – генератор паролей
Чтобы опробовать свои силы в бруте нам нужен пароль, ..а если готовых паролей нету, то напишем примитивный их генератор. Гугл предлагает нам различные варианты, а мы пополним этот зоопарк ещё одним "зверьком". В масштабах этой задачи, нужно будет сформировать три типа символьных алфавитов, из которых рандомом будем выбирать числа и буквы для пассвордов. С алфавитом проблем нет – его мы честно скомуниздим у кодировок "Base64/32/16", а рандом будем использовать как индекс (порядковый номер), для выборки символов из этих алфавитов. Base64 описывает стандарт
Ссылка скрыта от гостей
, в котором приводится полный его паспорт, в том числе и задействованные в кодировках символы:Посмотрите на таблицу символов ASCII – все числа и буквы в ней собраны последовательно с инкрементом +1. К примеру символ верхнего регистра "А" будет лежать в памяти нашей программы как значение
41h
, а все буквы нижнего регистра начинаются с 61h
, т.е. разница между ними =20h
или всего один бит(5). Символы всех чисел 0..9 отличаются от своего-же 10-тичного значения ровно на 30h
, т.е. "1"=31h, "2"=32h и т.д. Это упрощает их перевод из верхнего в нижний регистр, или из строки в число.Синтаксис ассемблера FASM имеет интересную директиву "%". Если поместить её внутрь цикла, она возвращает текущий счётчик. Так-же, в наличии имеется директива "times", которая повторяет одну инструкцию указанное кол-во раз. За ней должно следовать значение счётчика, и инструкция, которую нужно повторять (в нашем случае символ). Например, такая конструкция запишет в память все символы кодировки "Base64". Поскольку счётчик(%) начинает считать с единицы, делаем -1:
C-подобный:
Base64: ;//<----------------- Метка для обращения к массиву алфавита
times 26 db % +('A'-1) ;// ABCDEFGHIJKLMNOPQRSTUVWXYZ
times 26 db % +('a'-1) ;// abcdefghijklmnopqrstuvwxyz
times 10 db % +('0'-1) ;// 0123456789
db '+/' ;// +/
Будем считать, что забросили алфавиты в память, теперь нужно выбирать из них псевдо-случайные значения для пароля. Как видим, каждый из символов имеет свою позицию в дампе – это "индекс элемента" в массиве. Например по индексу(3) от начала лежит символ "D" со-значением
44h
и т.д. (отсчёт с нуля). Соответственно если мы сгенерим рандомный байт, то можем использовать его в качестве индекса для выборки случайного символа из общего алфавита. Только этот рандом обязательно должен быть в диапазоне 0..63, чтобы индекс не вылетал за пределы алфавита.Если юзер захочет в генераторе паролей использовать символы кодировки "Base32", то и рандом должен быть в пределах 0..31. На практике, загнать значение в диапазон можно взятием остатка от деления. В данном случае, "потолки" всех алфавитов у нас кратны двум: 16,32,64, поэтому находить остаток от деления можно логикой, как в примере ниже (случайное значение лежит в регистре AL):
C-подобный:
and al,63 ;// AL = число в диапазоне 0..63
and al,31 ;// AL = число в диапазоне 0..31
and al,15 ;// AL = число в диапазоне 0..15
Теперь в AL лежит число, значение которого мы должны использовать в качестве индекса в алфавите. Для этого, воспользуемся инструкцией ассемблера
XLAT
с таким описанием от Intel. Преимущество её в том, что она 1-байтная и процессор тратит на её исполнение всего 2 своих такта. За это время, инструкция "убивает сразу два зайца" по такой схеме:XLAT – Table Look-up Translation (opcode = D7h)
-----------------------------------------------
Находит запись байта в таблице в памяти, используя содержимое регистра AL в качестве индекса в таблице. Затем копирует содержимое записи таблицы обратно в регистр AL. Индекс в AL рассматривается как целое число без знака = 127.
Другими словами, эта инструкция требует, чтобы в регистре
EBX
лежал указатель на массив в памяти, а в регистр AL
нужно поместить некоторый байт без-знака. Теперь XLAT
берёт значение из AL
и использует его как порядковый номер элемента в массиве EBX
. На заключительном этапе, найденный элемент копируется обратно в регистр AL
перезаписывая находящийся в нём индекс. На выходе в том-же AL
получаем результат в виде символа из алфавита. Для нашего случая как-раз то, что доктор прописал, ..и всё это за каких-то пару тактов процессора.Ну и в заключении о самом рандоме..
Сгенерить его можно различными путями – это и счётчик тактов процессора
RDTSC
, и текущие милли-секунды системного времени, и.. список можно продолжать бесконечно. Но без дополнительных танцев с бубном все эти варианты возвращают лишь 2, 4 или 8-байтный рандом, а ведь для индекса нам нужно число в диапазоне 0..16..32..64. Так-что мы пойдём другим путём, и специально предназначенной для крипта функцией CryptGenRandom() сгенерим сразу массив случайных чисел требуемой длинны – благо функция это позволяет. Теперь из этого массива будем читать по 1-байту, что и позволит решить проблему. В конечном итоге, прожку для генерации случайных паролей можно оформить например так:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//--------
.data
Base64: ;//<----------------- Алфавит 64-символов
times 26 db % +('A'-1) ;// ABCDEFGHIJKLMNOPQRSTUVWXYZ
times 26 db % +('a'-1) ;// abcdefghijklmnopqrstuvwxyz
times 10 db % +('0'-1) ;// 0123456789
db '+/' ;// +/
Base32: ;//<----------------- Алфавит 32-символов
times 26 db % +('A'-1) ;// ABCDEFGHIJKLMNOPQRSTUVWXYZ
times 6 db % +('2'-1) ;// 234567
Base16: ;//<----------------- Алфавит 16-символов
times 10 db % +('0'-1) ;// 0123456789
times 6 db % +('A'-1) ;// ABCDEF
title db '*** Password Gen v0.1 ***',0
alphabet dd 0 ;// будет адресом алфавита,
alphaLen dd 0 ;// ..а здесь его длина.
passLen dd 0 ;// длина пароля
passCount dd 0 ;// кол-во паролей для генерации
hcrypt dd 0 ;// дескриптор крипто-провайдера "Win32Crypt"
buff rb 64 ;// буфер для рандома (макс.Base64)
passStr db 0 ;// буфер для строки с нашим паролем
;//--------
.code
start: invoke SetConsoleTitle,title
;//----- Позволяем юзеру выбрать алфавит
cinvoke printf,<10,\
' *** Alphabet*** : 1=Base64, 2=Base32, 3=Base16',10,\
' Choise number...: ',0>
cinvoke scanf,<'%d',0>,alphaLen
;//----- Настраиваем переменные под выбранный юзером алфавит
cmp [alphaLen],1 ;// проверить ввод на 1
jne @f ;// на нижнюю метку, если нет..
mov [alphabet],Base64 ;// иначе: сохраняем в переменной адрес "Base64"
mov [alphaLen],64 ;// ..и следом его длину.
jmp @getRandom ;// уйти на генератор!
@@: cmp [alphaLen],2
jne @f
mov [alphabet],Base32
mov [alphaLen],32
jmp @getRandom
@@: cmp [alphaLen],3
jne @f
mov [alphabet],Base16
mov [alphaLen],16
jmp @getRandom
@@: cinvoke printf,<' Error alphabet num!!!',0> ;// Ошибка ввода!!!
jmp @exit
@getRandom:
;//----- Запрашиваем у юзера необходимую длину пароля
cinvoke printf,<' Password length.: ',0>
cinvoke scanf,<'%d',0>,passLen
;//----- Запрашиваем у юзера кол-во паролей для генерации
cinvoke printf,<' Password counter: ',0>
cinvoke scanf,<'%d',0>,passCount
;//====== ГЕНЕРАТОР ПАРОЛЕЙ ===================================
;// Сначала выбираем системного крипто-провайдера = 0xf0000000,
;// после чего генерим рандом по длине пароля, в приёмный буфер
invoke CryptAcquireContext,hcrypt,0,0,1,0xf0000000
@cycle: invoke CryptGenRandom,[hcrypt],[passLen],buff
mov esi,buff ;// ESI = источник байт-рандомов
mov edi,passStr ;// EDI = приёмник выбранных из алфавита символов
mov ecx,[passLen] ;// ECX = длина пароля для цикла LOOP
mov ebx,[alphabet] ;// EBX = адрес выбранного юзером алфавита для XLAT
mov edx,[alphaLen] ;// EDX = длина алфавита 16/32/64,
dec edx ;// ..-1 для получения остатка логикой AND.
@@: lodsb ;// AL = очередной байт-рандом из ESI (esi+1)
and al,dl ;// загнать рандом в диапазон (остаток от длины)
xlatb ;// взять в AL символ из EBX, по индексу в AL
stosb ;// сохранить AL в по адресу в EDI (edi+1)
loop @b ;// повторить цикл "@@" ECX-раз..
cinvoke printf,<10,' Generate pass...: %s',0>,passStr ;// пасс на консоль!
dec [passCount] ;// уменьшить счётчик генератора
jnz @cycle ;// повторить внешний цикл, если счётчик не нуль..
;//----- Освободить системного крипто-провайдера
invoke CryptReleaseContext,[hcrypt],0
@exit: cinvoke scanf,<'%s',0>,buff
cinvoke exit,0
;//--------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',advapi32,'advapi32.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
include 'api\kernel32.inc'
include 'api\advapi32.inc'
3. Практика – программа для брутфорса паролей
Не последнюю роль в подборе паролей играет "атака по-словарю". Но процент попадания в орфографический словарь представленных выше паролей ничтожно мал, ведь это не осмысленные слова, а скорее напоминают помещённые в миксер предложения. В таких случаях, брут по-словарю абсолютно не эффективен и заранее обречён на провал. Поэтому приготовившись к длительной осаде бастиона, приходится подбирать пароль последовательным перебором всех возможных вариантов, начиная с первого и до последнего его символа. Рассмотрим, как это можно осуществить на практике.
Авансом скажу, что за разумное время (примерно месяц), брутфорсом на одной машине можно подобрать пароли длинной не более 8-ми символов. На подбор представленных выше 16-значных паролей могут уйти года! Но если учесть, что производительность современных компьютеров сейчас на высоте, то "лобовой перебор" из утопии превратился в реальность – это не брут на третьих пеньках. А если распараллелить вычисления по всем ядрам CPU и собрать в кластер несколько таких машин (что собственно и делают хакерские группировки), то время брутфорса можно уменьшить в тысячи раз.
Ниже приводится не оптимизированный исходный код программы-брутфорсера.
Значит всё-что нам нужно, это запросить у юзера пароль для брута, после чего выбрать алфавит с возможными в пароле символами. Теперь, начиная с первого индекса этого алфавита, в цикле перебирать все его символы. Как только сделаем один круг, добавляем к нашему паролю второй символ, ..следом третий и так, пока не найдём схожую с оригинальным паролем комбинацию. То-есть медленно, но верно будем продвигаться к своей цели.
Соответственно на каждой итерации цикла нужно сравнивать обе строки, а в данном примере я отображаю ещё и процесс брута на консоль – это отнимает львиную долю времени перебора. Чтобы узнать чистый профайл брутфорса, можно закомментировать демонстрацию и оставить только сравнение паролей функцией lstrcmp() и сам полезный код, без SetConsoleCursorPosition() + printf(). Вот пример:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//--------
.data
Base64: ;//<----------------- Алфавит 64-символов
times 26 db % +('A'-1) ;// ABCDEFGHIJKLMNOPQRSTUVWXYZ
times 26 db % +('a'-1) ;// abcdefghijklmnopqrstuvwxyz
times 10 db % +('0'-1) ;// 0123456789
db '+/' ;// +/
Base32: ;//<----------------- Алфавит 32-символов
times 26 db % +('A'-1) ;// ABCDEFGHIJKLMNOPQRSTUVWXYZ
times 6 db % +('2'-1) ;// 234567
Base16: ;//<----------------- Алфавит 16-символов
times 10 db % +('0'-1) ;// 0123456789
times 6 db % +('A'-1) ;// ABCDEF
sTime SYSTEMTIME ;// время начала/конца брута
title db '*** Password Bruteforce v0.1 ***',0
passLen dd 0 ;// длина пароля
alphaLen dd 0 ;// длина алфавита
alphabet dd 0 ;//
hcrypt dd 0 ;// хэндл Win32Crypt
hndl dd 0 ;// дескриптор консоли
align 16
offset db 64 dup(0) ;// относительная адресация
buff rb 128 ;// буфер для рандома
passStr db 0 ;// буфер для строки с паролем
;//--------
.code
start: invoke SetConsoleTitle,title
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov [hndl],eax
;//====== Запрашиваем у юзера пароль, который будем брутить
cinvoke printf,<10,' Type password...: ',0>
cinvoke scanf,<'%s',0>,passStr
mov [passLen],eax
;//====== Даём ему возможность выбрать алфавит
cinvoke printf,<10,\
' ==============================================',10,\
' ============== SET CONFIGURATION =============',10,\
' Alphabet.....: 1 = Base64 --> A..Z, a..z, 0..9, +/',10,\
' : 2 = Base32 --> A..Z, 2..7',10,\
' : 3 = Base16 --> 0..9, A..F',10,\
' Choise number: ',0>
cinvoke scanf,<'%d',0>,alphaLen
;//====== Настраиваем переменные под выбранный алфавит
cmp [alphaLen],1 ;// проверить ввод на 1
jne @f ;// на нижнюю метку, если нет..
mov [alphabet],Base64 ;// иначе: сохраняем в переменной адрес алфавита,
mov [alphaLen],64 ;// ..и его длину.
jmp @startBrute ;// уйти на брут!
@@: cmp [alphaLen],2
jne @f
mov [alphabet],Base32
mov [alphaLen],32
jmp @startBrute
@@: cmp [alphaLen],3
jne @f
mov [alphabet],Base16
mov [alphaLen],16
jmp @startBrute
@@: cinvoke printf,<' Error alphabet num!!!',0> ;// Ошибка ввода!!!
jmp @exit
;//==============================================
;//====== ПЕРЕБОР ПАРОЛЕЙ БРУТФОРСОМ ============
;//==============================================
@startBrute:
cinvoke printf,<' ==============================================',10,10,\
' Bruteforce......: ',0>
;// Очистить буфер, куда будем сбрасывать сгенерированные пароли
;// (пароль юзера для сверки лежит в буфере "passStr").
mov edi,buff
mov ecx,128/4
xor eax,eax
rep stosd
;// Системное время начала брута
invoke GetSystemTime,sTime
movzx eax,word[sTime.wHour]
movzx ebx,word[sTime.wMinute]
movzx ecx,word[sTime.wSecond]
movzx edx,word[sTime.wMilliseconds]
cinvoke printf,<10,10,' Start time......: %02d:%02d:%02d.%03d ms',0>,\
eax,ebx,ecx,edx
;// Генерируем пароли!!!
mov ecx,[alphaLen] ;// длина текущего алфавита
@mainLoop:
mov esi,offset ;// позиция символа, справа от текущего
mov edi,buff ;// приёмник паролей
@decode: mov bl,byte[esi] ;// текущий индекс в нашем пароле
and ebx,0ffh ;// оставить в EBX только мл.байт BL
add ebx,[alphabet] ;// EBX = индекс в алфавите
mov al,byte[ebx] ;// взять от туда символ
mov byte[edi],al ;// сохранить его в своём пароле
;// Отобразить ход и сравнить пароль с юзерским!!!
push ecx edi esi
invoke SetConsoleCursorPosition,[hndl],0B0013h ;// позиция курсора в консоли
cinvoke printf,<'%s',0>,buff ;// показать текущий наш пароль
invoke lstrcmp,passStr,buff ;// сравнить его с юзерским
pop esi edi ecx
or eax,eax ;// проверить результат
je @stop ;// если пароль найден!
inc byte[esi] ;// иначе: индекс +1
cmp byte[esi],cl ;// это конец алфавита?
jna @mainLoop ;// нет - мотаем цикл дальше..
@subLoop:
mov byte[esi],0 ;// иначе: сбросить индекс
mov ebx,[alphabet] ;// возвратиться в начало алфавита
mov al,byte[ebx] ;// взять с него первый символ
mov byte[edi],al ;// сохранить его в своём пароле
inc esi ;// сместить указатели
inc edi ;// ...^^^
inc byte[esi] ;// следующий индекс в нашем пароле типа "АА"
mov bl,byte[esi] ;// взять от туда позицию
and ebx,0ffh ;//
add ebx,[alphabet] ;// EBX = индекс в алфавите
mov al,byte[ebx] ;// байт от туда
mov byte[edi],al ;// сохранить его в своём пароле
cmp byte[esi],cl ;// проверить на конец алфавита
ja @subLoop ;// если байт из ESI больше
jmp @mainLoop ;// иначе: мотаем внешний цикл..
;//=====================================================
;//====== Нашли пароль! Системное время конца брутфорса.
@stop: invoke GetSystemTime,sTime
movzx eax,word[sTime.wHour]
movzx ebx,word[sTime.wMinute]
movzx ecx,word[sTime.wSecond]
movzx edx,word[sTime.wMilliseconds]
cinvoke printf,<10,10,10,' Stop time......: %02d:%02d:%02d.%03d ms',0>,\
eax,ebx,ecx,edx
cinvoke printf,<10,10,' ******** Password found! ********',0>
@exit: cinvoke scanf,<'%s',0>,buff
cinvoke exit,0
;//--------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',advapi32,'advapi32.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
include 'api\kernel32.inc'
include 'api\advapi32.inc'
Обратите внимание, что чем длиннее алфавит, тем дольше длиться процесс перебора. Например алфавит "Base64" содержит вдвое больше символов, чем "Base32". Соответственно и обходить его в цикле по-времени будет в два раза накладней. Поэтому если пароль состоит только из цифр типа "12345", то и выбирать нужно самый короткий алфавит "Base16" – это на порядок увеличит скорость брутфорса, чем если-бы мы выбрали только для цифр алфавит "Base64".
4. Варианты оптимизации кода
Время – основной фактор в циклическом инкременте паролей, а потому код должен быть максимально компактным, без избыточных обращений к памяти. Пример выше не может похвастаться этим, хотя и выполняет поставленную задачу. Ныне покойный К.Касперски был увлечён этим направлением и в своих трудах приводил различные фишки для оптимизации кода. Маэстро бесспорно отличался неординарным мышлением и предложил для брута весьма оригинальный вариант – рассмотрим его детали..
Значит по-прежнему имеем в памяти алфавит, из которого последовательно планируем выбирать символы для своего пароля. Здесь, мыщъх посоветовал выстрелить дробью и сразу убить стаю куропаток по такому алго. Мы знаем, что первым символом в алфавите является "А" с кодом
41h=65
. А что если использовать код очередного символа, в качестве индекса в алфавите? Правда в этом случае сам алфавит должен иметь строго определённый формат, зато код брутфорса сократится буквально до нескольких байт.То-есть по адресу(0) в алфавите кладём символ(А) с кодом
41h
, а по адресу(41h) вставляем в алфавит следующий символ(B) с кодом 42h
. Далее, по адресу(42h) ставим символ(С) с кодом 43h
, и т.д. Чтобы по окончании очередного прохода по всему алфавиту на автомате опять перейти в его начало, нужно всего-то закончить алфавит терминальным нулём, который послужит индексом к первому символу(А) алфавита. Всё остальное сделают инструкции XLAT
(чтение в AL по индексу) и STOSB
(сохранение считанного символа в пароль). Чуть запутано, но надеюсь рисунок ниже прояснит эту ситуацию:Таким способом можно сформировать таблицу буквально всех символов, начиная с пробела с кодом
20h
, и до самого подвала с символом тильда(~). Теперь наш текущий пароль будет хранить не только просто символы, но эти символы сами будут индексами к следующему символу в алфавите:
C-подобный:
.data
fullAlphabet:
db ' ' ;// элемент(0) массива = пробел с кодом 20h
rb 1Fh ;// пропускаем следующие, до элемента(20h)
times 94 db % + ('!'-1) ;// вставляем все печатные символы латиницы (см.таблицу ASCII)
db 0 ;// терминальный нуль для возврата в начало (к пробелу).
passStr db 0 ;// приёмник для символов пароля
;//---------
.code
@BruteForce:
mov ebx,fullAlphabet ;// адрес алфавита для XLAT
mov edi,passStr ;// адрес приёмника
@@: movzx eax,byte[edi] ;// AL = очередной символ из нашего пароля
xlatb ;// использовать его как индекс!
or eax,eax ;// конец алфавита?
je @01 ;// если да..
mov byte[edi],al ;// иначе: обновить его в пароле
jmp @BruteForce ;// уйти на начало..
@01: xlatb ;// добавляем в пароль сл.символ справа
stosb ;// ^^^
jmp @b ;// промотать цикл, пока не встретим нуль.
Невероятно, но буквально в 26-ти байтах мыщъх умудрился закодировать полноценный брутфорс паролей! Вот это действительно мысль достойная аплодисментов. В примере ниже я позаимствовал у него этот алгоритм, и добавил в него счётчик сгенерированных на текущий момент паролей. При каждом обновлении пароля, я увеличиваю регистр
ECX
на 1, после чего проверяю его на 100.000. Если равно, то запрашиваю процедуру вывода счётчика на консоль. Это позволит наблюдать скорость перебора паролей – вот вторая, оптимизированная версия брутфорса:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//--------
.data
alphabet db ' ' ;// элемент(0) массива = пробел с кодом 20h
rb 1Fh ;// пропускаем следующие, до элемента(20h)
times 94 db % + ('!'-1) ;// вставляем все печатные символы латиницы!
db 0 ;// терминальный нуль для возврата в начало.
title db '*** Password Brute v0.2 ***',0
sTime SYSTEMTIME ;// время начала/конца брута
counter dd 0 ;// счётчик сгенерированных паролей
hndl dd 0 ;// дескриптор консоли
align 16
buff rb 128 ;// под пароль юзверя
passStr db 0 ;// буфер для строки с паролем
;//--------
.code
start: invoke SetConsoleTitle,title
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov [hndl],eax
;//====== Запрашиваем у юзера пасс, и сбрасываем его в буфер
cinvoke printf,<10,' Type password: ',0>
cinvoke scanf,<'%s',0>,buff
;//====== Системное время начала брута
cinvoke printf,<10,' =========================',\
10,' Bruteforce...: ',0>
invoke GetSystemTime,sTime
movzx eax,word[sTime.wHour]
movzx ebx,word[sTime.wMinute]
movzx ecx,word[sTime.wSecond]
movzx edx,word[sTime.wMilliseconds]
cinvoke printf,<10,10,' Start time...: %02d:%02d:%02d.%03d ms',0>,\
eax,ebx,ecx,edx
;//====== ПЕРЕБОР ПАРОЛЕЙ ПО ВСЕМУ АЛФАВИТУ =============
xor ecx,ecx ;// сбрасываем счётчик паролей
@BruteForce: ;// Начало атаки!
mov ebx,alphabet ;// EBX = адрес алфавита для XLAT
mov edi,passStr ;// EDI = приёмник для STOSB
@main: movzx eax,byte[edi] ;//
xlatb ;//
or eax,eax ;//
je @newSize ;//
mov byte[edi],al ;// запись очередного пароля..
inc ecx ;// увеличить счётчик,
cmp ecx,100000 ;// ..и проверить его на 100.000
jb @f ;// пропустить, если меньше
call PrintCounter ;// иначе: вывод счётчика на консоль
@@: push ecx ebx edi ;//
invoke SetConsoleCursorPosition,[hndl],040010h ;// позиция курсора в консоли
cinvoke printf,<'%s',0>,passStr ;// показать текущий наш пароль
invoke lstrcmp,passStr,buff ;// сравнить его с юзерским
pop edi ebx ecx ;//
or eax,eax ;//
je @stop ;// стоп, если нашли пароль!
jmp @BruteForce ;//
@newSize: xlatb ;// увеличилась длина пароля
stosb ;// расширить его справа.
jmp @main ;//
@stop:
;// Системное время начала брута
invoke GetSystemTime,sTime
movzx eax,word[sTime.wHour]
movzx ebx,word[sTime.wMinute]
movzx ecx,word[sTime.wSecond]
movzx edx,word[sTime.wMilliseconds]
cinvoke printf,<10,10,10,' Stop time...: %02d:%02d:%02d.%03d ms',0>,\
eax,ebx,ecx,edx
cinvoke printf,<10,10,' ***** Pass found!!! *****',0>
cinvoke scanf,<'%s',0>,buff
cinvoke exit,0
;//--------
PrintCounter:
xor ecx,ecx
inc [counter]
pusha
invoke SetConsoleCursorPosition,[hndl],04001Bh ;// позиция курсора в консоли
cinvoke printf,<'%d00.000 passwords',0>,[counter]
popa
ret
;//--------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',advapi32,'advapi32.dll'
import msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
include 'api\kernel32.inc'
include 'api\advapi32.inc'
Чтобы посмотреть, с какой скоростью код перебирает пароли, я скомпилировал его в двух вариантах – с выводим процесса на консоль, и без него. Для этого я просто закомментировал следующие две строки кода:
C-подобный:
invoke SetConsoleCursorPosition,[hndl],040010h ;// позиция курсора в консоли
cinvoke printf,<'%s',0>,passStr ;// показать текущий наш пароль
Если клавишей(F7) зайти в эти функции отладчиком, то обнаружим сотни строк кода, при помощи которых система реализует эти функции. Непосредственно полезная нагрузка брутфорса уходит в этом случае на 10-ый план, поэтому и время увеличивается соответственно. Эта разница показана на скринах ниже:
5. Заключение
Здесь я попытался освятить только одну сторону этого интересного направления. За бортом остались "атаки по-словарю", и это чуть другая тема со-своим подходом. Как нибудь в другой раз мы обязательно вернёмся к ней, поскольку в своей массе юзеры используют для паролей именно осмысленные слова, а вот фантазии у них не хватает. Поэтому перебор по-словарю всегда идёт на шаг впереди брутфорса.
В скрепку я положил три исполняемых файла, коды которых мы рассмотрели выше. В версии(1) имеется возможность выбрать алфавит для паролей "Base16/32/64", зато сам алгоритм перебора оставляет желать лучшего. Во-второй версии алго оптимизирован, но алфавит включает в себя все печатные символы, из-за чего процесс перебора может длиться дольше. Без проблем можно было заточить версию(2) и на выбор алфавита, с построением соответствующих таблиц. Но оставлю это вам в качестве дз. Здесь ставлю точку, и до скорого!
Вложения
Последнее редактирование: