Решение daybreak - FuelVM

Сложность: 2 – Needs a little brain (or luck)
Платформа: Windows
Язык: Assembler
Дано: FuelVM

This is a fairly straight forward VM keygenme. If you have not dealt with VMs before it might be a little harder than a 2. Let me know if there are bugs.

-daybreak

Решение (автор slackspace)
опубликовано 11.04.2012

Daybreak в описании к crackme говорит, что его задачка основана на виртуальной машине (сокращенно VM). Как будет сказано ниже, в crackme встроена простейшая виртуальная машина с 16-битным “Intel-подобным” набором инструкций.
Но начнем сначала. Для решения этой задачки я использовал IDA. Большая часть кода была проанализирована статически и только в некоторых случаях я полагался на встроенный отладчик IDA.

1) Инициализация



Основной алгоритм начинается с адреса 0x401142, где программа считывает данные, введенные пользователем (как имя пользователя и пароль) через стандартную WinAPI функцию GetDlgItemTextA(). После того, как прочитали имя пользователя и пароль, вызываем функцию process_input() (начало с 0x401238) по адресу 0x401171.

2) Тривиальные проверки

Сначала в process_input() идет проверка длин имени пользователя и пароля. Обе переменные должны быть не менее 7 символов. Верхний потолок - 11 символов. Его задает сама программа. Именно на границе в 11 символов возникает проблема (она будет описана ниже). Кроме того, цикл по адресу 0x401271 немного обфусцирует имя пользователя с помощью операции XOR каждого символа имени с его порядковым номером. Преобразование, подобное следующему (код на Python):

s = ""
for i in range(len(username)):
     s += chr(ord(username[i]) ^ i) #код символа имени XOR индекс этого символа


Я не буду обсуждать эти проверки более подробно, так как они очень просты для реверса.

3) Цепочка SEH-обработчиков

Я пропущу инициализацию VM (вызов функции init_vm() по адресу 0x4012b0 и 0x4012c8), т.к. она будет обсуждаться в следующем разделе. Вместо этого, я приведу здесь краткое описание антиотладочного трюка, основанного на SEH-обработчике (Windows structured exception handling - структурная обработка исключений). Даже этот кусок кода очень прост, поэтому он не должен быть слишком сложным для реверса.



По адресам 0x4012b5-0x4012c1 приложение устанавливает свой обработчик исключений. Далее по адресу 0x4012cd (int 3) программа принудительно вызывает trap-исключение, обрабатываемое установленным обработчиком, код которого начинается с 0x4012d7. В свою очередь, этот обработчик устанавливает дополнительный обработчик и вызывает еще одно исключение (деление на 0). Следующий обработчик поступает также. Т.о. формируется целая цепочка обработчиков.

Антиотладочный прием, основанный на SEH-цепочке:
1) установка SEH-обработчика
2) генерируем исключение
3) в обработчике ставим новый SEH-обработчик
4) генерируем исключение
5) повторяются шаги 3 и 4, до тех пор, пока новый SEH-обработчик не будет указывать на чистый код.

Чистый код начинается с адреса 0x40135e. По этому адресу находится основной цикл эмуляции байт-кода.

4) Инициализация виртуальной машины

Самая интересная часть crackme - это виртуальная машина. Итак, здесь реализована 16-битная виртуальная машина, состоящая из 4-х регистров, стека размером в 50 байт, указателя стека, указателя инструкций и 2-х флажков (флаг нуля - zero flag, флаг знака - sign flag).
Я назвал четыре регистра общего назначения, как "R1", "R2", "R3" и "R4". "ZF" и "SF" - флаг нуля и флаг знака, соответственно. "stack_pointer" и "program_counter" - глобальные переменные указателя стека и указателя инструкций.
Как говорилось выше, функция инициализации VM - init_vm() вызывается дважды (по адресам 0x4012b0 и 0x4012c8).

Что делает init_vm():



Все регистры и флаги устанавливаются равными нулю, за исключением регистра "R4", который содержит текущий байт имени пользователя."stack_pointer" инициализируется 0x32 (перемещаем к верхней границе стека).

Как вы можете видеть из кода init_vm(), "keyindex" глобальная переменная, номер позиции символа как в строке имени пользователя, так и в пароле. С каждым вызовом init_vm() "keyindex" увеличиваем на 1 (переходим к обработке следующего байта имени пользователя). Но т.к. init_vm() вызывается два раза, происходит интересная штука. Смотрите:



