Решение bearchik - Errors Keygenme

Сложность: 3 - Getting harder
Платформа: Windows
Язык: Assembler
Дано: Errors Keygenme

Привет. Это очень маленькая и несложная программа. Для создания ключа здесь используется широко известный алгоритм.
Но я добавил немного хитростей, чтобы интересней было решать keygenme.
Удачи, Bearchik.

Решение (автор rmolina, demoth)
опубликовано 08.01.2013

Инструмент: IDA

  • Введение


Обозначим цели. Keygenme, прежде всего, интересен не столько задаваемой проблемой, а скорее реализацией. Что в наличии – чистый ассемблер: только код (секция .text) с данными (.data) и секция ресурсов (.rsrc). По сути, crackme ведет себя как настоящий шелл-код (shellcode): динамическая загрузка импортируемых функций, скрытие строк, управляемые исключения. Но все по порядку.

  • Структурная обработка исключений (Structured Exception Handling, SEH)


Из имени crackme можно догадаться, что «ошибки» (errors) являются важной составляющей алгоритма работы исследуемой задачи. Статей по обработке исключений в сети предостаточно. Например: http://www.securitylab.ru/contest/212085.php, http://www.insidepro.com/kk/014/014r.shtml.

Немного теории.
Ошибки вызывают исключения. Исключение - это событие к которому приводит ненормальное (неправильное) поведение кода, например, исключение, возникает при обращение к невыделенной области памяти (и как следствие возникает нарушение доступа) или при делении на нуль. Исключения бывают как аппаратные (источник - процессор), так и программные (источник – операционная система или приложение). Любое исключение, как аппаратное, так и программное, можно обработать.
Обработчик исключения программа задает следующим кодом:



Процедура обработки структурных исключений имеет следующий прототип:



Keygenme обрабатывает четыре различных исключения (эти «ошибки» щедро раскиданы по коду программы). Фишка в том, что каждый обработчик исключения выполняет некую «скрытую» инструкцию. Работает это так. Коду обработчика доступны состояния регистров (см. прототип handler, нас интересует параметр pContext) на момент вызова исключения. Значит, если регистрам задать некое значение, затем вызвать исключение, то обработчик сможет выполнить над этими значениями простейшую операцию (add, xor и т.п.) и вернуть результат в другом регистре. Например, код обработчика исключения «деление на нуль»:

# получить pContext (регистровый контекст)
.text:00402065          mov eax, [esp+0Ch]
.text:00402069          mov edx, [eax+CONTEXT._Edx]
# «скрытая» инструкция: _EDX = _EDX операция _ESI
.text:0040206F          xor edx, [eax+CONTEXT._Esi]
# результат операции сохраняем в _Eax
.text:00402075          mov [eax+CONTEXT._Eax], edx
# пропускаем команду, вызвавшую исключение
.text:0040207B          mov ebx, [eax+CONTEXT._Eip]
.text:00402081          add ebx, 2
.text:00402084          mov [eax+CONTEXT._Eip], ebx
# Завершаем обработку
.text:0040208A          mov eax, 0
.text:0040208F          retn

Вот список исключений и их операции:
«деление на ноль» INTEGER_DIVIDE_BY_ZERO = операция XOR
«привилегированная инструкция» PRIVILEGED_INSTRUCTION = операция ADD
«недопустимая инструкция» ILLEGAL_INSTRUCTION = операция AND
«нарушение прав доступа» ACCESS_VIOLATION = операция OR

  • Динамическая загрузка импортируемых функций


Надеюсь, вы обратили внимание, что таблица импорта отсутствует в exe-файле. Ее просто нет. Хотя общеизвестно, что без импортируемых функций и шага ступить нельзя. Весь импорт можно организовать, зная всего лишь пару функций LoadLibraryA и GetProcAddress (в crackme поиском их адресов занимается функция 004020D4). Рассмотрим работу функции 004020D4 более подробно (большинство структур недокументированно, но, используя отладчик WinDbg с отладочными символами, можно подсмотреть состав полей структур):

TIB http://en.wikipedia.org/wiki/Win32_Thread_Information_Block
PEB http://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx
PEB_LDR_DATA http://msdn.microsoft.com/en-us/library/windows/desktop/aa813708(v=vs.85).aspx

image00.jpg

