Привет всем!
В конце лета мне удалось-таки ухватить себе немного свободного времени и потратить на любимое дело.
Итак, сегодня в нашей программе: взлом crackme#02 от [KSDR].
Не будем дергать кота за хвост - приступим к делу.
Давайте посмотрим на крэкми:
На небольшой форме мы видим два текстовых поля и две кнопки.
В поле 'Name' вводим имя пользователя, а в поле 'Password', соответственно, пароль пользователя.
Потом жмем кнопку 'CHECK' и проверяем правильный ли пароль мы ввели.
Я проверил несколько пар имя\пароль (от балды), но мне ничего не выдало...
Кнопка 'About' выводит следующую информацию:
Теперь давайте посмотрим на крэкми в отладчике:
На скрине выше мы видим точку входа (4011A0) программы (да и весь "подготовительный" код). По коду можно легко догадаться о том, что форма создается на основе ресурсов.
Есть форма - значит, есть код обработки сообщений. Адрес обработчика передается функции DialogBoxParam четвертым параметром:
А в коде передача данного параметра находится по адресу 4011AE и выглядит как PUSH 4011C8.Code:int DialogBoxParam( HINSTANCE hInstance, // handle to application instance LPCTSTR lpTemplateName, // identifies dialog box template HWND hWndParent, // handle to owner window DLGPROC lpDialogFunc, // pointer to dialog box procedure LPARAM dwInitParam // initialization value );
Теперь перейдем на код по адресу 4011C8.
На рисунке Вы можете видеть комментарии в виде кода на С-подобном языке.
Самое важное, что можно здесь увидеть: при нажатии на кнопку 'CHECK' вызывается код по адресу 401000.
Я запустил программу, ввел пару AbreC/1165165806 и поставил бряк на 401000.
Теперь посмотрим на этот участок:
Итак, содержимое текстового поля 'Name' копируется в память по адресу 403004. Так как целых 64 байта, начиная с адреса 403004, выделены для хранения имени пользователя, давайте назовем эту область памяти (переменную) cName:
char cName[64]; // &cName[0] = 403004
Сразу после вызова функции, проверяется содержимое регистра EAX:
CMP EAX, 4
В данном случае EAX содержит длину строки, прочитанной из поля 'Name'. Если длина имени пользователя меньше 4 символов - выходим из процедуры. То есть ничего не выполняется.
А вот если имя пользователя равно 4 и более символам, тогда выполняются два вызова:
CALL 0040109A
CALL 00401027
Я уже исследовал содержимое этих процедур и обозвал их кое-как (смотрите в комментариях).
Код по адресу 40109A назовем GetCodeHash. И вот почему:
Берется содержимое поля 'Password', копируется в область памяти по адресу 40302С и длиной в 64 байта. Область назовем cCode:
char cCode[64]; // &cCode[0] = 40302C
Далее следует код:
То же самое можно представить в виде кода на С:Code:004010B3 |. BE 2C304000 MOV ESI,crackme#.0040302C 004010B8 |. 33C0 XOR EAX,EAX 004010BA |. 33FF XOR EDI,EDI 004010BC |. 33DB XOR EBX,EBX 004010BE |> B0 0A /MOV AL,0A 004010C0 |. 8A1E |MOV BL,BYTE PTR DS:[ESI] 004010C2 |. 84DB |TEST BL,BL 004010C4 |. 74 0B |JE SHORT crackme#.004010D1 004010C6 |. 80EB 30 |SUB BL,30 004010C9 |. 0FAFF8 |IMUL EDI,EAX 004010CC |. 03FB |ADD EDI,EBX 004010CE |. 46 |INC ESI 004010CF |.^ EB ED \JMP SHORT crackme#.004010BE 004010D1 |> 5B POP EBX 004010D2 |. 58 POP EAX 004010D3 \. C3 RETN
При выходе из процедуры в регистре EDI будет наш nCodeHash. Хотя на самом деле это никакой не хэш (пока что).Code:void GetCodeHash() { GetDlgItemText( hWnd, 1002, cCode, 64 ); int nCodeHash = 0, i = 0; while( cCode[i] ) { nCodeHash *= 0xA; nCodeHash += ( cCode[i] - 0x30 ); i++; } return; }
Выходим из GetCodeHash и влетаем в следующую процедуру - CheckCodeHash. Как и следует из названия, здесь будет проверяться код на валидность (наполовину, т.к. окончательная проверка происходит в процедуре Base64Decode).
Рассмотрим код процедуры:
Почти то же самое (в моем видении) выглядело бы так:
Как видите, если после двух циклов в eax (после сдвига) оказывается число 0x484E, то вызываются еще две процедуры XorCipher и Base64Decode, после чего вызывается MessageBox для вывода сообщения.Code:void CheckCodeHash() { nHashCopy = nCodeHash; int eax = 0x0; eax = cName[3]; eax <<= 8; eax += cName[2]; eax <<= 8; eax += cName[1]; eax <<= 8; eax += cName[0]; int ecx = cName[1]; do { nCodeHash = nHashCopy; nCodeHash <<= ecx; nCodeHash ^= eax; nCodeHash &= -1; ecx -= 0x3; eax = nCodeHash; } while( ecx > 0 ); int edx = 0x2010; ecx = 0x42; do { nHashCopy = edx; nHashCopy <<= ecx; nHashCopy ^= eax; nHashCopy &= -1; ecx -= 0x5; eax = nHashCopy; } while( ecx > 0 ); int ebx = eax; eax <<= 0x10; if( eax == 0x484E ) { XorCipher; Base64Decode; MessageBoxA ( hWnd, cMsg, "Attention - a confidential code!", 0x40 ); } return; }
Рассмотрим первую процедуру:
Думаю, я достаточно хорошо прокомментировал код... Ну а если в общих чертах, то проводится операция XOR между зашифрованным сообщением (cCipher) и содержимым регистра BX. Результат (в моем понимании) помещается в cXoredCipher.
Теперь пара слов о процедуре Base64Decode.
В самом начале процедуры проверяется содержимое cXoredCipher на наличие двух знаков "равно" в конце (что указывает на Base64). Если они присутствуют, в AL и DL закладываются определенные значения, которые "способствуют" правильной дешифровке конечного сообщения cMsg.
Далее по коду идет дешифровщик.
Конечно, одно дело писать статью во время взлома и совсем другое, когда это уже сделано. Поэтому скажу пару слов про то, как я находил пароль.
Когда впервые видишь какой-то ассемблерный код, не сразу приходит в голову что там к чему.
Но сразу бросилось в глаза то, что сначала берется текст из поля Name, и только потом Code (и это если имя состоит из четырех или более символов).
Видно сразу также то, что над Code проводятся какие-то преобразования. И только потом понимаешь, что это.
Самое сложное в крэкми - это процедура CheckCodeHash. Мне пришлось немало времени ломать над ней голову. Особенно безжалостна эта процедура к тем, кто пытается обратить первый цикл. Хотя вторая такая же по устройству, хоть ее и можно превратить в константу (как подметил BoRoV).
Наконец, понимаешь, что единственный способ по-быстрому достать ключ - это брутфорс.
Первый раз я прошил крэкми так, чтобы генерировался такой nCodeHash, при котором после второго цикла и сдвига влево на шестнадцать битов в EAX было бы число 0x484E.
Да, это у меня получилось, но сообщение я так не прочитал...
Потом только я уделил больше внимания процедуре XorCipher, где использовалось содержимое BX для XOR'а шифра.
Но я еще думал, что только брутфорсом можно достать это самое содержимое BX. И продолжал думать до тех пор, пока не заметил проверку в начале процедуры Base64Decode.
Тут, зная, что XOR имеет одно важное свойство, а именно:
X xor Key = Y
Y xor Key = X
X xor Y = Key
Я быстро вычислил искомое содержимое BX, на которое надо проXORить cCipher, чтобы в конце cXoredCipher появились знаки "==".
Мне пришлось подменить только одно сравнение, а именно:
CMP EAX,484E
Поменял на:
CMP EAX,484EE8C3
и удалил побитовый сдвиг влево (SHR EAX,10).
И брутфорс заработал правильно =)
Небольшая оговорка по поводу выводимого сообщения. Сообщение проходит такую "эволюцию":
cCipher --> cXoredCipher --> cMsg
На этом статья завершается.
AbreC © r0 Crew
Спасибо [KSDR] за занимательный крэкми!
P.S. Ссылка на исходник кейгена: keygen.txt



Reply With Quote
Thanks