Вывод:
Поэтому первый и второй байт нашего пароля могут быть любыми. И длина пароля ВСЕГДА на 1 длиннее (например, длина имени равна 7, то длина пароля равна 8). И вот тут возникает проблема. Программа ограничивает введенные данные длиной в 11 символов. Значит, если длина имени пользователя равна 11, то для правильного пароля надо 12 символов, а программа режит 12-й символ при чтении, оставляя только 11. Результат - badboy-сообщение.

Функция init_vm() вызывается каждый раз при анализе виртуальной инструкции 0xFF. Эта команда сравнивает один байт имени пользователя с байтом пароля (позиция байта имени отличается от позиции байта пароля на единицу). Если они совпадают, то init_vm() двигает "keyindex" к следующему символу имени пользователя.

5) Основной цикл эмуляции байт-кода

Основной цикл эмуляции байт-кода начинается с 0x40135e. Эта процедура реализует традиционную выборку-декодирование-выполнение команд-инструкций VM. Сам байт-код хранится в глобальном массиве по адресу 0x403000, а "program_counter" используется в качестве индекса в этом массиве. Инструкции имеют переменную длину, и согласно длины текущей инструкции декодер обновляет "program_counter" соответственно.

В двух словах, VM поддерживает следующие инструкции:



Микрокод инструкций достаточно просто реверсить, поэтому я не буду подробно на нем останавливаться.

Если встречается инструкция PASSWORD_CHECK, то VM сравнивает значение в регистре R1 с текущим байтом пароля. Микрокод PASSWORD_CHECK:



Если сравнение по адресу 0x401935 успешно, init_vm() вызывается снова, переходя к обработке следующего байта имени пользователя, обнуляя регистры и выставляя значение указателей по умолчанию. В противном случае, выполнение crackme прекращается.

Замечу, что вызов init_vm() обнуляет “program_counter” и байт-код выполняется сначала. Если просто проанализировать байт-код визуально, то актуальный код занимает всего лишь 80 байт (00403000 - 00403050). Еще один момент, в выполняемом байт-коде нет ни одной команды перехода (JMP, JZ, JS, JZS - опкод 0х1Е).

6) Кейген

Есть два варианта решения crackme:
1) Написать свой эмулятор байт-кода, что сделал автор решения.
Автор решения в качестве кейгена написал скрипт на Python, где в командной строке передается имя пользователя, а сам скрипт, выполняя байткод, создает нужный пароль (помним, что 0-й и 1-й байт пароля может быть абсолютно любым).

Пример:
$ python fuelvm_keygen.py slackspace
[*] Password for user 'slackspace' is: iVJDGHQQALK

2) Разобрать байт-код (благо, он совсем простой), написать свой кейген.
Можно дописать fuelvm_keygen.py, чтобы получить листинг инструкций



Анализируем листинг, вычищая мусорные инструкции. Удивительно, но весь код можно свести к очень простой конструкции:
move R1, R4
xor R1, 0x27 - соответствующий байт имени пользователя XOR 0x27

Код кейгена:

import sys, random, string
 
originaluser = sys.argv[1]
 
if len(originaluser) < 7:
    print "[!] Длина имени должна быть от 7 до 10 символов"
    exit(0)
if len(originaluser) > 10:
    print "[!] Длина имени должна быть от 7 до 10 символов"
    exit(0)
 
# Обфускация имени пользователя
s = ""
for i in range(len(originaluser)):
    s += chr(ord(originaluser[i]) ^ i)
 
USERNAME = s
PASSWORD = ""
 
# Первые два символа пароля выбираем случайно
PASSWORD += random.choice(string.letters + string.digits)
PASSWORD += random.choice(string.letters + string.digits)
 
s = ""
for i in range(1,len(USERNAME)):
    x = ord(USERNAME[i]) ^ 0x27 #весь байт-код выполняется ради этой инструкции
 
    # PASSWORD_CHECK
    if x < 0x21:
         x += 0x21
    PASSWORD += chr(x)
 
print (PASSWORD)


Примеры:
[*]Username: daybreak
[+]Password: XLG\FQG@K

[*]Username: solution
[+]Password: 8aIIQWKNN

image00.jpg
image01.jpg

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