[Из песочницы] Подробный разбор решения crackme01_x64
Данная статья рассчитана на начинающих, интересующихся обратной разработкой, и имеющих базовые представления о работе ЦП, языке ассемблера. Этот crackme относительно старый и простой, но при его решении применяются в основном те же приемы, что и при решении более сложных. На просторах Сети можно найти несколько статей с его разбором такие как эта, а еще он здесь упоминается (crackme то с историей), однако те решения не такие подробные как это. В свое время мне сильно не хватало такого построчного разбора, куда можно было бы заглянуть, когда запутался и не понимаешь что делает тот или иной участок кода. Если этот пост окажется полезным хотя бы для одного человека, значит я не зря старался. Все скрины (кроме первого) кликабельны. Приятного прочтения.
Итак, перед нами простой crackme, запустим его и посмотрим как он работает.
Ага, все довольно просто, мы должны ввести правильный серийник. Теперь откроем программу в дизассемблере. Как правило дизассемблерные листинги, даже относительно простых программ, довольно объемны. Для определения той части кода, которая проверяет ввод серийника, найдем где в памяти программы хранится строка с сообщением об ошибке «Fail, Serial is invalid!!!» и какой код к этой строке обращается.
В секции данных видим искомую строку, а под ней дизассемблер сформировал ссылку на участок кода, который обращается к строке, перейдем по ссылке.
Loc_140001191 это метка для перехода, ниже видим строку »call cs: MessageBoxA». Предположим, так вызывается функция, которая отрисовывает окошко с текстом «Fail, Serial is invalid!!!». Получается, если мы вводим неправильный серийник, то управление передается именно по метке Loc_140001191. Отлично, продолжим копать и разберемся, где еще есть обращение по этой же метке, для чего перейдем по ссылке справа от нее, которую там заботливо разместил для нас, дизассемблер.
Это очень важный участок кода, приглядимся к нему внимательно. Мы видим вызов функции »GetDlgItemTextA», в названии есть слова get и text, очевидно эта функция читает введенный серийник. После того как функция отработала, команда »lea rcx, [rsp+78h+String]» загружает в регистр rcx, адрес чего то из стека. Затем записывает в edx значение из eax и вызывает функцию sub_140001000. Манипуляции с регистрами перед вызовом не случайны, таким образом осуществляется передача аргументов в функцию, а после того как функция отработала, командой »test eax, eax» выполняется проверка содержимого eax, если в нем не 0, то подгружаются адреса строк, рапортующих, что все ок. Затем запускается отрисовка окошка с соответствующим оповещением. Если же после работы sub_140001000 в еax будет 0, то управление передается на ранее рассмотренную метку, где мы получим сообщение об ошибке. Делаем вывод: sub_140001000 возвращает значение в регистр eax. Теперь мы можем восстановить логику работы этого crackme: вызывается функция GetDlgItemTextA, напомню, она предположительно считывает наш серийник, затем вызывается функция sub_140001000 которая в качестве аргументов получает адрес чего-то из стека и значение регистра eax через регистр edx, если же функция возвращает «не ноль», то был введен правильный серийник, если «ноль», то не правильный. Предположим, что именно sub_140001000 проверяет наш ввод, переименуем метки и функции в нашем листинге для удобства и наглядности. Теперь наш листинг выглядит примерно так.
Итак, пол дела сделано. Теперь давайте разберемся, что же получает функция check_func в качестве аргуметнов, для этого запустим программу в отладчике до вызова функции и посмотрим, что хранится в rcx и edx.
Я ввел 12345. Выполнение остановилось на вызове функции проверки. Смотрим регистры, в rcx лежит число 12F660, как мы знаем это адрес, посмотрим дапм памяти по нему в окне внизу, ага там лежит наш ввод, теперь глянем на edx(он же младшая часть rdx) там лежит число 5, а это длинна нашего серийника. Вот мы и выяснили, какие аргументы и что возвращает функция проверки, теперь мы можем предположить как мог выглядеть ее прототип: int check_func (* char, int). Самое время ее изучить. Откроем функцию проверки в дизассемблере. Она довольно большая, поэтому разберем по частям.
Здесь все относительно просто. Напомню, регистр edx хранит в себе размер введенного серийника, а rcx его адрес в памяти. Видно, что в первую очередь функция check_func проверяет длину введенного номера. Она должна быть равна 19(13 в шестнадцатеричной системе), если это так, то продолжается проверка (переход на метку good_size), если же нет, то управление переходит по метке bad_serial, где командой »xor eax, eax» регистр eax обнуляется и происходит выход из функции. Делаем вывод: в валидном серийнике 19 знаков, а каких именно, разберемся далее. Обращаем внимание, что практически сразу адрес введенного номера помещается в регистр R8.
Продолжим исследовать механизмы проверки.
Мы помним, что R8 указывает на первый элемент серийника, значит по адресу R8+4 будет храниться адрес 5-го элемента, он записывается в RAX. Дальше видим очень странный набор команд, который встретится нам еще не раз:
xchg ax, ax
db 66h, 66h
xchg ax, ax
Теперь подробнее:
»xchg ax, ax» — это эквивалент команды »nop» в платформе х86.
Следующая строка — это так называемый префикс, он используется для указания разрядности следующей инструкции. Этот код мы можем его игнорировать. После этих инструкций идет проверка следующего рода: то что находится по адресу RAX(а там адрес 5-го элемента серийника) должно быть равно числу 2D (в шестнадцатиричной системе). 2D это ASCII код знака »-», делаем выводы:
1. Функция, которая считывает наш ввод считывает строку, а не число,
2. В нашем серийнике будет несколько групп по 4 числа разделенных дефисами.
Если 5-й символ не »-», то серийный номер не правильный, происходит переход на метку bad_serial и выход из функции со значением 0, но если на 5-й позиции дефис, тогда RAX увеличивается на 5, теперь когда он указывает на 10 элемент проверяется какой символ на этой позиции. Всего такая проверка проводится 3 раза, каждый 5-й символ серийника это »-», и в нем всего 19 знаков, значит он имеет форму: XXXX-XXXX-XXXX-XXXX. Если формат ввода соответствующий, то проверка продолжается.
Следующий участок функции не представляет для нас особого интереса.
Идем дальше.
Обращаем внимание на следующее: R9 всегда указывает на первый элемент блока, а RCX хранит смещение внутри блока, поэтому после проверки, R9 увеличивается на 5, чтобы попасть в следующий блок, а RCX обнуляется после проверки блока, помним, что в RDX лежит ноль, поэтому по метке zero_to_rcx происходит обнуление. Строчка »add eax, 0FFFFFFD0h» не так проста как кажется. На самом деле здесь происходит не сложение, а вычитание. Да-да, не верь глазам своим, 0FFFFFFD0h для компьютера это число -30h, видимо дело в том, что команда »add eax, 0FFFFFFD0h» оптимальнее с точки зрения компилятора чем »sub eax, 30h». В eax хранится ASCII код элемента, из него вычитают 30h и сравнивают с 9. Смысл тут такой: ASCII коды цифр от 0 до 9 это 30h — 39h соответственно, поэтому если из кода цифры вычесть 30h, то результат не превысит 9, если же превысит, значит в eax лежал код не цифры, а какого то другого знака и тогда мы переходим по метке wrong_serial. Наш серийник должен состоять из цифр и дефисов. Далее суммируется коды всех 4-х элементов блока, 3 раза плюсуется код 4-го элемента и вычитается 150h. Результат пушится в стек, суммы всех блоков мы складываются в R10 и результат делится на 4.
Теперь проверяются суммы для всех 4-блоков. Значение для каждого блока должно быть равно из сумме деленной на 4, иными словами сумма ASCII кода первого знака плюс код второго, третьего плюс 3 раза код четвертого и минус 150h должна быть одинаковой для всех блоков.
Остался последний этап проверки.
Тут все понятно, в серийнике не должно быть одинаковых блоков. Не забываем, что R8 хранит адрес нашего ввода, RAX здесь используется для выбора знака внутри блока, а R8 для выбора блока внутри серийного номера.
Теперь мы знаем каким образом осуществляется проверка и можем написать keygen. Я постарался максимально подробно описать решение, вопросы можно задать в комментариях, благодарю за прочтение.