Запускаем IDA Pro и открываем фаил под названием: XYZ_KeygenMe20110405.EXE. Через несколько секунд когда она проанализирует фаил - запускаем его и пытаемся ввести имя и пароль - неудачно, программа выводит "Error! Try Again Please!". Чтож на это и не надеялись, возвращаемся в IDA Pro и первым делом заглянем в таблицу импорта, может там что нибуть полезное есть, какие нибудь например интересные вызовы функций.
Глаз сразу цепляется за 3 штуки strcmp - сравнение строк, strlen - длинна строки, strcpy - копирование строк.
Чтож 2 щелкаем на каждой строчк и ставим точки останова. Запускаем программу в отладчике по F9. Точки останова срабатывают до ввода имени и пароля, но нигде не останавливается во время. Вот обидно похоже сравнение идет както по другому. Чтож лезем в строки, может быть там найдем строку с ошибкой. Там совсем ничего нет похожего, надо всетаки придумать как зацепиться за программу. Чтобы начать раскручивать весь клубок.
Снимаем все брякпоинты и перезапускаем программу в IDA Pro, в поле Name вбиваем какой нибудь текст который нам будет проще искать, я например использую комбинацию типа PHPKLmna, Нажимаем Enter, чтобы программа спросила пароль, после чего жмем паузу в IDA PRo.
Теперь можно попробовать поискать свою введенную строчку. Поднимаем ползунок в окне дебага в самый верх, чтобы установить курсор на позицию 00010000, ну или жмем кнопку G и вводим туда адрес 00010000.
Дальше идем в Search->Sequence of bytes и выставляем галочки как на этом скриншете. В поле поиска вбиваем 'PH'.
Нажимаем на OK и смиренно ждем когда завершится процесс поиска. Получиться вот такое окно, если тыкнуть на первую строчку то тутже найдем наше введеное имя, хотя иногда приходится подольше потыкать и поискать.
Ставим на эту строчку брякпоинт и нажимаем продолжить выполнение программы, тоесть F9. Точка останова тут же срабатывает, но нам это не очень интересно, нам нужно чтобы брякпоинт сработал сразу после того как мы введем имя пользователя, по этому нажимаем еще раз F9 и програма снова спрашивает имя. Вводим теперь любое другое имя, жмем enter и снова срабатывает хардварная точка останова. Мы оказываемся в системной библиотеке, выходим из них при помощи CTRL+F7. И оказываемся в мало понятной процедуре, рядом с retn, скорей всего мы слишком глубоко, трейсим по F8 выбираясь на верх.
Трейсим дальше кнопкой F8 и наблюдая за выводом, пока ен выпадет следующая строка, спрашивающая пароль. В строке 00401645 сработывается Call и появляется строчка просящая ввести пароль. Мы уже ближе. Переименовываем вызов в Output_String ставим на него брякпоинт и продолжаем трейсить.
Сразу следующий вызов, это что то похожее на ReadConsole. Эту функцию переименуем в CustomReadCons, пусть будет так, а то больно страшное название. И в комментарий поставим Read Password. Вбиваем любой пароль и продолжаем дальше трейсить по F8 в поисках вывода ошибки. И вот на строке 004018BC срабатывает вывод ошибки в консоль. Судя по имени это таже функция которая вывела нам сообщение об вводе пароля и похоже так как чтение из консоли происходит во вне этой функции то эта функция нужна только для вывода сообщений, если заглянуть внутрь то можно увидеть, что в стек заностся код ошибки и в зависимости от него выводиться разная строка. Выше видно сраванение и условный переход. Снимаем все брякпоинты и ставим на этот условный переход точку останову.
Рестартуем программу и правим этот услвный переход, чтобы он срабатывал в другую сторону от вывода плохова сообщения, но это нам не помогает, чтож печально, идем дальше.
Снова вводим логин и пароль останавливаемся на нашем условном переходе, если поглядеть дальше то можно увидеть безусловный переход с уходом кудато далеко в верх, можно попробовать сделать предположение(особенно если проследить его полностью глазами), что это уход на следующий цикл. Идем туда по F8. Видим наши знакомые функции которые мы переменовали Output_String и CustomReadCons если пройти через них то увидим, что теперь весь процесс начинается на вводе имени, ставим тут тоже точку останова. Теперь нам нада разобраться что делается с именем и как создается серийный номер. Вводим имя, например Bearchik. Если следить за стеком, то тутже можно обнаружить, как в ESP передается указатель на строку с именем.
Если зайти в неизвестную процедуру, то можно увидеть, что она вычисляет количество символов в имени и сравнивает с нулем, нам это не нужено. Трейсим дальше. Еще одна такаяже процедура, но теперь сравнвиается с 14h, в десятичном это значит 20, что в принципе вписывается в требования к имени, еслиб мы вышли за рамки, то программа бы не приняла имя. Ок, с этим разобрались идем дальше. Пара сравнений чтобы первая буква была заглавной, это нам пока тоже не интересно.
А интересно нам где же сама обработка имени. Чтож пока идем опять дальше нажимая F8 и поглядывая в поисках каких нибуть арифметических команд. Для удобства, можно группировать, чото бы был более понятен общий план. Дальше мы упираемся в ввод пароля, похоже работа с именем идет поззже, чтож, вбиваем пароль и трассируем дальше. Мы уже знаем, что unknown_libname_19 это вычисление длинны, так что можно обозвать эту процедуру как нибуть, типа custom_strlen. Видим кучу разных вычислений и один раз сравнивается, длинна пароля и длинна имени.
А вот ниже, уже что то интересное идет. Там появились какието Add с константами. Посмотрим что же там.
А вот и вычисление некой цифры из имени, формула получается следующая:
2 х ДлиннуИмени + 10 - 7 + 2
Тоесть для моего имени получается: Bearchik = 8 символов
2 x 8 + 10 - 7 +2 = 21 в дестяичном или 15h в 16ричном. Проверяем. Совпадает, теперь надо понять где это число применяется. Ставим хардварный брякпоинт на чтение/запись в ячейке памяти куда поместилось это некое число. Сразу после прибавления цифры 2, число помещается по некому адресу следующей командой
00401831 mov [ebp+var_9A], ax
2 раза тыкаем и ставим брякопинт. И опа, останавливаемся на знакомой развилке, если посмотреть в регистры то можно увидеть, что сравнивается число 0Ah и наше 15h, у меня есть огромное подозрение что сравнивается длинна пароля(мой пароль как раз 10 цифр в виде 1234567890) с этой некой цифрой и если он не совпадает, тогда программа выдает ругательное сообщение. Если посмотреть выше, то видно что вычисляется длинна пароля.
Итого мы вычислили, что пароль должен состоять из 21символа для имени Bearchik. Пробуем ввести пароль такой длинны(21символ), получается вот такой: 123456789012345678901
Рестартим программу и вбиваем имя и новый пароль.
Останавливаемся снова на этом брякпоинте и видим, что теперь стрелка показывает в другую сторону, похоже мы узнали какой длинны должен быть пароль. Чтож пойдем дальше потрассируем, может чтонитбуть еще интересного найдем. Будем обращать внимание на развилки, которые отсылают к функции Output_String с параметром 932h, так как это сочетание приводит к ошибке.
А вот и еще одно.
Находим очередную вилку с выбросом нас на ошибку, теперь надо разобраться, что там такое. Если внимательно приглядеться к сравнению:
00401AD6 cmp al, [edx]
То в AL находится первая цифра из моего серийника, это цифра 1 или в HEX - 31, а в [EDX] буква B из имени, я думаю это обозначает, что 1 символ серийника должен совпадать с 1 символом имени. Ставим тут брякпоинт и рестартуем. Серийний номер у нас на текущий момент должен выглядеть следующим образом: B23456789012345678901
Вводим имя пользователя: Bearchik
Пароль: B23456789012345678901
Останавливаемся на нужном нам переходе и видим, что теперь переход не ведет к ошибочному сообщению, значит ставим рядом коментарий, проверка 1 символа пароля. Трейсим дальше по F8 и смотря по сторонам в поисках чего нибудь интересного...
Через некоторое время выходим на похожую процедуру сравнения символов отправляющую на функцию выполнения вывода ошибки и но теперь в al находится второй символ введенного серийника, а в [edx] второй символ имени, значит можно сделать вывод, что вторая буква пароля.
Делаем вывод, что в пароле второй символ должен быть такойже как и в имени. Ставми брякпоинт и перезапускаем программу. Данные для ввода выглядят в текущий момент следующим образом:
Вводим имя пользователя: Bearchik
Пароль: Be3456789012345678901
Можно выключить предыдущие брякпоинты. Воводим имя, пароль и останавливаемся на этом переходе, теперь он работает, в правильную сторону, не выводя ошибки, трассируем дальше и через некоторое время замечаем, что возвращаемся опять к этой же проверке, только теперь сравнивается третий символ, я сделал предположение, что в начале пароля должен находиться имя. Снова перезапускаем программу имя оставляем тоже, а пароль будет у нас таким: Bearchik9012345678901
Останавливаемся на этой проверке и если некоторое время потрейсить, то можно заметить, что я был прав. Цикл делает 8 итераций и после чего уходит на странную процедуру, с какимито вычислениями и главное опять видим Output_String с параметром 932h после развилки. Похоже, это очередная проверка пароля. Пытаемся разобраться, что тут делается. Я прокоментировал в коде для себя, а дальше будет разбор алгоритма на русском.
Итак, что происходит во время генерирования пароля:
call __ZNSsixEj ; получаем указатель на пароль, причем с символа сразу после имени, тоесть после Bearchik mov [ebp+var_10C], eax ; записываем указатель в память movsx ecx, word ptr [ebp+var_A8+2] ; помещаем в eax счетчик символа, в начале он 1 mov [ebp+var_130], ecx ; переносим из ecx счетчик в ячейку памяти mov eax, 55555556h ; заносим в eax 55555556h imul [ebp+var_130] ; умножаем eax на [ebp+var_130] где лежит счетчик mov ecx, edx ; переносим edx в ecx mov eax, [ebp+var_130] ; записываем в eax счетчик из [ebp+var_130] sar eax, 1Fh ; сдвигаем eax в право на 31бит sub ecx, eax ; вычитаем из ecx eax mov eax, ecx ; заносим в eax ecx add eax, eax ; умножаем eax на 2 add eax, ecx ; прибавляем к eax ecx mov ecx, [ebp+var_130] ; записываем в ecx счетчик из[ebp+var_130] sub ecx, eax ; отнимаем из ecx eax mov eax, ecx ; перемещаем в eax ecx cwde mov [esp], eax ; заносим в стек на первую позицию получишийся резултат вычислений call sub_404196 ; вызываем какуюто процедуру которая что то делает результатом mov edx, [ebp+var_10C] ; в edx помещаем адрес текущего символа пароля cmp [edx], al ; сравниваем текущий символ с вернувшимся значением из процедуры jnz short loc_401C18 ; прыгаем по итогам сравненияЕсть у меня подозрение, что мы совсем близко к разгадыванию генерации пароля. Теперь надо зайти в процедуру: 00401C07 call sub_404196 и посмотреть, чтож она делает со значением этих вычислений.
Ну тут все в принципе просто, если процедуре передается 1h тогда возваращем 78h, если 2 то возвращаем 79h, если 0 то возвращаем 7Ah, в любом другом случае возвращаем 30h.
Генерировать пароль руками нам лень, так что пока подменим условный переход:
00401C14 jnz short loc_401C18 тоесть переведем ZF из 0 в 1 и посмотрим какие дальше проверки идут. В определенный момент я вижу, что мы вернулись опять в начало, видно начался цикл проверки следующего символа, посмотрим, где мы можем из него выскочить, нам нада найти сравнение длинны пароля и цифрой следующего символа. Я вот вижу похожее, сравнивается 15h с 9h. 15h это длинна пароля, а 9h это скорей всего текущая позиция.
Изменяем SF с 1 на 0 и нажимаем F9. В окне пишут, что мы правильно подобрали пароль, похоже больше никаких проверок нет и предыдущее длинное вычисления это последнее.
Ну теперь самое простое, надо написать keygen чтобы руками не подбирать каждый символ. А для начала подведем итоги, как генериться серийник.
1. Вычисляется длинна по формуле: 2 х ДлиннуИмени + 10 - 7 + 2
2. В начале пароля должен быть логин целиком
3. Вычисляем некое значение 0,1,2 по формуле и в зависимости от нее подставляем символы с кодам в 16ричной 78h, 79h, 7Ah
Хитрая формула заключается в следующем:
16ричную цифру 55555556h умножаем на текущую позицию пароля, за вычетом имени
Все что попало превысило разрядность и попало в edx сохраняем в ecx
Счетчик сдвигаем в право на 31 бит.
Вычитаем из ecx результат сдвига счетчика.
Переносим в результат в eax.
Умножаем eax на 2.
Прибавляем к eax то что лежит в ecx.
Отнимаем от счетчика получившееся значение.
И в итоге скармливаем
Ну а теперь получив представление как генериться пароль пишем keygen:
.586 .model flat, stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib SerialLengh PROTO:DWORD SerialCreate PROTO:DWORD .data helloMsg db "KeyGen for XYZ_KeygenMe20110405",0dh,0ah, "Created by Bearchik",0dh,0ah, "-",0dh,0ah ;мой банер nameMsg db "Enter name:",0 ; вводим имя serialMsg db "You Serial:",0 ; выводим серийник .data? stdout dd ? cWritten dd ? buffer db 50 DUP (?) nameCount dd ? tmp dd ? serialCount dd ? tmpCount dd ? pbuff dd ? .code start: invoke GetStdHandle, STD_OUTPUT_HANDLE ; получаем хендл вывода mov stdout,eax ; сохраняем хендл, так как он нам понадобится invoke WriteConsole, stdout, ADDR helloMsg, sizeof helloMsg, ADDR cWritten, NULL ; выводим мой банер invoke WriteConsole, stdout, ADDR nameMsg, sizeof nameMsg, ADDR cWritten, NULL ; выводим приглашение ввести имя invoke GetStdHandle, STD_INPUT_HANDLE ; получаем хендл ввода invoke ReadConsole, eax, addr buffer, 20, addr nameCount, NULL ; читаем имя invoke SerialLengh, nameCount ; вычисляем длинну серийника sub eax, nameCount ; отнимаем количество символов от имени add eax, 2h ; отнимаем 2 спец символа окончания строки 0dh и 0ah mov serialCount, eax ; сохраняем что получилось mov eax, offset buffer ; получаем указатель на место где лежит наше имя add eax, nameCount ; добавляем количество символов в имени sub eax, 2h ; уменьшаем на 2 спец символа mov pbuff, eax ; записываем указатель на конец имени next: inc tmpCount ; увеличиваем временный счетчик xor ecx, ecx ; обнуляем ecx mov cl, BYTE PTR [tmpCount] ; записываем в cl данные счетчика mov tmp, ecx ; сохраняем во временной переменной mov eax, 55555556h ; помещаем в eax 55555556h imul tmp ;умножаем на число во временной переменной счетчика mov ecx, edx ; перемещаем в ecx edx mov eax, tmp ; записываем в eax счетчик sar eax, 1fh ; сдвигаем на 31бит eax sub ecx, eax ; отнимаем от ecx eax mov eax, ecx ; переносим ecx в eax shl eax, 1 ; умножаем eax на 2 add eax, ecx ; добавляем к eax ecx mov ecx, tmp ; переносим счетчик в ecx sub ecx, eax ; отнимаем от ecx eax mov eax, ecx ; переносим ecx в eax invoke SerialCreate, eax ; вызываем процедуру определения символа mov ecx, pbuff ; в ecx копируем указатель на позицию в пароле mov BYTE PTR [ecx], al ; в позицию вписываем результат процедуры SerialCreate inc pbuff ; увеличиваем позицию в пароле mov eax,tmpCount ; копируем в eax временный счетчик .IF eax == serialCount ; сравниваем, обработали последний символ? jmp serprint ; если да, то уходим на печать .ENDIF jmp next ; повторяем цикл для следующей позиции serprint: mov ecx, pbuff ; в ecx копируем указател на текущую позицию серийника mov BYTE PTR [ecx], 0dh ; добавляем 0dh в конец mov BYTE PTR [ecx+1h], 0ah ; добавляем 0ah в конец invoke WriteConsole, stdout, ADDR serialMsg, sizeof serialMsg, ADDR cWritten, NULL ; выводим You serial: invoke WriteConsole, stdout, ADDR buffer, sizeof buffer, ADDR cWritten, NULL ; выводим сам пароль invoke ExitProcess, 0 ; корректно завершаем программу SerialCreate proc calc:DWORD .IF calc == 1 ; если calc == 1 mov eax, 78h ; eax = 78h jmp exit ; на выход .ENDIF .IF calc == 2 ; если calc == 2 mov eax, 79h ; eax = 79h jmp exit ; на выход .ENDIF .IF calc == 0 ; если calc == 0 mov eax, 7Ah ; eax = 79h jmp exit ; на выход .ENDIF mov eax, 30h ; в любом другом случае eax = 30h exit: Ret SerialCreate EndP SerialLengh proc len:DWORD mov eax, len ; записываем в eax длинну имени sub eax, 2h ; отнимаем 2 спец символа shl eax, 1 ; умножаем на 2 получившееся add eax, 0Ah ; прибавляем 0Ah sub eax, 7h ; отнимаем 7h add eax, 2h ; прибавляем 2h Ret SerialLengh EndP end startВот такая в принципе несложная программка, в работе она выглядит следующим образом:
Ну вот и все, в общей сложности у меня весь этот процесс занял приблизительно 2 часа с перекурами, написание сего опуса заняло гораздо больше.
Комментариев нет:
Отправить комментарий