Стандартный алгоритм восстановления паролей к PWL-файлам
Считываем из исходного PWL-файла по смещению 0x24C - CheckSeed (DWORD).
-//- по смещению 0x252 - CryptoSign (16 байт).
-//- по смещению 0x262 - CheckSign (16 байт).
Формируем массив X (размером 64 байта) следующим образом:
0xFFFFFFFF (DWORD)
Логин в верхнем регистре
0x0 (1 байт)
CheckSeed (DWORD)
0x80 (1 байт)
0x0 (по адрес 0x37 включительно)
По адресу 0x38 записываем (0x48 + (длина логина << 3)) (DWORD)
0x0 (до конца массива)
Выполняем MD5 (массив X), получаем массив Cnt (16 байт), т.е. производим свертку логина.
Формируем массив Y (размером 64 байта) следующим образом:
Логин в верхнем регистре
0x0 (0x11 байт)
0x80 (1 байт)
0x0 (по адрес 0x37 включительно)
По адресу 0x38 записываем (0x88 + (длина логина << 3)) (DWORD)
0x0 (до конца массива)
Формируем массив Z (размером 64 байта) следующим образом:
Пароль
0x0 (1 байт)
Cnt (16 байт)
0x80 (1 байт)
0x0 (по адрес 0x37 включительно)
По адресу 0x38 записываем (0x88 + (длина пароля << 3)) (DWORD)
0x0 (до конца массива)
Выполняем MD5 (массив Z), получаем массив Key (16 байт), т.е. производим свертку пароля.
Выполняем RC4, используя в качестве ключа Key.
Копируем во временный буфер Temp (32 байта):
CryptoSign (16 байт)
CheckSign (16 байт)
Выполняем процедуру XorT (массив M, массив Temp), получаем модифицированный массив Temp.
Копируем первые 16 байт из массива Temp в буфер Y с адреса (длина логина + 1)
Выполняем MD5 (массив Y), получаем массив TempKey (16 байт).
Сравниваем 16 байт массива TempKey и вторые 16 байт из массива Temp и если они не равны, то инкремент пароля и возврат на пункт 7, иначе - пароль верный!
Внимание! А на "десерт" - самое вкусненькое ;) - то, что позволило в свое время программе PWLInside существенно увеличить скорость перебора и по этому параметру здорово "оторваться" от своих конкурентов.
Оказывается, для быстрой оценки - тот пароль или нет, можно пункты 10...14 не выполнять, а значит и не вызывать второй раз MD5, который (как уже было сказано) выполняется около 300 тактов, что дает прирост скорости еще на 20-25%!
Дело вот в чем.
Если внимательно присмотреться к процедуре декодирования ресурсов, то мы увидим, что с помощью массива M после пункта 9 стандартного алгоритма при проходе с правильным паролем мы декодируем:
начальный буфер Temp - это не что иное, как 32 байта исходного PWL-файла с адреса 0x252,
смещения (адреса) блоков ресурсов с адреса 0x272,
и, наконец, сами ресурсы.
Поэтому можно не выполнять операции над CryptoSign и CheckSign (для проверки правильности пароля), а просто проверить после XOR'a адрес первого блока ресурсов, который находится по адресу 0x272.
Если он лежит в диапазоне 0x292...(длина PWL-файла), то есть вероятность, что этот пароль верный! Для окончательной же проверки правильности пароля нужно выполнить пункты 10...14, т.к. только в этом случае (когда совпадут все 16 байт массива Temp), можно быть уверенным, что это именно верный пароль.
И, соответственно, наоборот - если предварительная проверка показала, что адрес первого блока ресурсов декодировался неверно, то пароль однозначно неверный и можно смело идти на перебор следующего. :)
Практика показала, что процент "ложных" попаданий, т.е. когда после XOR'а первого адреса получили смещение, похожее на правду, крайне низок, и временем выполнения повторных полных "перепроверок" можно пренебречь.
Поэтому код предварительной оценки пароля в упрощенном виде будет выглядеть так:
...
//Массив M перерабатывается с помощью RC4
...
byte t; //Временная ячейка
byte A = 0; //Здесь будем получать новый псевдослучайный индекс
WORD Offset = (WORD *)&lpFile[0x272]; //Зашифров. адрес 1-го блока ресурсов
WORD Len = XXX; //Длина исходного PWL-файла
//
for (int i = 1; i < 35; i++)
{
A += M[i]; // Вычисляем новый индекс для обмена байтами
t = M[i];
M[i] = M[A]; //Меняем местами i-й байт и байт по вычисленному индексу A
M[A] = t;
//
t = M[i] + M[A]; //Вычисляем еще один индекс
if (i == 33)
((byte *)&Offset)[0] ^= M[t]; //Декодируем 1-й байт адреса
if (i == 34)
((byte *)&Offset)[1] ^= M[t]; //Декодируем 2-й байт адреса
}
//
if ((Offset > 0x291) && (Offset < Len))
{
//Есть вероятность, что пароль верный,
//делаем полную перепроверку по пунктам 10...14
}
else
{
//Однозначно пароль неверный, переходим на новый пароль
}
Как видим, переработка массива M все равно остается, но(!) - первые 32 итерации мы НИЧЕГО не XOR'им, а декодируем ТОЛЬКО на 33 и 34 итерациях адрес блока ресурсов.
Таким образом, зачем нам делать пункты 10...14 и проверять 16 байт, когда можно выполнить "упрощенный" вариант процедуры XorT и проверить всего 2 байта! ;)
При всем моем уважении к Microsoft, однако, это еще одна их "дыра" в реализации качественных методов хранения паролей пользователей!
Итак, что мы получили в среднем:
- ~40 тактов на инициализацию массива M.
- ~300 тактов на первый проход MD5.
- ~1000 тактов на проход RC4.
- ~150 тактов на все остальное (формирование массива Z, инкремент пароля, "упрощенная" функция XorT, проверки и пр.)
В итоге мы получаем суммарное время обработки одного пароля около 1500 тактов
на всех типах процессоров, что приведены в начале статьи, кроме процессора Pentium 4. :(
Дело в том, что микроархитектура P4 по сравнению с P-II/III была существенно переработана - увеличено время выполнения команды (до 20 стадий), изменились размеры строк кэша данных и кода и еще ряд "усовершенствований", поэтому этот код (в особенности, реализация алгоритма RC4) для P4 не является оптимальным и в следующей версии PWLInside будет модифицирован. При этом на процессорах AMD, даже последних, таких проблем не возникает - на Athlon'e XP 1700+ (1466МГц) с ядром ThoroughBred программа исправно выдает около миллиона паролей в секунду. Вот так AMD делает аналоги процессоров Intel в чем-то лучше, чем сама Intel. :)