# Важная инструкция. Между прочим, всегда обращайте внимание на то, используется ли в коде регистр FS. FS указывает на самую важную структуру ядра, связанную с текущим процессом/потоком – TIB (Thread Information Block). В нашем случае по смещению fs:[0x30] находится адрес структуры TIB ->PEB (Process Information Block). Помещаем его в EAX.
.text:004020D9          mov eax, large fs:[0x30]
# Получаем адрес LDR (структура PEB->PEB_LDR_DATA). LDR – это список загруженных модулей. Помещаем его в EAX.
.text:004020DF          mov eax, [eax+0Ch]
# Получаем указатель на 0-й элемент списка LDR->InLoadOrderModuleList. Почему мы используем InLoadOrderModuleList? Потому что InLoadOrderModuleList - позиционный список загруженных процессом модулей, в котором 0-й элемент описывает текущий модуль, 1-й элемент - NTDLL модуль, 2-й – KERNEL32 модуль, который нам и нужен. Сам по себе InLoadOrderModuleList - это двухсвязный список (_LIST_ENTRY) указателей на структуры описания загруженных модулей _LDR_DATA_TABLE_ENTRY, где первые четыре байта Flink указывают на следующий _LIST_ENTRY, а следующие четыре байта Blink указывают на предыдущий _LIST_ENTRY.
.text:004020E2          mov eax, [eax+0Ch]
# Получаем указатель на 1-й элемент (NTDLL image)
.text:004020E5          mov eax, [eax]
# Получаем указатель на 2-й элемент (KERNEL32 image), то, что нам надо
.text:004020E7          mov eax, [eax]
# получить базовый адрес загрузки модуля KERNEL32 (в eax поместить hKernel32). Такой метод поиска модуля KERNEL32 работает в Windows 2000, XP, VISTA, SEVEN. Чем удобен этот метод? Тем, что не надо искать модуль по имени, а достаточно получить указатель на 2-й элемент в списке InLoadOrderModuleList.
.text:004020E9          mov eax, [eax+18h]

# Теперь, зная базовый адрес загрузки модуля KERNEL32 (обозначим его hKernel32), нам надо найти его таблицу экспорта, и получить адреса функций LoadLibraryA и GetProcAddress. Но сначала приведем описание структуры IMAGE_EXPORT_DIRECTORY - таблица экспорта.



