- Главная
- Блог
- Информационная безопасность
- Основы реверсинга: пишем кейген и боремся с антиотладкой
Основы реверсинга: пишем кейген и боремся с антиотладкой
Автор: Андрей Бирюков.
Специально для Академии Кодебай
Основы реверсинга: пишем кейген и боремся с антиотладкой
В предыдущей статье мы рассмотрели основы ассемблера, поговорили об основных инструкциях, и начали анализировать крякмиксы. В этой статье мы продолжим заниматься практическим реверсингом, рассмотрим еще один crackme, напишем кейген к нему, а также рассмотрим некоторые приемы антиотладки.
Если в предыдущей статье мы осуществили патчинг, поменяли несколько байт в выполнимом файле для того, чтобы изменить логику работы программы, то теперь мы ни одного бита в выполнимых файлах менять не будем. Вместо этого мы попытаемся понять, по какому алгоритму осуществляется проверка и попробуем передать программе нужный ключ.
Алгоритм проверки ключа может работать по-разному. В простейшем случае можно просто осуществлять проверку переданного ключа на соответствие какому-то заранее зашитому в программе набору байт. В случае полного совпадения получаем Success, в противном случае - Fail.
Однако, наиболее распространены алгоритмы проверки ключа, генерирующие правильный ключ в процессе работы программы. Так, часто ключ генерируется на основе имени узла, на котором запускается программа, времени запуска, IP адреса и других параметров.
Есть и более творческие варианты проверок. Так, можно обращаться по сети к какому-либо узлу, например, по протоколу HTTP, и загружать с него нужный контент для проверки ключа. Также можно проверять наличие файла с ключом или процесса в памяти.
Кстати, все представленные методы могут использоваться для защиты реальных программ. От простейших проверок зашитого кода и до генерации сложных ключей, активации по сети или через файлы.
В этой статье мы разберем крякми с генерацией ключа, а также посмотрим простейший механизм защиты от отладки.
Виды механизмов защиты
Для варианта с жестко захардкоженным ключом, многое зависит от того, смогли ли мы правильно определить место в коде, где осуществляется проверка ключа и далее необходимо найти, где этот ключ, собственно, хранится.
Для борьбы с подобными механизмами защиты никаких дополнительных приложений, как правило, писать не нужно. Ключ один для всех инсталляций и нам необходимо лишь его узнать. Но на практике такую защиту используют лишь на очень старых программах, гораздо интереснее формировать нужный ключ динамически, в зависимости от определенных условий.
Далее посмотрим вариант разбора крякми с генерацией пароля. Здесь в качестве рабочего инструмента мы будем использовать дизассемблер IDA. С его помощью мы посмотрим как выглядит код нашего крякмикса и попробуем понять принцип генерации пароля.
Разбор крякми
Для начала просто запустим наш крякмикс keygenme и посмотрим, что происходит. У нас запрашивают логин и пароль. После ввода случайных значений предсказуемо получаем сообщение о некорректном серийнике.
Для начала откроем этот файл в отладчике и поищем вхождения всех текстовых строк. Этот прием далеко не всегда является полезным, иногда текстовые вхождения могут только еще больше запутать исследователя кода, но в простых случаях с незащищенными приложения он тоже может помочь.
Нажимаем правую кнопку мыши, выбираем Поиск в -> Текущая область -> Ссылки на строки. В принципе для полного поиска по всему коду можно выбрать Все модули -> Ссылки на строки, но такой поиск займет больше времени.
Получим информацию о найденных текстовых строках.
В случае, если при таком поиске ничего найти не удалось, можно, во-первых, поискать по всем модулям, а во-вторых, в процессе выполнения программы, остановившись на каком-либо брейкпоинте, еще раз попробовать поискать. Иногда это помогает найти новые строковые вхождения.
Теперь нам достаточно будет перейти на нужный адрес по строке Serial is correct… Далее мы видим целый ряд проверок, по результатам каждой мы можем оказаться в блоке с Incorrect serial.
Если бы мы говорили о патчинге, как в прошлой статье, то нам было бы достаточно просто забить NOPами соответствующие команды. Но сейчас мы хотим разобраться в том, как проверяется серийный номер.
Для этого можно, конечно, поставить брейкпоинт сразу на адрес 0х401001, где начинается блок проверки, но есть некоторая вероятность, что мы до него не дойдем, так как ранее есть другие проверки. Поэтому лучше переместиться немного подальше, найти блок с вводом Username и Serial и поставить брейкпоинт там, с помощью клавиши F2. Далее запустим программу и введем осмысленные значения, например username и password. Я поставил брейк сразу после вызова функции, считывающей серийник.
Теперь мы знаем, по каким адресам в памяти хранятся Username и Serial. Далее можно пошагово перемещаться по программе, выполняя команды последовательно. Переходим на адрес 0х4010с0. Здесь довольно интересный блок кода. В нем программа берет поочередно блоки по четыре байта из серийного номера и на выходе возвращает в ECX количество этих блоков. Для значения password это будет 2, а после возращения из этой подпрограммы выполняется sub ECX,3 (вычитание из ECX). В нашем случае значение будет отрицательным, и следующая команда jne отправит нас на Incorrect Serial. Таким образом мы узнали, что длина серийника должна быть не меньше 12 символов. Проверим это. Введем password1234. Теперь ECX равно 3 и переход по jne не произойдет.
Следующий блок кода 0х401049 обрабатывает значение username. Обратим внимание на инструкции cld и repne scasb. Команды CLD и STD позволяют сбросить или установить флаг направления DF (Direction Flag). Команда CLD (Clear DF) сбрасывает флаг в значение 0, а команда STD (Set DF) устанавливает его в значение 1. Проще говоря, CLD говорит, что читать строку мы будем слева направо. Scasb это поиск в строке байтов, а при использовании префикса repne scas сканирует строку в поисках первого элемента, равного значению в регистре al (мы используем только младшую часть EAX).
В результате преобразований мы получим набор байт, находящихся по адресу 0х4020a0.
Следующим блоком кода, на который будет выполнен переход является переход на адрес 0х401000. Здесь мы видим наш сгенерированный набор байтов по адресу 0х4020a0 и фрагмент исходного серийника, начинающийся с адреса 0х402053, то есть с четвертого байта.
Далее команда CMPSD сравнивает двойное слово из памяти по адресу DS:SI с двойным словом по адресу ES:DI. Аналогична по действию команде CMP. Очевидно, что в нашем случае значения не совпадут.
Давайте сразу посмотрим на код дальше. На третьей и восьмой позициях в серийнике должны быть знаки разделителя “-”.
То есть формат серийника ХХ-ХХХХ-ХХХХ. По сути, мы уже можем на основе этих данных подготовить пару Username/Serial. В качестве имени пользователя оставляем слово username, а в качестве серийника указываем SN-E88E-54DF, где SN это любые два символа, их все равно не проверяют. Давайте проверим.
Как видно, мы правильно нашли области кода, в которых осуществляется генерация и проверка ключа, теперь давайте рассмотрим основные принципы написания генератора ключей и заодно поработаем в дизассемблере IDA.
Пишем кейген
Откроем файл Crackme в IDA. Здесь мы видим несколько блоков кода, представленных в виде блоков со связями. Визуально такое отображение упрощает понимание принципов работы алгоритма.
Как можно увидеть в левой части экрана указаны вызовы подпрограмм и, в частности, уже знакомые нам подпрограммы по адресам 0х4010F9 и 0х401049.
С точки зрения написания кейгена нас больше всего будет интересовать процедура, находящаяся по адресу 0х401049.
Если вы собираетесь писать свой кейген на языке высокого уровня, то вам необходимо будет реализовать все эти преобразования на соответствующем языке, но если вы будете использовать ассемблер, то можно просто скопировать фрагмент кода процедуры и поместить его в свой кейген. Для этого в IDA можно на нужной процедуре нажать правую кнопку мыши и выбрать Text view, а затем просто скопировать нужный фрагмент кода.
Ниже представлен фрагмент кода кейгена, который получает на вход username и serial как параметры командной строки и затем генерирует по ним нужный серийный номер.
START:
PUSH STD_OUTPUT_HANDLE
CALL GetStdHandle@4
MOV HANDL,EAX
CALL NUMPAR
CMP EAX,1
JE NO_PAR
MOV EDI,2
LEA EBX,USERNAME
CALL GETPAR
push ebx
push esi
push edi
xor eax, eax
lea edi, USERNAME
mov ecx, 0FFh
cld
repne scasb
not cl
dec ecx
xor ebx, ebx
lea esi, USERNAME
label4:
xor eax, eax
lodsb
mul cl
add ebx, eax
inc edi
dec cl
jnz short label4
xor ebx, 13131313h
not ebx
xor ebx, 1234ABCDh
mov eax, ebx
and eax, 0F0F0F0Fh
and ebx, 0F0F0F0F0h
shr ebx, 4
LEA esi,PASSWORD
mov [ESI], eax
mov [ESI+4], ebx
mov ecx, 8
label7:
cmp byte ptr [esi], 9
ja short label5
or byte ptr [esi], 30h
jmp short label6
label5:
add byte ptr [esi], 37h ; '7'
label6:
inc esi
dec ecx
jnz label7
mov ESI,offset PASSWORD
mov EDI,offset PASSWORD1+3
MOV EAX,[ESI]
MOV [EDI],EAX
mov ESI,offset PASSWORD+4
mov EDI,offset PASSWORD1+8
MOV EAX,[ESI]
MOV [EDI],EAX
PUSH 0
PUSH OFFSET NUMW
PUSH 12
PUSH OFFSET PASSWORD1
PUSH HANDL
CALL WriteConsoleA@20
pop edi
pop esi
pop ebx
NO_PAR:
PUSH 0
CALL ExitProcess@4
;retn
NUMPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ;указатель на строку
XOR ECX,ECX ;счетчик
MOV EDX,1 ;признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ;номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
INC ESI
JMP L1
L4:
MOV EAX,ECX
RET
NUMPAR ENDP
;получить параметр из командной строки
;EBX - указывает на буфер, куда будет помещен параметр
;в буфер помещается строка с нулем на конце
;EDI - номер параметра
GETPAR PROC
CALL GetCommandLineA@0
MOV ESI,EAX ;указатель на строку
XOR ECX,ECX ;счетчик
MOV EDX,1 ;признак
L1:
CMP BYTE PTR [ESI],0
JE L4
CMP BYTE PTR [ESI],32
JE L3
ADD ECX,EDX ;номер параметра
MOV EDX,0
JMP L2
L3:
OR EDX,1
L2:
CMP ECX,EDI
JNE L5
MOV AL,BYTE PTR [ESI]
MOV BYTE PTR [EBX],AL
INC EBX
L5:
INC ESI
JMP L1
L4:
MOV BYTE PTR [EBX],0
RET
GETPAR ENDP
_TEXT ENDS
END START
Здесь мы разобрали вариант защиты с использованием генерации серийного ключа в зависимости от имени пользователя, либо других значений. Но, в реальности можно встретить более экзотические механизмы защиты. Например, можно встретить проверку, основанную на взаимодействии с узлом в сети и выдающего соответствующее сообщение в зависимости от результата. Здесь мы можем ограничиться отладчиком, хотя при работе с сетью может также потребоваться сниффер, например Wireshark. Также, возможны варианты, когда проверяющий код ищет какой-либо файл на диске или процесс в сети.
Про антиотладку
Приложения могут содержать механизмы защиты от работы под отладчиком. Самый простой вариант проверки наличия отладчика в системе - это вызов функции IsDebuggerPresent.
Так, если следующей фрагмент кода будет выполняться без отладчика, то будет выполнен переход на метку No_debug, а если под отладчиком (значение EAX не равно нулю), то выполнится ExitProcess.
start:
call [IsDebuggerPresent]
add esp, 4
cmp eax, 0
je No_debug
push 0
call [ExitProcess]
No_debug:
Вариант немного посложнее, который часто можно встретить в различных crackme. Мы можем обратиться к блоку окружения процесса (PEB), который заполняется загрузчиком операционной системы. Он содержит много полей: например, отсюда можно узнать информацию о текущем модуле, окружении и загруженных модулях. Получить структуру PEB можно, обратившись к ней напрямую по адресу fs:[30h]. Так в примере ниже мы в первых двух строках обращаемся к PEB, а в третьей извлекаем значение BegingDebugged.
start:
mov eax, [fs:18h]
mov eax, [eax+30h]
movzx eax, byte [eax+2]
or eax, eax
je No_debug
push 0
call [ExitProcess]
No_debug:
Альтернативный вариант написания:
mov eax, [fs:18h]; ????????? ?? TEB
mov eax, [eax+30h]; ????????? ?? PEB
ADD eax,68h
movzx eax, byte [eax]; ???? BegingDebugged ? PEB
cmp eax, 0
je No_debug
push 0
call [ExitProcess]
No_debug:
Это самые простые примеры борьбы с отладчиком. В более сложных случаях применяются вызовы различных функций операционной системы и работа с прерываниями.
Заключение
В этой статье на примере крякмикса мы рассмотрели генерацию серийного ключа и написание кейгена. Также мы посмотрели, как строится работа с дизассемблером и другими инструментами для реверсинга.
Конечно, в этой и предыдущей статье мы разобрали лишь малую часть реверсинга, показав лишь основные методы решения crackme. В следующей статье мы будем говорить о реверсивном инжиниринге приложений, написанных на .NET.
Ну, а на нашем курсе по реверсивному инжинирингу ПО под Windows можно изучить практические аспекты реверсинга различных видов приложений, в том числе и вредоносного кода.