23 мая 2012 г.

Разбираем crackme - XYZ_KeygenMe20110405

От скуки зашел на сайт http://crackmes.de, тыкнул на угад в поисках чтоб поломать и выпал мне crackme от китайского товарища по имени Xia Yuanzhong. Ну чтож, написанно что очень легкое, вот и потренируемся в ломании и написании генератора ключей.

 


Запускаем 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 в и посмотрим какие дальше проверки идут. В определенный момент я вижу, что мы вернулись опять в начало, видно начался цикл проверки следующего символа, посмотрим, где мы можем из него выскочить, нам нада найти сравнение длинны пароля и цифрой следующего символа. Я вот вижу похожее, сравнивается 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 часа с перекурами, написание сего опуса заняло гораздо больше.

Комментариев нет:

Отправить комментарий