ОБСУЖДЕНИЕ Статья 

ASM. Брутфорс – алгоритмы и оптимизация

4 ответов 16,1 тыс.
Автор темы
Стоит сразу уточнить рамки материала: здесь пароль, который мы брутим, хранится в открытом виде, и сравнение идёт функцией lstrcmp() напрямую. На практике чаще встречается ситуация, когда система хранит не сам пароль, а его хеш (MD5, SHA и т.п.), и перебор ведётся против этого хеша – тогда на каждой итерации цикла кандидат нужно сначала пропустить через хеш-функцию, что становится основным узким местом по скорости. Логика перебора и оптимизации, разобранные ниже, при этом остаются теми же – меняется только финальное сравнение. Ну а с точки зрения защиты против лобового перебора работают те же ограничители: лимит попыток ввода, соль в хеше и достаточная длина пароля.

Чужая информация всегда привлекала к себе внимание, а когда к ней нет прямого доступа, возникает желание прибрести этот доступ контробандным путём. Если социальная инженерия и прочая игра на чувствах не срабатывает, остаётся идти на таран и использовать метод "грубой силы", что в дословном переводе на инглиш звучит как "Brute-Force". За периметром конкретной задачи брутфорс лишён смысла – каждый случай требует индивидуального подхода. А потому предлагаю ознакомиться с алгоритмами его реализации на программном уровне, чтобы самостоятельно решать проблемы по мере их поступления.

Оглавление:

1. Общие сведения о криптологии;

2. Практика – генератор паролей;

3. Практика – брутфорс паролей;

4. Варианты оптимизации кода;


• выбор по индексу,​

• распределение по потокам Thread.​

5. Заключение.

-------------------------------------------------

1. Криптология – общие сведения

Разработкой методов крипта и декрипта информации занимается "криптология", которая пошла по двум направлениям – криптография и криптоанализ. Первая (криптография) – это наука о методах обеспечения конфиденциальности, т.е. изучает криптосистемы и способы шифрования данных. Криптоанализ-же занимается вопросами оценки слабых сторон отдельно взятых методов защиты. Соответственно два эти направления являются враждующими сторонами, между которыми идёт не шуточная война. В рамках представленного на ваш суд материала, нас будет интересовать только криптоанализ в виде атаки на шифр, а его обобщённую схему раскрывает рисунок ниже:

Crypto_class.png


Как видим, вариантов у взломщиков много, на все случаи жизни. Это довольно объёмная тема, и попытки охватить сразу весь материал ни к чему хорошему не приведут. Поэтому будем последовательны и рассмотрим только один из методов атак, а заинтересованный читать всегда может почерпнуть базовые сведения о криптоанализе в соответствующей литературе, или например в вики. Чтобы понять, о чём пойдёт речь конкретно в этом треде, рассмотрим определение атак, которые выделены на рисунке выше коричневым цветом.

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 описывает стандарт
RFC-4648, в котором приводится полный его паспорт, в том числе и задействованные в кодировках символы:
Base_ascii.png


Посмотрите на таблицу символов 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  '+/'          ;// +/
Base_mem.png


