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

Чужая информация всегда привлекала к себе внимание, а когда к ней нет прямого доступа, возникает желание прибрести этот доступ контробандным путём. Если социальная инженерия и прочая игра на чувствах не срабатывает, остаётся идти на таран и использовать метод "грубой силы", что в дословном переводе на инглиш звучит как "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) и на выбор алфавита, с построением соответствующих таблиц. Но оставлю это вам в качестве дз. Здесь ставлю точку, и до скорого!
 

Вложения

  • PassBrute.zip
    2,8 КБ · Просмотры: 534
Последнее редактирование:

Crazy Jack

Green Team
08.07.2017
335
68
еше раз перечетаю ваши статьи, вы какого года выпуска?
 

MLNK

Mod. Ethical Hacking
Red Team
23.01.2018
548
698
Ты просто зверь, читать в кайф.
 
  • Нравится
Реакции: Mogen
Мы в соцсетях: