| Циклический инкремент пароля |
|
|
|
| Автор ][aker | |
| 11.07.2008 г. | |
|
В данной статье мы рассмотрим один аспект перебора паролей, на котором обычно не заостряют внимание при написании программ-брутфорсеров (программ для подбора паролей), а именно - непосредственно сам инкремент пароля, т.е. формирование следующего пароля для его анализа.
Конечно, если проверка пароля (т.е. "тело" программы перебора) выполняется тысячи, десятки тысяч или же миллионы тактов процессора, то можно использовать любой способ инкремента пароля - заметного изменения скорости не будет. Но когда на проверку пароля уходит всего 150...300 тактов процессора и нам нужно быстро сформировать новый пароль, то, конечно же, вопрос оптимального инкремента пароля становится очень и очень актуальным. Сразу оговорюсь, что рассматриваться будет именно вариант полного перебора всех паролей, без семантического анализа рядом стоящих букв, не используя перебор по маске, гибридный перебор и т.д. Т.е., мы собираемся последовательно перебирать комбинации "A", "B", ... "Z", затем - "AA" и т.д. И еще одно условие - изменяемым символом в наших комбинациях будет первый символ пароля, а не последний, т.е. комбинации будут вида: Это существенно упрощает код инкремента пароля, да и, в принципе, нет особой разницы в том - слева или справа менять символы. Нам ведь главное - сделать это максимально быстро, не так ли? А, желательно, еще и компактно. :) Способов инкремента пароля существует немало. Самым распространенным является "индексный" метод инкремента, когда пароль представлен в виде индексов используемых символов. Т.е. при алфавите "ABCDEFGHIJKLMNOPQRSTUVWXYZ" пароль ADMIN будет представлять собой массив значений {1, 4, 13, 9, 14, 0}, где ноль - конец пароля. Или же массив вида {0, 3, 12, 8, 13}, если для хранения длины пароля используется дополнительная переменная. Данный метод, конечно же, является неплохим, но автор предлагает рассмотреть другой метод, называемый "циклическим" (и ниже мы рассмотрим, почему он так назван). Этот метод, помимо того, что является крайне быстрым, является еще и очень компактным инкрементом, что с успехом позволяет использовать его как в крупных проектах, так и миниатюрных программках, где необходим перебор паролей. Также в этом варианте "накладные расходы" на изменение длины пароля и изменение некрайнего символа также минимальны. Данный метод с успехом применяется во всех программах перебора паролей на сайте http://www.insidepro.com/. Примеры кода будут даны в синтаксисе языка Ассемблер, встроенного в Visual C++ 6.0 и, соответственно, самого языка C++ 6.0. Итак, что мы имеем: static char szPassword[256]; // Буфер для хранения текущего пароля Нам нужно:
Идея нашего инкремента следующая - попробуем-ка мы получать следующий символ ("B", к примеру) автоматически на основе того, что мы знаем предыдущий символ - "A". Для этого нам нужно, чтобы символ "A" был как-то связан с символов "B". Может, в виде таблицы? Или же ... да-да, нам нужна именно таблица, в которой по адресу 65 (код символа "A") будет расположен сам символ "B"! Поэтому строим таблицу (массив bAlphabet) размером 256 байт следующего вида: 0-й байт = код символа "A", т.е. 65 Тогда перебор символов (в развернутом виде) будет происходить следующим образом: mov ebx,offset bAlphabet // Адрес нашей таблицы (Примечание: использование команд "xor eax,eax" и "movxz eax,..." вместо "mov al,0" и "mov al,[ebx+eax] позволяет избежать больших штрафов на процессорах Intel, возникающих при чтении 32-битного регистра после модификации его 8-битной части). Или же, используя однобайтовую команду XLAT, наш пример будет выглядеть так: mov ebx,offset bAlphabet Таким образом, мы видим, что без лишних команд сравнения (хотя они, конечно, потом понадобятся, но их будет всего две) и лишнего кода мы осуществляем циклический "обход" символов нашего алфавита, причем переход от последнего символа к первому происходит автоматически! Код для инициализации нашей таблицы может быть, к примеру, таким: static unsigned char bAlphabet[256]; Также нам необходимо проверить алфавит, задаваемый пользователем, на предмет одинаковых символов, т.к. при построении такой таблицы с алфавитом "ABCADEF", к примеру, будет происходить следующее - после символа "A" получим символ "B", затем - "C", после него - "A" и вместо ожидаемого "D" мы снова выйдем на символ "B" и т.д. Таким образом, часть алфавита после второго символа "A" будет просто проигнорирована и нам необходимо перед перебором проанализировать алфавит и просто удалить повторные вхождения одинаковых символов. Теперь же приведем полный код циклического инкремента с подробными комментариями: __asm Если же нам проверка на изменение длины пароля не нужна (может быть, мы перебираем до нажатия пользователем Ctrl+С, или же до установки какого-либо флажка, или же просто останавливаем наш поток по внешнему воздействию и пр.), то наш код можно упростить: ... Ну как? ;) Затрачено каких-то 25 байт и у нас полноценный перебор паролей из любых 8-битных символов (в том числе и служебных 0x01...0x1F) и с любого начального пароля (при условии, что он состоит из символов нашего алфавита). Добавляем еще несколько байт и мы можем отслеживать длину текущего пароля! Теперь посмотрим на время, затраченное на инкремент пароля. Но сразу оговоримся, что если время исполнения нам более важно, чем объем кода, то сделаем следующую замену на более быстрый код: команды "mov ebx,offset bAlphabet" и "xlat" заменим на команду "movzx eax,byte ptr [bAlphabet+eax]". Тогда переход от комбинации "A" к "B" будет таким (по командам): ... Смотрите - всего 6 команд процессора.
Более того - в реальном коде декодирование (и исполнение) подобных простых команд тесно связано с последующим и предыдущим кодом, что может снизить среднее время выполнения этих 6 команд до 3-4 тактов. Итак, 3-4 такта на замену одного пароля другим - совсем даже неплохой результат. Но, конечно же, можно развивать эту тему и дальше - если основной код перебора (т.е. непосредственной проверки пароля - верный он или нет), написанный на Ассемблере, построить так, что текущий символ пароля (который оставался в регистре AL) сохранять на протяжении всего цикла перебора (конечно же, для этого можно использовать и другой регистр), тогда нам не придется его же считывать из памяти заново, что даст совокупную экономию еще в один такт! Теперь поговорим о "накладных расходах", возникающих при переходе на соседний символ и при изменении длины пароля. Это тоже немаловажный вопрос, т.к. если на эти переходы будет затрачено 20-30 тактов (к примеру), то при длине алфавита в 26 символов среднее время инкремента пароля увеличится на 1 такт. А если больше 20-30 тактов? Здесь нужен более сложный подход, чем при простом переходе от "A" к "B", но все-таки мы дадим несколько рекомендаций для снижения временных затрат.
И в итоге - немного арифметики. Пусть среднее время цикла проверки одного пароля - 300 тактов процессора. Видите - снизив время, затраченное на работу с одним паролем, всего лишь на несколько тактов, ваша программа сможет перебирать быстрее на сотню (или даже сотни) тысяч паролей в секунду! И еще один совет, касающийся подсчета количества обработанных паролей. Да-да, читатель может сказать, что тут-то и так все ясно. Конечно, ясно, но и здесь можно выиграть пару-тройку тактов (если мы стремимся максимально оптимизировать процесс перебора). Для накопления количества перебранных паролей обычно используется две переменных типа DWORD, т.к. при больших скоростях (миллионы паролей/сек) 4 миллиарда паролей (размер типа данных DWORD) будут обработаны за несколько десятков минут, что, конечно же, явно недостаточно. Итак, обычно счетчик паролей увеличивают так: DWORD dwLow, dwHigh; Или, на Ассемблере: inc dword ptr [dwLow] Что мы видим - 3 команды, из которых одна - команда проверки, причем практически всегда результат проверки будет TRUE и процессору придется "перепрыгивать" на команду по адресу L1, что влечет за собой штрафы и перезаполнение конвейера. Либо же сделать проверку не JNZ, а JZ, перенести по адресу L1 команду "inc dword ptr [dwHigh]" и добавить еще одну команду - JMP обратно... Но можно сделать такой трюк, используя следующую пару команд: ... В первой команде мы инкрементируем младший DWORD счетчика, а во второй - увеличиваем старший DWORD на ... ноль? Да, на ноль, но только если не было переполнения младшего DWORD'a счетчика. Если же оно было, то включится флаг переноса "C" и команда ADC прибавит в значению dwHigh ноль и значение этого флага, т.е. единицу. Что нам и нужно. И декодируется/выполняется наша пара команд за ... 1 такт. :) Кстати, хитрые парни из Microsoft тоже не зря получают свою зарплату. :) ... и сделать компиляцию с оптимизацией (не забудьте включить создание ASM-листинга), то вы можете увидеть, что команда "qwCount++;" откомпилируется в ту же пару команд ADD/ADC. Причем, это явно не "могучий интеллект" компилятора, а одна из "изюминок" оптимизации, заранее заложенных в компилятор его разработчиками. Автор будет рад, если приведенные выше примеры позволят хоть на чуть-чуть, но увеличить скорость работы ваших программ, а также, если у вас получится и дальше развить показанные выше идеи оптимизации перебора паролей. Удачи всем! Листинг 1. Программа генерации паролей. #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; } |
| След. » |
|---|
/images/logo_left.png)
/images/logo_rigth.png)











