Подробный разбор решения crackme01_x64 +45

- такой же как Forbes, только лучше.

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

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (18):

  1. BOOTor
    /#10527588

    Спасибо! Просто и доступно!

  2. skymal4ik
    /#10527792

    Спасибо, довольно интересно и познавательно!
    А какой дизассемблер и дебаггер использовались при написании статьи?

    • echo44
      /#10528002

      Благодарю за отзыв. Я специально не указал дизассемблер и отладчик.
      Выбор инструментов не принципиален, пользуйтесь теми, которые вам знакомы. Из отладчиков могу посоветовать: ollydbg, windbg, x64dbg. Из дизассемблеров можно упомянуть IDA Pro, но он не бесплатный.

  3. XSanchezX
    /#10528012

    Отладчик-то какой использовал?

  4. Hardwar
    /#10528014

    Спасибо, не хватало такой статьи!

  5. krrota
    /#10528016

    Спасибо за статью, полезная! Вспомнил как сам когда-то начинал изучать дизассемблирование BIOS'а с этого самого CrackMe, прослезился. Вообще ощущается катастрофическая нехватка русскоязычных материалов по обратной разработке и низкоуровневому программированию. С нетерпением жду дополнительных материалов.

    • echo44
      /#10528066

      Благодарю за отзыв. По мере возможности буду продолжать. В ближайшее время планируется ещё одна статья.

  6. VincentoLaw
    /#10528240

    Спасибо большое за статью, хорошо изучил многие области программирования, а эта оставалась неизведанной. Благодаря вашей статье всё становится просто и понятно. С радостью почитал бы ваши статьи на эту тематику!

  7. debounce
    /#10528364

    Отличная бесплатная книга на русском «Reverse Engineering для начинающих» beginners.re Научитесь реверсить сразу под все платформы: x86/x64, ARM, MIPS, + Java

    • Maccimo
      /#10531174

      Пролистал главу по Java.
      Использовать для этого IDA и вручную править байтики в class-файлах — неоптимально, мягко говоря.

  8. lierchan
    /#10528506

    Хорошо все изложено, спасибо.

  9. echo44
    /#10531202

    Сам crackme можно скачать по ссылке.

  10. koderr
    /#10531242

    Строчка «add eax, 0FFFFFFD0h» не так проста как кажется. На самом деле здесь происходит не сложение, а вычитание. Да-да, не верь глазам своим, 0FFFFFFD0h для компьютера это число -30h, видимо дело в том, что команда «add eax, 0FFFFFFD0h» оптимальнее с точки зрения компилятора чем “sub eax, 30h”.

    В этом месте пост внезапно скатился до уровня информатики 9-го класса, тема «Представление целых чисел в памяти ЭВМ».

    • echo44
      /#10531264 / +1

      Пост рассчитан на новичков, поэтому нахожу уместным обращать внимание даже на простые моменты. Если для читателя подобные вещи очевидны, то он просто продолжит читать дальше, а если нет, то сможет вынести для себя что то новое, и при встрече с подобной конструкцией в дальнейшем у него не возникнет лишних вопросов.

  11. kosyag
    /#10531446

    Я как очень ленивый человек просто перенес бы метку Loc_140001143 пониже )