Відновлення паролів до PWL-файлів, Windows 9x/NT, Security & Hack, статті

PolASoft

Зміст

Введення

PWL-файли (або так звані парольні кеші Windows) – це файли з іменами
користувачів комп’ютера і з розширеннями *. PWL (наприклад, Master.PWL,
Sales.PWL тощо), які знаходяться в директорії Windows.

Це файли, в яких зберігаються різні акаунти (тобто поєднання логін / пароль)
даного користувача, тобто паролі до всіх ресурсів, при першому підключенні до
яким у вікні введення пароля був включений прапорець “Запам’ятати пароль”.
Таким чином, у ньому зберігаються паролі до “Розшарені” ресурсів мережі,
паролі на підключення до NetWare / NT-серверів, паролі на Dial-Up-з’єднання і пр.

Функція запам’ятовування паролів в Windows реалізована давно і була введена з
“Благої” метою – полегшення роботи користувачів. І дійсно – навіщо
при кожному підключенні до провайдера пароль “q6Rf8UI24dq”:),
коли можна один раз ввести його з папірця, а потім забути про цей пароль
взагалі. Таким чином, Windows збирає всі паролі користувача, шифрує
їх за допомогою логіна (імені користувача) і поточного пароля користувача
(Тобто того пароля, який вводиться при завантаженні Windows)
і зберігає в цьому PWL-файлі.

Природно, всі ці дані зашифровані певними алгоритмами, і для їх
дешифровки необхідно знати пароль користувача – а це фактично пароль
на вхід в Windows. А так як людям властиво забувати свої паролі, то підбір
пароля, по-перше, дозволяє його “згадати”, а по-друге, дозволяє
переглянути список акаунтів цього користувача, які Windows зберігає в
цьому PWL-файл, наприклад, там може бути і забутий пароль на підключення до
провайдеру. 😉

У даній статті будуть описані ті методи оптимізації при підборі паролів до
PWL-файлів, які були використані при створенні програми
PWLInside і які
дозволили зайняти програмі лідируюче місце за швидкістю роботи серед
аналогічних програм.

І відразу обмовлюся, що мова піде тільки про PWL-файлах операційних систем
Windows’OSR2 / 98/ME, т.к. в PWL-файлах ОС Windows’3 .11/95 методи шифрування
набагато простіше і підбір пароля будь-якої складності – справа однієї години роботи будь
програми з відновлення до них паролів, у тому числі і PWLInside.

Вся інформація в тексті про час виконання фрагментів коду в тактах дана для
наступних типів процесорів:

  1. Intel Pentium MMX / II / III і все Celeron’и з цих родин.
  2. AMD K6-II/III, все Duron’и і Athlon’и.

Така інформація дається як усереднене час виконання на всіх цих типах
процесорів.

Приклади коду, наведені нижче, дані в синтаксисі Microsoft Visual С + + v6.0 і
MASM v7.0.

Формат PWL-файлів Windows’OSR2 / 98/ME

Інформація про формат PWL-файлів компанією Microsoft ніде і ніколи не
документувалася, і, хоча в Інтернеті можна знайти багато різних документів
за форматами PWL-ок, вся ця інформація взята з практичного використання
цих файлів і в основному написана авторами програм, аналогічних
PWLInside.

Детально розглянемо як приклад наступний PWL-файл:

0000: E3 82 85 96 03 00 00 00 02 00 01 00 00 00 00 00
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00D0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00E0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0100: 00 00 00 00 00 00 00 00 00 0D 03 FF FF FF FF FF
0110: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0120: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0130: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0140: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0150: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0160: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0170: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0180: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0190: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
01A0: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
01B0: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
01C0: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
01D0: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
01E0: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
01F0: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
0200: FF FF FF FF FF FF FF FF 52 02 00 00 00 00 00 00
0210: 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00
0220: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0240: 01 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00
0250: 00 00 88 51 47 58 2C 74 13 7C 6F D7 9E 9D 58 59
0260: 0B 3A A5 22 85 22 94 80 58 F9 61 00 B6 51 72 28
0270: D5 D7 3A 58 23 03 DD BC A7 4B E7 E2 71 65 66 CE
0280: 3F 58 FC 59 76 02 F0 12 8E 5F 79 94 39 E0 36 29
0290: 13 B9 8E 3B A1 F3 74 D4 78 38 01 E0 B5 FE DE A3
02A0: 80 CC 4E B7 67 1D 7C 36 7B C5 AA 76 4C D0 8E EE
02B0: 28 78 8A BB 7A 5A 81 2C AB 29 79 97 33 89 60 79
02C0: F7 6C 1C 72 1B 33 0A 09 F2 7E E4 3A A6 BE F4 C6
02D0: AE 06 DE 31 67 BB EA 7B D5 CA AE 01