.text:004020F1          add eax, 3Ch
.text:00402121          mov bl, [eax]
# ebx -> (указывает) на PE-заголовок
.text:00402123          add ebx, hKernel32
# ebx -> IMAGE_DATA_DIRECTORY экспорта
.text:00402129          add ebx, 0x78
# смещение 0x00
.text:0040212C          mov eax, [ebx+IMAGE_DATA_DIRECTORY.VirtualAddress]
# eax -> на таблицу экспорта (IMAGE_EXPORT_DIRECTORY)
.text:0040212E          add eax, hKernel32
.text:00402134          mov k32_export, eax
# смещение 0x04
.text:00402139          mov eax, [ebx+IMAGE_DATA_DIRECTORY.Size]
# eax = размер таблицы экспорта
.text:0040213C          mov k32_size_ex, eax
.text:00402141          mov eax, k32_export
.text:00402146          mov ebx, [eax+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
# ebx = количество экспортируемых KERNEL32 функций
.text:00402149          mov numNames, ebx
.text:0040214F          mov ecx, numNames
.text:00402155          xor edx, edx
.text:00402157          mov eax, k32_export
.text:0040215C          mov edi, [eax+IMAGE_EXPORT_DIRECTORY.AddressOfNames]
# edi -> на массив адресов Names[], каждый элемент которого указывает на имя функции
.text:0040215F          add edi, hKernel32

# Далее, идет цикл обхода Names[] в поисках имени функции. Еще один момент. Чтобы не сравнивать строки, в коде к каждому имени применим кодирующую процедуру и будем сравнивать хэш-значения:
.text:00402165 next_name:
.text:00402165          mov eax, [edi]
.text:00402167          mov ebx, eax
# ebx -> имя экспортируемой функции Names[i]
.text:00402169          add ebx, hKernel32
.text:0040216F          push ebx
# получить хэш-значение имени функции
.text:00402170          call encode
# хэш-значение для LoadLibrary
.text:00402175          cmp eax, 0x577A7461
.text:0040217A          jnz short @1
# зная имя, находим адрес функции
.text:0040217C          call getaddrfunction
.text:00402181          mov LoadLibraryA, eax
.text:00402186 @1:
# хэш-значение для GetProcAddress
.text:00402186          cmp eax, 0x74651D22
.text:0040218B          jnz short @2
.text:0040218D          call getaddrfunction
.text:00402192           mov GetProcAddress, eax
.text:00402197 @2:
# edx = i
.text:00402197          inc edx
# цикл while (i < numNames)
.text:00402198          cmp edx, numNames
.text:0040219E          jb short next
.text:004021A0          jmp short end_loop
.text:004021A2 next:
.text:004021A2          add edi, 4
.text:004021A5          jmp short next_name

Код функции encode (004021D8) на Python’е:

import struct
 
def encode(name):
    name_func = name
    name_func += "\x00" * (len(name_func) % 4)
    h = 0
    for i in range(len(name_func) / 4):
        h ^= struct.unpack("<L",name_func[4*i:4*i+4])[0]
    return h
 
name_func = "LoadLibraryA"
myhash = encode(name_func)
print ("[*]Name = "+name_func)
print ("[+]myhash = "+hex(myhash))
 
name_func = "GetProcAddress"
myhash = encode(name_func)
print ("[*]Name = "+name_func)
print ("[+]myhash = "+hex(myhash))
 
Результат:
[*]Name = LoadLibraryA
[+]myhash = 0x577a7461
[*]Name = GetProcAddress
[+]myhash = 0x74651d22


Псевдокод getaddrfunction по адресу 00402298:
(смотрите IMAGE_EXPORT_DIRECTORY)
Мы нашли имя функции, т.е. Names[i] == "имя функции"
Тогда адрес функции = Functions[Ordinal[i]]
  • Скрытие строк


Теперь адреса функций LoadLibraryA и GetProcAddress у нас на руках. Простая комбинация:

hModule = LoadLibraryA(«имя модуля»)
addr_func = GetProcAddress(hModule, «имя функции»)

позволит получить адреса всех необходимых - для работы программы - функций. Но есть проблема. Ни одной читаемой строки мы в exe-файле не найдем. И сделано это в целях маскировки, тем самым, затрудняя анализ crackme.
Здесь используется обычный xor-конверт, скрывающий строки. Функция трансформации находится по адресу 00401FBF (дадим ей имя transform). В функцию передаем последовательность байт, длину и xor-байт, а на выходе получаем читаемую ASCII-строку. Как только строка больше не нужна, вызываем ту же функцию для обратного преобразования строки в последовательность байт. Еще один нюанс. Если посмотреть на код, то transform вызывается для тех строк, надобность которых нужна в текущий момент. Например:

«имя модуля» = transform(xor_byte_x, arr_x)
hModule = LoadLibraryA(«имя модуля»)
arr_x = transform(xor_byte_x, «имя модуля»)
«имя функции» = transform(xor_byte_y, arr_y)
addr_func = GetProcAddress(hModule, «имя функции»)
arr_y = transform(xor_byte_y, «имя функции»)

Вот прототип transform:

def transform(xor_byte, arr):
    return "".join([chr(ord(i) ^ xor_byte) for i in arr])
 
#Пример для строки «EndDialog»:
arr = "\x31\x1a\x10\x30\x1d\x15\x18\x1b\x13\x74"
print (transform(0x74, arr))
  • Алгоритм


Так, а теперь нам нужно отловить тот момент, когда вызывается функция GetDlgItemTextA, читающая имя и ключ из полей диалога. Для этого достаточно поставить точку останова на transform (нам нужна строка «GetDlgItemTextA») и запустить программу на выполнение…
В итоге: имя мы читаем здесь 0040128C (адрес буфера - 00403C48), а ключ здесь 00401387 (адрес буфера - 00403D47). Сам алгоритм проверки стартует тут 004015D7. Псевдокод алгоритма:

# Алгоритм обработки имени
…
.text:00401AC4 lea     edi, dword_403A19, 
где dword_403A19 – здесь хранится результат работы алгоритма.
 
# Проверка ключа идет побайтно:
# Цикл - 20 (0х14) итераций
.text:00401ACA mov     ecx, 14h 
next_byte:
       .text:00401AFE mov     bl, [edi]
       # получить младшую часть байта
       bl = (bl OR 0xF0) XOR 0xF0
              if bl <= 9:
       bl  = 0x30+bl # возможные значения bl: от «0» до «9»
              else:
       bl = 0x40+bl # возможные значения bl: от «J» до «O»# eax указывает на наш ключ, esi – индекс 
       .text:00401BE2 cmp     [eax+esi], bl
       .text:00401BE5 jz      short loc_401BEC
       .text:00401BE7 jmp     _badboy_message
       .text:00401BEC inc     esi
       .text:00401BED inc     edi
       loop next_byte


Автор crackme сообщал, что используемый алгоритм – общеизвестный. Самый простой способ – поиск используемых констант. И действительно, поиск дает основание предполагать, что здесь применен алгоритм хэш-функции SHA-1 (константы 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0). Итак, алгоритм - знаем, как проверяется ключ - знаем. Остался последний штрих.
  • Кейген


Важный момент: порядок байт в спецификации к SHA-1 – «big-endian». А здесь проверка байт ключа идет в порядке «little-endian». Здесь я использую скрипт slowsha.py (или прямая ссылка) для вычисления SHA-1, поэтому я в него добавил функцию digest_le(), которая возвращает хэш SHA-1 в формате «little-endian». Если вы будете для вычисления хэша использовать стандартный модуль hashlib, то о порядке байт стоит позаботиться самим.

import slowsha
 
# Example:
# name = "crackmes.de"
name = raw_input()
if len(name) > 55:
    print ("[-]Error: invalid length of name.")
    exit()
 
name_hash = slowsha.sha1(name).digest_le()
res = ""
for i in range(len(name_hash)):
    z = (ord(name_hash[i]) | 0xF0) ^ 0xF0
    if z <= 9:
       res += chr(0x30+z)
    else:
       res += chr(0x40+z)
 
print ("[+]Name = "+name)
print ("[+]Serial = "+res)


Примеры:
[+]Name = crackmes.de
[+]Serial = 6J01J01851NJ42L42M9M

[+]Name = solutionmes
[+]Serial = 6JNL810M03K3J068308K

[+]Name = H31l0 wOr1d!
[+]Serial = 0863L447N88470998J3J

image01.jpg

Пока не указано иное, содержимое этой страницы распространяется по лицензии Creative Commons Attribution-ShareAlike 3.0 License