Будем считать, что забросили алфавиты в память, теперь нужно выбирать из них псевдо-случайные значения для пароля. Как видим, каждый из символов имеет свою позицию в дампе – это "индекс элемента" в массиве. Например по индексу(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'
Pass_gen.png


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'
Brute.gif


Обратите внимание, что чем длиннее алфавит, тем дольше длиться процесс перебора. Например алфавит "Base64" содержит вдвое больше символов, чем "Base32". Соответственно и обходить его в цикле по-времени будет в два раза накладней. Поэтому если пароль состоит только из цифр типа "12345", то и выбирать нужно самый короткий алфавит "Base16" – это на порядок увеличит скорость брутфорса, чем если-бы мы выбрали только для цифр алфавит "Base64".

4. Варианты оптимизации кода

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

Значит по-прежнему имеем в памяти алфавит, из которого последовательно планируем выбирать символы для своего пароля. Здесь, мыщъх посоветовал выстрелить дробью и сразу убить стаю куропаток по такому алго. Мы знаем, что первым символом в алфавите является "А" с кодом 41h=65. А что если использовать код очередного символа, в качестве индекса в алфавите? Правда в этом случае сам алфавит должен иметь строго определённый формат, зато код брутфорса сократится буквально до нескольких байт.

То-есть по адресу(0) в алфавите кладём символ(А) с кодом 41h, а по адресу(41h) вставляем в алфавит следующий символ(B) с кодом 42h. Далее, по адресу(42h) ставим символ(С) с кодом 43h, и т.д. Чтобы по окончании очередного прохода по всему алфавиту на автомате опять перейти в его начало, нужно всего-то закончить алфавит терминальным нулём, который послужит индексом к первому символу(А) алфавита. Всё остальное сделают инструкции XLAT (чтение в AL по индексу) и STOSB (сохранение считанного символа в пароль). Чуть запутано, но надеюсь рисунок ниже прояснит эту ситуацию:
Alpha_Table.png


Таким способом можно сформировать таблицу буквально всех символов, начиная с пробела с кодом 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-ый план, поэтому и время увеличивается соответственно. Эта разница показана на скринах ниже:
SpeddTest.png


5. Заключение

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

В скрепку я положил три исполняемых файла, коды которых мы рассмотрели выше. В версии(1) имеется возможность выбрать алфавит для паролей "Base16/32/64", зато сам алгоритм перебора оставляет желать лучшего. Во-второй версии алго оптимизирован, но алфавит включает в себя все печатные символы, из-за чего процесс перебора может длиться дольше. Без проблем можно было заточить версию(2) и на выбор алфавита, с построением соответствующих таблиц. Но оставлю это вам в качестве дз. Здесь ставлю точку, и до скорого!
 
AI-выжимка (тезисы)
Общие сведения о криптологии :: Криптология делится на криптографию (защита данных) и криптоанализ (поиск слабостей), материал фокусируется на атаках на шифр.
1.1. Атака с известным шифротекстом – "Сiphertext only attack". :: В атаках этого класса взломщик знает алгоритм шифрования, но не знает пароль; три ветки: словарь, полный перебор, частотный анализ.
Полный перебор ключей :: Классический брутфорс порождает пароли циклом, с выбором алфавита (цифры, A..Z, a..z, все символы); полный алфавит гарантирует результат, но время может превысить возраст вселенной.
Практика – генератор паролей :: Автор на FASM пишет генератор паролей на алфавитах Base16/32/64 (RFC-4648), рандом берёт из CryptGenRandom() и выбирает символы инструкцией XLAT.
Практика – программа для брутфорса паролей :: Брутфорсер перебирает комбинации в цикле и сравнивает их через lstrcmp(); за месяц на одной машине реально подобрать пароли не длиннее 8 символов.
Варианты оптимизации кода :: Приём мыщъх'а: символ пароля сам является индексом в таблице алфавита, что сжимает брутфорс до 26 байт кода.
Вывод прогресса через printf() замедляет перебор в разы — сотни строк системного кода на каждую итерацию затмевают полезную нагрузку.

Вложения

Последнее редактирование модератором:
  • Нравится
Реакции: Rukh, vandam 228, JackSparrow и ещё 21
24
еше раз перечетаю ваши статьи, вы какого года выпуска?
 
@Eugene75, вопросы личного характера - в личку.
 
Ты просто зверь, читать в кайф.
 
  • Нравится
Реакции: ROP
1
Ты просто зверь, читать в кайф.
Да не то слово! просто и доходчиво, и да, по поводу вывода на экран, всегда замечал, что с выводом делает дольше, эффект наблюдателя, не хер подсматривать ХDDD
 
  • Нравится
Реакции: ROP
1