|
SAMInside
Скачать v2.6.4.0
Архив обновлен: 14.01.2010 PasswordsPro Скачать v2.5.5.0
Архив обновлен: 28.02.2010 Extreme GPU Bruteforcer Скачать v1.6.1
Архив обновлен: 14.03.2010 Купить лицензию
Словари
Файлов: 74
Библиотека Документов: 501
Rainbow-таблицы Таблиц: 132
Генератор хэшей Алгоритмов: 135
Подписка на новости Подписчиков: 1056
Дополнительные сервисы
|
Циклический инкремент паролейВ данной статье мы рассмотрим один аспект перебора паролей, на котором обычно не заостряют внимание при написании программ-брутфорсеров (программ для подбора паролей), а именно - непосредственно сам инкремент пароля, т.е. формирование следующего пароля для его анализа. Конечно, если проверка пароля (т.е. "тело" программы перебора) выполняется тысячи, десятки тысяч или же миллионы тактов процессора, то можно использовать любой способ инкремента пароля - заметного изменения скорости не будет. Но когда на проверку пароля уходит всего 150...300 тактов процессора и нам нужно быстро сформировать новый пароль, то, конечно же, вопрос оптимального инкремента пароля становится очень и очень актуальным. Сразу оговорюсь, что рассматриваться будет именно вариант полного перебора всех паролей, без семантического анализа рядом стоящих букв, не используя перебор по маске, гибридный перебор и т.д. Т.е., мы собираемся последовательно перебирать комбинации "A", "B", ... "Z", затем - "AA" и т.д. И еще одно условие - изменяемым символом в наших комбинациях будет первый
символ пароля, а не последний, т.е. комбинации будут вида:
Это существенно упрощает код инкремента пароля, да и, в принципе, нет особой разницы в том - слева или справа менять символы. Нам ведь главное - сделать это максимально быстро, не так ли? А, желательно, еще и компактно. Способов инкремента пароля существует немало. Самым распространенным является "индексный" метод инкремента, когда пароль представлен в виде индексов используемых символов. Т.е. при алфавите "ABCDEFGHIJKLMNOPQRSTUVWXYZ" пароль ADMIN будет представлять собой массив значений {1, 4, 13, 9, 14, 0}, где ноль - конец пароля. Или же массив вида {0, 3, 12, 8, 13}, если для хранения длины пароля используется дополнительная переменная. Данный метод, конечно же, является неплохим, но автор предлагает рассмотреть другой метод, называемый "циклическим" (и ниже мы рассмотрим, почему он так назван). Этот метод, помимо того, что является крайне быстрым, является еще и очень компактным инкрементом, что с успехом позволяет использовать его как в крупных проектах, так и миниатюрных программках, где необходим перебор паролей. Также в этом варианте "накладные расходы" на изменение длины пароля и изменение некрайнего символа также минимальны. Данный метод с успехом применяется во всех программах перебора паролей на сайте www.InsidePro.com. Примеры кода будут даны в синтаксисе языка Ассемблер, встроенного в Visual C++ 6.0 и, соответственно, самого VC++ 6.0. Итак, что мы имеем:
static char szPassword[256]; // Буфер для хранения текущего пароля
static char szAlphabet[256]; // Алфавит, т.е. набор символов для перебора ... strcpy(szAlphabet, "ABC"); // Для примера возьмем короткий алфавит "ABC" ZeroMemory(szPassword, sizeof(szPassword)); // Начинаем перебирать с пустого пароля ... Нам нужно:
Идея нашего инкремента следующая - попробуем-ка мы получать следующий символ ("B", к примеру) автоматически на основе того, что мы знаем предыдущий символ - "A". Для этого нам нужно, чтобы символ "A" был как-то связан с символов "B". Может, в виде таблицы? Да, нам нужна именно таблица, в которой по адресу 65 (код символа "A") будет расположен сам символ "B"! Поэтому строим таблицу (массив bAlphabet) размером 256 байт следующего вида:
0-й байт = код символа "A", т.е. 65
65-й байт = код символа "B", т.е. 66 66-й байт = код символа "C", т.е. 67 67-й байт = ноль. (все остальные байты не важны, т.к. к ним обращений не будет) Тогда перебор символов (в развернутом виде) будет происходить следующим образом:
mov ebx,offset bAlphabet // Адрес нашей таблицы
xor eax,eax // В AL - ноль movzx eax,byte ptr [ebx+eax] // В AL - символ "A" movzx eax,byte ptr [ebx+eax] // В AL - символ "B" movzx eax,byte ptr [ebx+eax] // В AL - символ "C" movzx eax,byte ptr [ebx+eax] // В AL - ноль movzx eax,byte ptr [ebx+eax] // В AL - снова символ "A" ... // И так далее (Примечание: использование команд "xor eax,eax" и "movxz eax,..." вместо "mov al,0" и "mov al,[ebx+eax] позволяет избежать больших штрафов на процессорах Intel, возникающих при чтении 32-битного регистра после модификации его 8-битной части). Или же, используя однобайтовую команду XLAT, наш пример будет выглядеть так:
mov ebx,offset bAlphabet
xor eax,eax xlat // В AL - символ "A" xlat // В AL - символ "B" xlat // В AL - символ "C" xlat // В AL - ноль xlat // В AL - снова символ "A" ... // И так далее Таким образом, мы видим, что без лишних команд сравнения (хотя они, конечно, потом понадобятся, но их будет всего две) и лишнего кода мы осуществляем циклический "обход" символов нашего алфавита, причем переход от последнего символа к первому происходит автоматически! Код для инициализации нашей таблицы может быть, к примеру, таким:
static unsigned char bAlphabet[256];
... int i = 0, k = 0; ZeroMemory(bAlphabet, sizeof(bAlphabet)); while (TRUE) { bAlphabet[k] = (unsigned char)szAlphabet[i]; if (!szAlphabet[i]) break; k = (unsigned char)szAlphabet[i]; i++; } Также нам необходимо проверить алфавит, задаваемый пользователем, на предмет одинаковых символов, т.к. при построении такой таблицы с алфавитом "ABCADEF", к примеру, будет происходить следующее - после символа "A" получим символ "B", затем - "C", после него - "A" и вместо ожидаемого "D" мы снова выйдем на символ "B" и т.д. Таким образом, часть алфавита после второго символа "A" будет просто проигнорирована и нам необходимо перед перебором проанализировать алфавит и просто удалить повторные вхождения одинаковых символов. Теперь же приведем полный код циклического инкремента с подробными комментариями:
__asm
{ L0: ... // Проверка пароля, т.е. тело программы-брутфорсера ... mov edi,offset szPassword // Адрес строки с паролем mov ebx,offset bAlphabet // Адрес таблицы L1: movzx eax,byte ptr [edi] // Текущий символ пароля cmp al,0 // Не изменилась ли длина пароля? je L4 L2: xlatb // Следующий символ пароля cmp al,0 // Не последний ли символ алфавита? je L3 mov [edi],al // Новый символ пароля ... // Инкремент кол-ва обработанных паролей и другой завершающий код ... jmp L0 // На начало глобального цикла L3: xlatb // AL = снова первый символ алфавита stosb // Его сохранение в [EDI], инкремент EDI jmp L1 L4: inc dword ptr [nLen] // Увеличение длины пароля jmp L2 } Если же нам проверка на изменение длины пароля не нужна (может быть, мы перебираем до нажатия пользователем Ctrl+С, или же до установки какого-либо флажка, или же просто останавливаем наш поток по внешнему воздействию и пр.), то наш код можно упростить:
...
mov edi,offset szPassword mov ebx,offset bAlphabet L1: movzx eax,byte ptr [edi] xlat cmp al,0 je L3 mov [edi],al ... // Инкремент кол-ва обработанных паролей и другой завершающий код ... jmp L0 L3: xlatb stosb jmp L1 Ну как? ;) Затрачено каких-то 25 байт и у нас полноценный перебор паролей из любых 8-битных символов (в том числе и служебных 0x01...0x1F) и с любого начального пароля (при условии, что он состоит из символов нашего алфавита). Добавляем еще несколько байт и мы можем отслеживать длину текущего пароля! Теперь посмотрим на время, затраченное на инкремент пароля. Но сразу оговоримся, что если время исполнения нам более важно, чем объем кода, то сделаем следующую замену на более быстрый код: команды "mov ebx,offset bAlphabet" и "xlat" заменим на команду "movzx eax,byte ptr [bAlphabet+eax]". Тогда переход от комбинации "A" к "B" будет таким (по командам):
...
mov edi,offset szPassword // Адрес пароля L1: movzx eax,byte ptr [edi] // Текущий символ пароля - "A" movzx eax,byte ptr [bAlphabet+eax] // В AL - следующий символ пароля, т.е. "B" cmp al,0 // У нас НЕ равно нулю je L3 // Переход не происходит mov [edi],al // Сохраняем новый символ "B" вместо "A" ... Смотрите - всего 6 команд процессора. На большинстве процессоров такой код исполнится за ... 4-5 тактов. Вы можете измерить это время и сами, вставив до и после этого кода команды RDTSC. А вот почему это происходит:
Более того - в реальном коде декодирование (и исполнение) подобных простых команд тесно связано с последующим и предыдущим кодом, что может снизить среднее время выполнения этих 6 команд до 3-4 тактов. Итак, 3-4 такта на замену одного пароля другим - совсем даже неплохой результат! Но, конечно же, можно развивать эту тему и дальше - если основной код перебора (т.е. непосредственной проверки пароля - верный он или нет), написанный на Ассемблере, построить так, что текущий символ пароля (который оставался в регистре AL) сохранять на протяжении всего цикла перебора (конечно же, для этого можно использовать и другой регистр), тогда нам не придется его же считывать из памяти заново, что даст совокупную экономию еще в один такт! Теперь поговорим о "накладных расходах", возникающих при переходе на соседний символ и при изменении длины пароля. Это тоже немаловажный вопрос, т.к. если на эти переходы будет затрачено 20-30 тактов (к примеру), то при длине алфавита в 26 символов среднее время инкремента пароля увеличится на 1 такт. А если больше 20-30 тактов? Здесь нужен более сложный подход, чем при простом переходе от "A" к "B", но все-таки мы дадим несколько рекомендаций для снижения временных затрат.
И в итоге - немного арифметики. Пусть среднее время цикла проверки одного пароля - 300 тактов процессора.
Видите - снизив время, затраченное на работу с одним паролем, всего лишь на несколько тактов, ваша программа сможет перебирать быстрее на сотню (или даже сотни) тысяч паролей в секунду! И еще один совет, касающийся подсчета количества обработанных паролей. Да-да, читатель может сказать, что тут-то и так все ясно. Конечно, ясно, но и здесь можно выиграть пару-тройку тактов (если мы стремимся максимально оптимизировать процесс перебора). Для накопления количества перебранных паролей обычно используется две переменных типа DWORD, т.к. при больших скоростях (миллионы паролей/сек) 4 миллиарда паролей (размер типа данных DWORD) будут обработаны за несколько десятков минут, что, конечно же, явно недостаточно. Итак, обычно счетчик паролей увеличивают так:
DWORD dwLow, dwHigh;
... dwLow++; if (dwLow == 0) dwHigh++; Или, на Ассемблере:
inc dword ptr [dwLow]
jnz L1 inc dword ptr [dwHigh] L1: ... Что мы видим - 3 команды, из которых одна - команда проверки, причем практически всегда результат проверки будет TRUE и процессору придется "перепрыгивать" на команду по адресу L1, что влечет за собой штрафы и перезаполнение конвейера. Либо же сделать проверку не JNZ, а JZ, перенести по адресу L1 команду "inc dword ptr [dwHigh]" и добавить еще одну команду - JMP обратно... Но можно сделать такой трюк, используя следующую пару команд:
...
add dword ptr [dwLow],1 adc dword ptr [dwHigh],0 ... В первой команде мы инкрементируем младший DWORD счетчика, а во второй - увеличиваем старший DWORD на ... ноль? Да, на ноль, но только если не было переполнения младшего DWORD'a счетчика. Если же оно было, то включится флаг переноса "C" и команда ADC прибавит в значению dwHigh ноль и значение этого флага, т.е. единицу. Что нам и нужно. И декодируется/выполняется наша пара команд за ... 1 такт. Кстати, хитрые парни из Microsoft тоже не зря получают свою зарплату.
...
unsigned __int64 qwCount; // Сразу используем переменную типа QWORD ... qwCount++; ... и сделать компиляцию с оптимизацией (не забудьте включить создание ASM-листинга), то вы можете увидеть, что команда "qwCount++;" откомпилируется в ту же пару команд ADD/ADC. Причем, это явно не "могучий интеллект" компилятора, а одна из "изюминок" оптимизации, заранее заложенных в компилятор его разработчиками. Автор будет рад, если приведенные выше примеры позволят хоть на чуть-чуть, но увеличить скорость работы ваших программ, а также, если у вас получится и дальше развить показанные выше идеи оптимизации перебора паролей.
#include "stdio.h"
#include "windows.h" int main(int argc, char* argv[]) { static char szPassword[256]; ZeroMemory(szPassword, sizeof(szPassword)); static char szAlphabet[256]; strcpy(szAlphabet, "ABC"); static unsigned char bAlphabet[256]; ZeroMemory(bAlphabet, sizeof(bAlphabet)); int i = 0, k = 0; while (TRUE) { bAlphabet[k] = (unsigned char)szAlphabet[i]; if (!szAlphabet[i]) break; k = (unsigned char)szAlphabet[i]; i++; } while (TRUE) { __asm { pushad mov edi,offset szPassword mov ebx,offset bAlphabet L1: movzx eax,byte ptr [edi] xlat cmp al,0 je L3 mov [edi],al jmp L5 L3: xlat stosb jmp L1 L5: popad } printf("%s\n", szPassword); } return 0; } Листинг 1. Программа генерации паролей.
|
Copyright ©2003-2010, InsidePro Software. All rights reserved. Thursday, 18th of March 2010, 15:28:02. |