Тепер уважно подивимося, з яких полів складається файл:

  1. Відразу скажу наступні обмеження PWL-файлів: у файлі може
    знаходиться максимум 16 блоків ресурсів. Максимальне
    кількість ресурсів у файлі – 255. Це обмеження самої Windows. У кожному
    блоці може розташовуватися будь-яку кількість ресурсів, але їх сумарне
    кількість не може бути більше 255. І ще одне обмеження PWL-файла
    – Те, що він не може бути більше 64Кб.
  2. Отже, перше, що ми бачимо – це сигнатура (тобто перші 4 байти файлу),
    яка дорівнює 0x968582E3, Відразу ж зауважу, що у PWL-файлів від
    Windows’3 .11/95 сигнатура інша – 0x4E464DB0.
  3. По зсуву 0x4 слід DWORD з невідомим вмістом.
  4. По зсуву 0x8 слід WORD. Він визначає загальну кількість
    ресурсів у файлі. У нашому прикладі – 2 ресурсу.
  5. Починаючи з адреси 0x00A до адреси 0x109 розташовується
    дивна таблиця розміром 255 байт. Яку вона містить інформацію,
    відомо лише Microsoft. Є припущення, що там зберігаються номера
    ресурсів, хоча ця таблиця для декодування даних з файлу в
    принципі не потрібна.
  6. Починаючи з адреси 0x109 до адреси 0x208 знаходиться
    інша таблиця (теж розміром 255 байт), в якій зберігається така
    інформація: будь-який байт із цієї таблиці рівний i (крім 0xFF)
    означає, що i-й блок містить ресурси. Кількість однакових байт
    рівних i в даній таблиці відображає кількість ресурсів в i-му блоці.
    У нашому прикладі – 1-й байт в таблиці показує, що у нас є
    ресурси в 13-м (0x0D) блоці, а 2-й байт в таблиці показує, що у
    нас є ресурси в 3-му блоці ресурсів.
  7. За адресою 0x208 знаходиться DWORD (у нас він дорівнює 0x00000252),
    який визначає усунення CryptoSign. До речі, я в цьому полі інших значень
    не бачив ні в одній PWL-ке!
  8. З адреси 0x20C по 0x24C розташовується масив CryptoSeed [16],
    він необхідний для декодування всіх 16 блоків ресурсів.
  9. За адресою 0x24C розташовується CheckSeed (DWORD), з якого і
    починається декодування PWL-файла.
  10. Далі йдуть два нульових байта. Яку вони несуть функцію – невідомо.
  11. За адресою 0x252 розташовується масив CryptoSign (16 байт).
  12. За адресою 0x262 розташовується масив CheckSign (16 байт) –
    цей масив разом з CryptoSign є “контрольним значенням”
    для визначення правильності пароля. Їх застосування розглянемо нижче.
  13. За адресою 0x272 розташовується масив з 15 WORD’ов – це адреси 15
    блоків ресурсів, починаючи з другого. Адреса ж першого ресурсу завжди один і
    Того ж і становить 0x290. Ці адреси вже є зашифрованими.
    Подивимося, що це будуть за байти після декодування правильним паролем:

    0270: .. .. 92 02 94 02 96 02 B2 02 B4 02 B6 02 B8 02
    0280: BA 02 BC 02 BE 02 C0 02 C2 02 C4 02 D8 02 DA 02

    Як ми бачимо – дійсно там знаходяться адреси! Ці адреси ніколи не можуть
    бути однаковими і виходить, що якщо блок ресурсів порожній, то він все
    одно займає 2 байти – це ми бачимо на початкових адресах: 0x292,
    0x294 – ці значення посилаються на “сміття”, ресурси же
    в цьому файлі знаходяться за адресами 0x296 … 0x2B2 і 0x2C4 … 0x2D8 – це видно по
    тому, що різниця між цими сусідніми адресами більше двох байт і тому ми вже
    відзначали, у нас 3-й і 13-й блок мають ресурси (див. пункт 6).

  14. А з адреси 0x290 починаються безпосередньо ресурси. Ці дані також
    зашифровані. Після дешифрування ми побачимо, що з адрес 0x296 і 0x2C4 дійсно
    є щось, схоже на ресурси:)

    0290: 4C 9C 50 94 C9 82 1A 00 0A 00 08 00 01 03 43 52 |L.P………..CR
    02A0: 49 53 54 49 41 4E 5C 44 68 65 6C 6C 67 61 74 65 |ISTIAN\Dhellgate
    02B0: E3 A8 CF DD 80 8A 8D 9A 1B 97 6B B9 BD F0 AE 9A |….?…..k…..
    02C0: 5C D4 20 88 12 00 05 00 05 00 00 04 4D 41 50 49 |\. ………MAPI
    02D0: 00 4D 41 50 49 00 28 F3 1D B2 90 80             |.MAPI.(….?

Як правильно декодувати ресурси та їх формат буде розглянуто нижче.

Опис алгоритмів, використовуваних для шифрування PWL-файлів

Нижче детально опишемо ті алгоритми, які використовуються при шифруванні даних в
PWL-файлах. Т.к. докладні описи RC4 і MD5 можна знайти в різних джерелах, то я
опишу їх поверхово, тому що припускаю, що читач або знає принципи роботи
цих алгоритмів, або сам зможе знайти в Інтернеті докладні їх опису,
хоча б у тих же RFC.

RC4

Короткий опис: на вході маємо ключ розміром 16 байт, на виході отримуємо масив
з 256 байт, в якому рівномірно і псевдовипадкових розподілені байти від 0 до 255,
причому їх розподіл залежить від вхідного ключа:

byte t; / / Тимчасова осередок
byte A = 0; / / Тут будемо отримувати новий псевдовипадковий індекс
byte M [256]; / / Наш формований масив
byte Key [16]; / / Оригінальний ключ, за допомогою нього будемо формувати масив M

for (int i = 0; i < 256; i++)
M [i] = i; / / Послідовно заповнюємо масив значеннями від 0 до 255

for (int i = 0; i < 256; i++)
{

A + = (M [i] + Key [i% 16]); / / Обчислюємо новий індекс для обміну байтами
   t = M[i]; 
M [i] = M [A]; / / Міняємо місцями i-й байт і байт за обчисленим індексу A
   M[A] = t; 
}

Далі за цим алгоритмом байтами з масиву M за допомогою завершальній процедури RC4 з
застосуванням операції XOR розшифровуються будь-які необхідні дані.

MD5

MD5 – це не що інше, як операція згортки 64 байт в 16.

Подивимося як ця функція описана в офіційному документі –
RFC1321 з невеликими спрощеннями і нашими
коментарями:


#define S11 7
#define S12 12
#define S13 17
#define S14 22
#define S21 5
#define S22 9
#define S23 14
#define S24 20
#define S31 4
#define S32 11
#define S33 16
#define S34 23
#define S41 6
#define S42 10
#define S43 15
#define S44 21

/* F, G, H and I are basic MD5 functions */
/ * Основні макроси перетворення * /

#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))

/* ROTATE_LEFT rotates x left n bits */
/ * Цей макрос – це всього лише елементарна команда циклічного зсуву ROL
на АСМЕ! Оригінальний варіант ротації на С працює швидше на процесорах з
архітектурою RISC. Для x86 процесорів краще використовувати команду ROL * /

#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))

/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
Rotation is separate from addition to prevent recomputation */
/ * Основні макроси трансформації значень a, b, c і d * /

#define FF(a, b, c, d, x, s, ac) { \
(a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \
(a) = ROTATE_LEFT ((a), (s)); \
(a) += (b); \
}

#define GG(a, b, c, d, x, s, ac) { \
(a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \
(a) = ROTATE_LEFT ((a), (s)); \
(a) += (b); \
}

#define HH(a, b, c, d, x, s, ac) { \
(a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \
(a) = ROTATE_LEFT ((a), (s)); \
(a) += (b); \
}

#define II(a, b, c, d, x, s, ac) { \
(a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \
(a) = ROTATE_LEFT ((a), (s)); \
(a) += (b); \
}

/* MD5 basic transformation. Transforms state based on block */
/ * Безпосередньо сам алгоритм MD5 * /

static void MD5Transform (state, block)
{

UINT4 a,b,c,d,state[4], x[16];

a = state[0] = 0x67452301;
b = state[1] = 0xefcdab89;
c = state[2] = 0x98badcfe;
d = state[3] = 0x10325476;

/* Round 1 */
FF (a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */
FF (d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */
FF (c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */
FF (b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */
FF (a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */
FF (d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */
FF (c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */
FF (b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */
FF (a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */
FF (d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */
FF (c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
FF (b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
FF (a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
FF (d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
FF (c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
FF (b, c, d, a, x[15], S14, 0x49b40821); /* 16 */

/* Round 2 */
GG (a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */
GG (d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */
GG (c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */
GG (a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */
GG (d, a, b, c, x[10], S22, 0x2441453); /* 22 */
GG (c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */
GG (a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */
GG (d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
GG (c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */
GG (b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */
GG (a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */
GG (c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */
GG (b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */

/* Round 3 */
HH (a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */
HH (d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */
HH (c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
HH (b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
HH (a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */
HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */
HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */
HH (b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
HH (a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
HH (d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */
HH (c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */
HH (b, c, d, a, x[ 6], S34, 0x4881d05); /* 44 */
HH (a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */
HH (d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
HH (c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
HH (b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */

/* Round 4 */
II (a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */
II (d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */
II (c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
II (b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */
II (a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
II (d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */
II (c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
II (b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */
II (a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */
II (d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
II (c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */
II (b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
II (a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */
II (d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */
II (b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */

state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
}

У підсумку отримуємо з вхідного масиву x (16 DWORD’ов) масив
state (Всього 4 DWORD’a).

Завершальна процедура XorT алгоритму RC4

Ця частина RC4 (не описана вище), яка декодує необхідні дані
за допомогою масиву M:


/ / Масив M переробляється за допомогою RC4

byte t; / / Тимчасова осередок
byte A = 0; / / Тут будемо отримувати новий псевдовипадковий індекс

for (byte i = 1; i < 33; i++)
{
A + = M [i]; / / Обчислюємо новий індекс для обміну байтами
    t = M[i]; 
M [i] = M [A]; / / Міняємо місцями i-й байт і байт за обчисленим індексу A
    M[A] = t;
    //
t = M [i] + M [A]; / / Обчислюємо ще один індекс
Data [i – 1] ^ = M [t]; / / Декодируем 32 байта масиву Data
}

Стандартний алгоритм відновлення паролів до PWL-файлам

  1. Зчитуємо з вихідного PWL-файла по зсуві 0x24C – CheckSeed (DWORD).
  2. – / / – По зсуві 0x252 – CryptoSign (16 байт).
  3. – / / – По зсуві 0x262 – CheckSign (16 байт).
  4. Формуємо масив X (розміром 64 байта) наступним чином:
    • 0xFFFFFFFF (DWORD)
    • Логін в верхньому регістрі
    • 0x0 (1 байт)
    • CheckSeed (DWORD)
    • 0x80 (1 байт)
    • 0x0 (по адресу 0x37 включно)
    • За адресою 0x38 записуємо (0x48 + (довжина логіна <<3)) (DWORD)
    • 0x0 (до кінця масиву)
  5. Виконуємо MD5 (масив X), отримуємо масив Cnt (16 байт), тобто виробляємо згортку логіна.
  6. Формуємо масив Y (розміром 64 байта) наступним чином:
    • Логін в верхньому регістрі
    • 0x0 (0x11 байт)
    • 0x80 (1 байт)
    • 0x0 (по адресу 0x37 включно)
    • За адресою 0x38 записуємо (0x88 ​​+ (довжина логіна <<3)) (DWORD)
    • 0x0 (до кінця масиву)
  7. Формуємо масив Z (розміром 64 байта) наступним чином:
    • Пароль
    • 0x0 (1 байт)
    • Cnt (16 байт)
    • 0x80 (1 байт)
    • 0x0 (по адресу 0x37 включно)
    • За адресою 0x38 записуємо (0x88 ​​+ (довжина пароля <<3)) (DWORD)
    • 0x0 (до кінця масиву)
  8. Виконуємо MD5 (масив Z), отримуємо масив Key (16 байт), тобто виробляємо згортку пароля.
  9. Виконуємо RC4, використовуючи як ключ Key.
  10. Копіюємо в тимчасовий буфер Temp (32 байта):
    • CryptoSign (16 байт)
    • CheckSign (16 байт)
  11. Виконуємо процедуру XorT (масив M, масив Temp), отримуємо модифікований масив Temp.
  12. Копіюємо перші 16 байт з масиву Temp в буфер Y з адреси (довжина логіна + 1)
  13. Виконуємо MD5 (масив Y), отримуємо масив TempKey (16 байт).
  14. Порівнюємо 16 байт масиву TempKey і другі 16 байт з масиву Temp і якщо вони не рівні,
    то інкремент пароля і повернення на пункт 7, інакше – пароль вірний!

Алгоритм декодування ресурсів з PWL-файла з правильним паролем

  1. Після знаходження правильного пароля проганяємо функцію XorT з індексами
    не 1 … 33, а 33 … 63. Таким чином ми декодуємо 15 Адреса блоків з ресурсами.
    Повинні вийти значення типу 0x292, 0x294 і т.д. Як ми пам’ятаємо, адреса 1-го
    блоку завжди дорівнює 0x290. Таким чином, у нас буде масив Res [17] типу WORD,
    в перше значення – 0x290, далі 15 декодовані адрес, а останній WORD –
    це розмір файлу (у прикладі вище це буде значення 0x2DC).
  2. Далі слід цикл на 16 (перевірка блоків з ресурсами), на початку його
    обчислюємо різницю між сусідніми адресами N – якщо різниця між ними
    дорівнює 2, то перехід на наступну адресу.
  3. Якщо N> 2, то даний i-й блок містить ресурси.
  4. Формуємо новий масив X (розміром 64 байта) наступним чином:
    • 0xFFFFFFFF (DWORD)
    • Логін в верхньому регістрі
    • 0x0 (1 байт)
    • CryptoSeed [i] (DWORD) – це значення беремо з масиву за адресою 0x20C, причому i – це номер блоку ресурсів.
    • 0x80 (1 байт)
    • 0x0 (по адресу 0x37 включно)
    • За адресою 0x38 записуємо (0x48 + (довжина логіна <<3)) (DWORD)
    • 0x0 (до кінця масиву)
  5. Виконуємо MD5 (масив X), отримуємо масив Cnt (16 байт), тобто виробляємо згортку логіну з потрібним CryptoSeed.
  6. Формуємо масив Z (розміром 64 байта) наступним чином:
    • Пароль
    • 0x0 (1 байт)
    • Cnt (16 байт)
    • 0x80 (1 байт)
    • 0x0 (по адресу 0x37 включно)
    • За адресою 0x38 записуємо (0x88 ​​+ (довжина пароля <<3)) (DWORD)
    • 0x0 (до кінця масиву)
  7. Виконуємо MD5 (масив Z), отримуємо масив Key (16 байт), тобто виробляємо згортку пароля.
  8. Виконуємо RC4, використовуючи як ключ Key.
  9. І тепер отриманим масивом M починаємо декодувати весь блок з ресурсами
    довжиною N процедурою, аналогічною XorT. Причому починаємо використовувати масив M
    також з 1-го значення (не з нульового (!)) до 255, якщо ресурс більше 255
    символів, то i “перевалює” байтове кордон і вже масив M
    починає використовуватися з нуля, а не з одиниці.
  10. Подивимося на наведеному вище прикладі структуру першого з наших декодовані ресурсів:

    0290: .. .. .. .. .. .. 1A 00 0A 00 08 00 01 03 43 52 |…………..CR
    02A0: 49 53 54 49 41 4E 5C 44 68 65 6C 6C 67 61 74 65 |ISTIAN\Dhellgate

    Її формат такий:

    • довжина ресурсу (WORD), у нашому прикладі – 26 (0x1A) байт.
    • довжина логіна (WORD), у нашому прикладі – 10 символів.
    • довжина пароля (WORD), у нашому прикладі – 8 символів.
    • BYTE, призначення якого поки точно не відомо.
    • тип зберігається ресурсу (BYTE):

    • 1 = NT Domain
    • 2 = NT Server
    • 3 = Shared
    • 4 = MAPI
    • 6 = Dial-Up
    • 18 = NetWare
    • 19 = WWW

    Далі розташовуються логін, а після нього – пароль.

    У нашому прикладі – тип ресурсу "Shared", Логін "CRISTIAN\D", Пароль "hellgate".

    (Примітка: для Shared-Ресурсів запис CRISTIAN\D буде означати наступне:
    CRISTIAN – Ім’я комп’ютера, а D – Ресурс, наданий для загального користування.)

  11. Далі аналізуємо поточний блок з ресурсами, поки не перевалили за N,
    “Поглядаючи” в таблицю за адресою 0x109, т.к. в PWL-файлах між
    блоками ресурсів дуже часто буває “сміття” (несповідимі шляхи
    Microsoft), а в цій таблиці буде точна вказівка ​​- в якому блоці скільки
    ресурсів.

Оптимізація алгоритмів відновлення паролів

Ось і починається найцікавіше. 😉
Подивимося, що ж ми можемо зробити для підвищення швидкодії вищенаведених алгоритмів.

RC4 (1-а частина)

1. Відразу ж впадає в очі ініціалізація масиву значеннями від 0 до 255, яке
відбувається при кожному новому значенні ключа (тобто фактично при кожному новому паролі).
Як же можна зробити її більш ефективною?

Вихід є – ініціалізувати масив не побайтно (256 команд), а, наприклад,
використовуючи 32-бітові регістри процесора, по 4 байти за 64 команди –
і дійсно, виграш за часом буде в 4 рази (звичайно ж, якщо
масив M вирівняний мінімум по межі DWORD’a). А чи є ще більш
“Широкі” регістри, щоб за одну команду “махом”
записувати більше у байт? Регістри FPU відпадають, тому що операції з ними
виконуються дуже довго, залишаються, звичайно ж, MMX-регістри:

.DATA
qwInit DQ 0706050403020100h
qwAdd DQ 0808080808080808h

.CODE
mov edi,offset M+128
movq mm0,qword ptr [qwInit]
movq mm1,qword ptr [qwAdd]
movq [qword ptr edi-128],mm0

paddb mm0,mm1
movq [qword ptr edi-120],mm0
paddb mm0,mm1
movq [qword ptr edi-112],mm0

paddb mm0,mm1
movq [qword ptr edi+112],mm0
paddb mm0,mm1
movq [qword ptr edi+120],mm0

Щоб не писати 31 однаковий фрагмент коду, набагато простіше використовувати
зарезервований макрос Асемблера IRP, Тоді останні рядки коду
можна замінити на наступну конструкцію:

IRP i,<-15,-14, … ,14,15>
   paddb mm0,mm1
   movq [qword ptr edi+(i*8)],mm0
ENDM

Разом отримуємо – на ініціалізацію масиву з 256 байт витрачаємо 66 команд
процесора, які виконуються за ~ 35-40 тактів, Тому що команди PADDB і MOVQ виконуються
синхронно.

Неважко здогадатися, ЩО навернув би на місці цього циклу будь-який компілятор С,
якщо цей код писати не на АСМЕ. 😉

У читача, напевно, виникло питання – чому ми инициализируем EDI не на
початок масиву M, а на його середину?

Просто річ у тому, що при такому варіанті програми додаткове зміщення до
EDI буде приводити до збільшення довжини команди MOVQ всього на один байт
(Знаковий діапазон -128 … +127) і команда отримує довжину в 4 байта.
А якщо, приміром, додати до EDI +256, то зсув буде розширено до DWORD’a
і довжина команди збільшиться до 7 байт. Використання ж більш коротких команд
є кращим, тому що йде більш “щільне” заповнення буфера
передвибірки команд і більш оптимальне їх декодування процесорами.

І ще – вдумливий читач скаже, що є ще більш “широкі”
XMM-регістри – ті, які використовуються в наборах команд SSE (які, до речі,
підтримують і Athlon’и) і SSE2 від Intel. Використовуючи їх, можна оперувати з
пам’яттю блоками по 16 байт за такт!

Дійсно, це так. В PWLInside не була включена підтримка SSE
тільки з причини недостатнього поширення таких процесорів в то
час, коли створювалася програма. Цілком можливо, що в такій
версії програми підтримка SSE буде присутній.

RC4 (2-а частина)

Тепер розглянемо “перетасовку” байт в масиві M, використовуючи вхідний ключ.

Тут виходить така картина – на обробку одного байта масиву M необхідно
мінімум 7 команд, покажемо це на прикладі однієї ітерації:

xor eax, eax; в AL будемо зберігати індекс A
mov ebp,offset Key
mov edi,offset M

add al,byte ptr[ebp+i] ;A += Key[i % 16]
mov dl, byte ptr [edi + i]; Прочитуємо M [i]
add al,dl ;A += M[i]
movzx esi,al
mov bl, byte ptr [edi + esi]; Прочитуємо M [A]
mov byte ptr [edi+esi],dl
mov byte ptr [edi+i],bl

Причому така конструкція має ряд особливостей:

  1. Використання команди MOVZX ESI, AL усуває наступний конфлікт на процесорах Intel:
    звернення до 32-бітному регістру відразу після модифікації його молодшій (16 – або 8-бітної)
    частини. У цьому випадку до часу виконання команди додається кілька тактів штрафу.
    До речі, на процесорах від AMD таких штрафів немає. 🙂
  2. Конфлікти з читання / запису в одну кеш-лінійку процесора.
    Відомо, що при зверненні до комірки пам’яті процесор зчитує з пам’яті (або кешу
    2-го рівня) не одну цю клітинку, а цілий рядок (наприклад, 32 байти), яку і
    розміщує в кеші 1-го рівня. При цьому ВСЯ цей рядок позначається як доступна або
    тільки для читання, або для запису. Тому, якщо прочитати байт за адресою X, а
    потім записати байт за адресою (X + 1), то незважаючи на те, що байт за адресою (X + 1) вже
    в кеші (!), процесор після виконання першої команди повинен спочатку “звільнити”
    весь рядок, а лише потім знову її завантажити, але вже для запису в неї, що,
    природно, призводить до штрафів. Але, тому що алгоритм формує рівномірний розподіл
    байт, то такі конфлікти виникають нечасто.
  3. Можливо, є ще ряд конфліктів саме по роботі з пам’яттю, тому що такі алгоритми –
    це для процесорів не самий “зручний” код:)

У підсумку така конструкція виконується в середньому за 5 тактів, тому що всі ці
команди прості і на процесорах P-II/III від Intel можуть декодувати всіма 3-ма
декодерами. Таким чином, декодування може
відбуватися по 3 команди за такт, що
частково компенсує вищеописані штрафи.

А на процесорах від AMD цифра в 5 тактів виходить через те, що деякі з цих
команд не залежать один від одного і після декодування наступна команда починає
виконуватися, коли попередня ще закінчує своє виконання в конвеєрі.

Разом на весь алгоритм RC4 йде: 5 x 256 = приблизно 1280-1300 тактів.

Звичайно ж, мається на увазі, що ніяких циклів немає і код весь “розгорнуть”.

Начебто нічого з цим вдіяти вже не можна, але допитливий розум підказує, що і
тут можна “рибку половити”. 😉

І дійсно – розглянемо ту частину коду, де обчислюваний індекс A
підсумовується з байтом з масиву Key.

Індекс – це ж один байт (!), Який підсумовується з поточним байтом M [i].
А потім підсумовується з байтом з масиву Key.

А якщо відразу підсумувати два байти (однією командою) для 2-х різних ключів і,
відповідно, 2-х різних паролів не в 8-бітному регістрі, а в 16-бітному?
Сказано – зроблено.

У результаті швидкість впала.
Та-а-а, значить приріст від цього не перекрив збільшення “накладних
витрат “через те, що тепер у програмі формується два масиви Z,
два масиви M і пр. Плюс штрафи від використання 16-бітових команд в 32-бітному
коді.

Ну що ж, а якщо одночасно 4 байта для 4-х паролів? 😉

Та-а-ак, тут вже відносно початкового (однопарольного) варіанту є
приріст продуктивності на 3-5%.

А якщо ще “ширше”? 🙂

“Звичайно ж, MMX!” – Скажете Ви і будете праві! :))

І ось ми приходимо до того варіанту, який і був реалізований в PWLInside
і дав приріст швидкості щодо початкового варіанту на 20-25%. Все просто:

  1. Формуємо 8 масивів M, які розташовуються в пам’яті таким чином:
    – 0-й байт 1-го масиву
    – 0-й байт 2-го масиву

    – 0-й байт 8-го масиву
    – 1-й байт 1-го масиву
    – 1-й байт 2-го масиву

    – 1-й байт 8-го масиву
    і так далі.
  2. Формуємо 8 паролів, що йдуть “поспіль” при переборі обраним алфавітом.
  3. На основі цих восьми паролів через виклик MD5 формуємо 8 масивів Key в такому ж порядку:
    – 0-й байт масиву Key від 1-го пароля
    – 0-й байт масиву Key від 2-го пароля

    – 0-й байт масиву Key від 8-го пароля
    – 1-й байт масиву Key від 1-го пароля
    – 1-й байт масиву Key від 2-го пароля

    – 1-й байт масиву Key від 8-го пароля
    і так далі.
  4. І ось що отримуємо – тепер всі 8 індексів для різних паролів зберігаються в
    одному (!) MMX-регістрі, в різних його 8-бітних частинах.
    Пам’ятайте рядок:

    A += (M[i] + Key[i % 16]);

    Тепер цей фрагмент коду для 8-ми паролів (і ключів, відповідно) виконується
    двома MMX-командами:

    mov edi,offset M
    mov ebp,offset Key

    pxor mm0, mm0; Початкові значення Обнуляємо

    ; Ось цей фрагмент:
    paddb mm0, qword ptr [edi]; Усі вісім A + = M [i]
    paddb mm0, qword ptr [ebp]; Усі вісім A + = Key [i% 16]

    Ось у чому незаперечна перевага MMX-регістрів – можливість оперувати
    з 8 -, 16 – або 32-бітними частинами всього регістру незалежно один від одного!

  5. Після цього виділяємо байти з MM0 яким способом, формуємо адреси, міняємо
    байти в масивах M і далі все те ж саме, що вище.
  6. І, звичайно ж, ініціалізація масивів M буде така ж, що наведена вище –
    по 40 тактів на один пароль. Але 8 разів.

Такий прийом привів до того, що на прохід алгоритму RC4 на один ключ / пароль йде
~ 980 … 1000 тактів, що означає в середньому менше 4-х тактів на обробку одного байта в
масиві M!

А якщо використовувати XMM-регістри, то можна аналізувати 16 паролів одночасно і це
дасть ще приріст швидкості, але тільки на тих процесорах, які підтримують набір
команд SSE. Спробуйте, може наступний “прорив” у галузі відновлення
паролів до PWL-файлів зробите саме Ви! 😉

MD5

Перший момент в оптимізації цього алгоритму – це заміна наступних
макросів із 4-ма логічними операціями:

#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))

На макроси з 3 двома логічними операціями:

#define F(x, y, z) ((((y) ^ (z)) & (x)) ^ (z))
#define G(x, y, z) ((((x) ^ (y)) & (z)) ^ (y))

Звичайно ж, це дасть невеликий, але все-таки приріст у швидкості.

Другий ж момент полягає в наступному: придивимося уважно до пункту 7
стандартного алгоритму і побачимо, що якщо ми обмежимо пароль для перебору 16-ма
символами (тому що на більшу глибину перебір вже марний), то при формуванні буфера
Z у нас залишаються кілька DWORD’ов, які заповнені нулями. Подивимося, які саме:

16 символів пароля + 1 байт нуля + Cnt (16 байт) + 1 байт (0x80) = разом 34 байти або 9 DWORD’ов.

Далі у нас йдуть нулі, до кінця ж масиву зайнятий ще тільки один DWORD – x [14].

І фактично порожні DWORD’и – це x [9], x [10], x [11], x [12], x [13] і x [15].

Тому, по всіх 64 ітераціях (4 блоки по 16 рядків) алгоритму MD5, скрізь, де до
результату додається якийсь із цих DWORD’ов, то його можна не додавати взагалі –
там же нуль!

Цей нехитрий маневр збільшує швидкість даної конкретної реалізації MD5 ще на 8-10%.

До речі, цю ідею можна розвинути і далі:

  • При довжині пароля 8 .. 12 символів ігнорувати x [8].
  • При довжині пароля 4 .. 8 символів ігнорувати x [7] і x [8].
  • При довжині пароля 1 .. 4 символи ігнорувати x [6], x [7] і x [8].

Що ще трохи, звичайно, але все-таки збільшить швидкість перебору.

Разом – після всіх цих маніпуляцій час виконання алгоритму MD5 стало займати ~280-320
тактів
, Тобто в середньому близько 5 тактів на кожну з 64 ітерацій. Непогано. 🙂

Але, вже створюючи цю статтю, у мене з’явилися певні думки, як можна ще
оптимізувати цей код. 😉
Цілком можливо, що в наступній версії PWLInside буде ще щось новеньке
в плані оптимізації MD5!

Я також сподіваюся, що вищенаведена інформація щодо оптимізації RC4 і MD5, можливо,
допоможе кому-небудь, хто використовує ці алгоритми у своїх розробках і дозволить з
мінімальними “витратами” зробити ці програми ще більш швидкими:)

Стандартний алгоритм відновлення паролів до PWL-файлам

Увага! А на “десерт” – саме смачненьке;) – те, що дозволило свого
час програмі 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. 🙂

Висновок

Ось і підійшло до кінця наше опис. Сподіваюся, тепер по роботі з PWL-файлами
від Windows’OSR2 / 98/ME ні в кого не залишилося “темних плям”. Видно, що дані
алгоритми можна було б прооптімізіровать ще більше з застосуванням команд
сучасних процесорів, але – операційні системи цих поколінь вже йдуть
“В минуле” – відсоток цих ОС і так вже невисокий, з часом знижується все
більше і більше, і відновлення паролів до PWL-файлів вже не таким актуальним,
як кілька років тому.

Хоча, можливо, варто ще попрацювати у цій галузі. 😉

Зараз основний відсоток ОС сімейства Windows – це Windows’2000, Windows’XP,
а тепер уже й Windows’2003. Так як у всіх цих систем ядро ​​інше (на базі
NT), то і методи зберігання, шифрування, а також відновлення;) паролів
користувачів теж інші.

У них інформація про паролі користувачів зберігається в SAM-файлах, які
являють собою гілку “HKEY_LOCAL_MACHINE \ SAM” реєстру Windows, і паролі
зашифровані іншими алгоритмами, ніж в PWL-файлах.

Але (!) – Незважаючи на всі спроби Microsoft створити максимально надійний
механізм авторизації, всі їхні методи чомусь мають явні огріхи – це
наочно демонструють як методики, описані вище, так і методи зберігання
і шифрування паролів в SAM-файлах.

В операційних системах Windows’NT, починаючи з 6-го сервіс-пака, Windows’2000,
Windows’XP (а по всій видимості і в Windows’2003) застосовується
додаткове шифрування хешей користувачів (які і так отримані
шляхом шифрування паролів певними алгоритмами) з використанням
утиліти Syskey.

Даний механізм успішно проіснував кілька років. Його багато разів намагалися
зламати, але всі спроби були безуспішні, поки цією проблемою не
зацікавились ми з Ocean’ом. 😉

І механізм був “зламаний”! Це наочно демонструє програма
SAMInside.

Але про SAM-файли та методи відновлення паролів до них розповім як-небудь
іншим разом. 🙂

Особливу подяку висловлюю Ocean‘У,
т.к. тільки наша з ним спільна діяльність у цій області привела
до появи на світло таких програм як PWLInside, SAMInside
і ряду інших.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*