Итак, перед нами простой 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)
skymal4ik
19.11.2017 01:45Спасибо, довольно интересно и познавательно!
А какой дизассемблер и дебаггер использовались при написании статьи?echo44 Автор
19.11.2017 11:52Благодарю за отзыв. Я специально не указал дизассемблер и отладчик.
Выбор инструментов не принципиален, пользуйтесь теми, которые вам знакомы. Из отладчиков могу посоветовать: ollydbg, windbg, x64dbg. Из дизассемблеров можно упомянуть IDA Pro, но он не бесплатный.
krrota
19.11.2017 11:57Спасибо за статью, полезная! Вспомнил как сам когда-то начинал изучать дизассемблирование BIOS'а с этого самого CrackMe, прослезился. Вообще ощущается катастрофическая нехватка русскоязычных материалов по обратной разработке и низкоуровневому программированию. С нетерпением жду дополнительных материалов.
echo44 Автор
19.11.2017 12:39Благодарю за отзыв. По мере возможности буду продолжать. В ближайшее время планируется ещё одна статья.
lexasss
20.11.2017 23:51Крис Касперски оставил большое наследие на русском языке. Можно почитать здесь, но нужно будет самому находить нужное, каталог не систематизирован. Ну и классическое Искусство дизассемблирования.
VincentoLaw
19.11.2017 15:54Спасибо большое за статью, хорошо изучил многие области программирования, а эта оставалась неизведанной. Благодаря вашей статье всё становится просто и понятно. С радостью почитал бы ваши статьи на эту тематику!
debounce
19.11.2017 17:13Отличная бесплатная книга на русском «Reverse Engineering для начинающих» beginners.re Научитесь реверсить сразу под все платформы: x86/x64, ARM, MIPS, + Java
Maccimo
21.11.2017 13:54Пролистал главу по Java.
Использовать для этого IDA и вручную править байтики в class-файлах — неоптимально, мягко говоря.
koderr
21.11.2017 14:52Строчка «add eax, 0FFFFFFD0h» не так проста как кажется. На самом деле здесь происходит не сложение, а вычитание. Да-да, не верь глазам своим, 0FFFFFFD0h для компьютера это число -30h, видимо дело в том, что команда «add eax, 0FFFFFFD0h» оптимальнее с точки зрения компилятора чем “sub eax, 30h”.
В этом месте пост внезапно скатился до уровня информатики 9-го класса, тема «Представление целых чисел в памяти ЭВМ».echo44 Автор
21.11.2017 15:02+1Пост рассчитан на новичков, поэтому нахожу уместным обращать внимание даже на простые моменты. Если для читателя подобные вещи очевидны, то он просто продолжит читать дальше, а если нет, то сможет вынести для себя что то новое, и при встрече с подобной конструкцией в дальнейшем у него не возникнет лишних вопросов.
BOOTor
Спасибо! Просто и доступно!