Недавно я углубился в проблемы MalwareTech. Причина, по которой я начал с vm1, заключается в том, что я не смог выполнить ее, когда впервые обратился к ней в 2018 году.

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

Об этом вызове

vm1.exe реализует простую 8-битную виртуальную машину (ВМ), чтобы попытаться помешать реверс-инженерам получить флаг. Оперативная память виртуальной машины содержит зашифрованный флаг и некоторый байт-код для его расшифровки. Можете ли вы разобраться, как работает виртуальная машина, и написать свою для расшифровки флага? Копия оперативной памяти ВМ предоставлена ​​в файле ram.bin (эти данные идентичны содержимому оперативной памяти ВМ вредоносной программы перед запуском и содержат как пользовательский код сборки, так и зашифрованный флаг).

Правила и информация

  • Вам не нужно запускать vm1.exe, эта задача предназначена только для статического анализа.
  • Не используйте отладчик или дампер для извлечения расшифрованного флага из памяти, это мошенничество.
  • Анализ можно провести с помощью бесплатной версии IDA Pro (отладчик не нужен).

Скачать: https://github.com/MalwareTech/Beginner-Reversing-Challenges/raw/master/vm1.zip

Пароль: MalwareTech

В начале

Итак, самое первое, что я делаю, начиная анализ исполняемого файла, — собираю информацию о нем. Кажется логичным, верно? Вы узнаете, для чего предназначена программа, которую вы пытаетесь реверсировать. Таким образом, вы можете провести дополнительные исследования, если у вас нет опыта работы с функциональностью программы.

Итак, что вы пытаетесь сказать MovSec?

Вау, успокойся и не откусывай мне голову, объясню по-человечески.

Если у вас есть 2 яблока, и я просто шучу, я не собираюсь приносить сюда яблоки, однако вы бы не стали есть яблоко от человека, которого вы не знаете, не так ли?

Я бы предпочел спросить человека, знают ли они, кто тот человек, который дал вам яблоко, но даже если они подтвердят, кому вы достаточно доверяете, чтобы съесть яблоко. Это большой заговор, не так ли. Впрочем, вернемся к обсуждаемой теме.

Мы знаем, что мы получаем краткий обзор задачи, мы получаем 2 файла, когда загружаем zip-папку и извлекаем содержимое в указанный каталог. MalwareTech также был достаточно великодушен, чтобы не использовать оптимизацию компилятора. Хотя безопасность в данном случае не имеет значения.

Так что же происходит, когда мы загружаем исполняемый файл в IDA?

Мы видим, что сама программа скомпилирована для 32-битных машин. Он использует соглашение о вызовах Intel x86. См. рисунок ниже.

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

Так что же происходит на картинке выше?

Первые 5 инструкций не важны, это настройка стека и я считаю функцию хеширования MD5. Следующие 5 инструкций настраивают кучу. Я добавил фрагмент кода, чтобы дать вам общее представление, он написан на псевдокоде.

HeapAlloc(507, HEAP_ZERO_MEMORY, GetProcessHeap());

Ого, это написано на C, лол

Таким образом, это в основном устанавливает кучу и выделяет ей 507 (0x1FB) байтов.

Следующие 6 инструкций после того, как мы настроим кучу, в основном передают аргументы в memcpy, я подробно расскажу об этом ниже.

Первая инструкция возвращает нашу выделенную кучу (eax) и помещает ее в Dst. Следующая инструкция проталкивает 507 байтов, как показано выше, затем следующая инструкция проталкивает смещение неизвестной функции, затем, наконец, мы проталкиваем нашу выделенную кучу (eax). Ниже я добавил фрагмент псевдокода.

// memcpy(src, dst, size_t n)
memcpy(allocated_heap, unk_404040, 507);

Хорошо, пока у нас есть следующий псевдокод, см. фрагмент ниже.

alloc = HeapAlloc(507, HEAP_ZERO_MEMORY, GetProcessHeap());
// memcpy(dst, src, size_t n)
memcpy(alloc, unk_404040, 507);

Давайте перейдем к дальнейшему изучению нашего ассемблерного кода. Мы поговорим о следующих 6 инструкциях.

Инструкция 1 добавит 0Ch (12) байт в регистр esp (расширенный указатель стека), инструкция 2 вызовет подпрограмму, которую мы проверим дальше после того, как объясним, что происходит с остальными нашими инструкциями. Затем мы видим Dst, который, как мы изначально предполагали, был нашей выделенной областью пространства кучи.

Должно быть, он был ранее изменен с помощью memcpy, который мы сделали, и это каким-то образом должно было получить флаг. потому что в следующей инструкции Dst затем устанавливается в ecx, затем следующая инструкция помещает ecx (в MD5::digestString(char *)) но после этой инструкции есть инструкция, которая загружает адрес с помощью регистра LEA (Load Effective Address) , но есть переменная, установленная в ecx, [ebp+var_90], кажется, она используется дважды, сначала мы видим ее вверху, а затем внизу, но только когда мы вызываем функцию MD5. Не будем слишком беспокоиться об этом. Итак, далее у нас остается следующий псевдокод.

// Allocating some heap space
alloc = HeapAlloc(507, HEAP_ZERO_MEMORY, GetProcessHeap());
// memcpy(dst, src, size_t n)
memcpy(alloc, unk_404040, 507);
// Getting the flag?
flag = VM(); 
// Hashing our flag 
MD5::digestString(flag)

Давайте углубимся в нашу предполагаемую виртуальную машину(); функция, которая относится к подпрограмме sub_4022E0 на фотографии, показанной выше.

Смотрите ниже еще одну замечательную загадочную картинку.

Подождите, MovSec, это слишком хорошо, чтобы быть правдой, только это чертовски мало?

Возможно, но давайте пока не будем раскрывать это, подождите, ребята, вы, вероятно, следите за IDA (sp00ked). Однако давайте поговорим о том, что делают эти несколько инструкций.

Первый блок устанавливает стек и перемещает значение 0 в var_1. Очень прямо вперед, переходя к следующему блоку.

2-й блок перемещает 1 в eax, затем регистр test выполняет побитовый оператор И над двумя операндами, если eax равен 0, то вызывается этот блок, см. ниже.

Что в основном очищает стек и возвращает.

Но если регистр eax не равен 0, то мы переходим в этот блок, см. ниже.

Итак, var_1 перемещается в ecx, который является регистром счетчика, поэтому мы можем предположить, что var_1 является своего рода переменной-счетчиком. Следующий Dst, который мы знаем, является нашей выделенной памятью кучи.

Затем мы сохраняем указатель на адрес в выделенном пространстве кучи, псевдокод см. ниже.

var_10 = buffer[counter + 0xFF]

Это повторяется еще 2 раза, но наш счетчик повторяется.

var_10 = buffer[counter + 0xFF]
var_C = buffer[counter + 1 + 0xFF]
var_8 = buffer[counter + 2 + 0xFF]

Однако, когда мы спускаемся вниз по блоку ассемблерного кода, мы видим, что ближе к концу мы помещаем наши переменные var_10, var_C, var_8, фактически передавая их в качестве аргументов функции или подпрограмме с именем sub_402270.

Прежде чем мы углубимся в описанную выше функцию, давайте подведем итоги того, что мы нашли до сих пор. Мы знаем, что нашему буферу выделено место в куче. Мы используем memcpy, чтобы скопировать в него определенное количество байтов, но мы копируем из исходного местоположения, которое было тем же именем/адресом функции, что и упомянутое выше.

Итак, теперь мы углубимся в функцию sub_402270 и выясним, что она делает. Смотрите картинку, которую я прикрепил ниже.

Мы видим аргументы, которые мы передали ранее в подпрограмму, они упорядочены так: arg_0, arg_4, arg_8. поскольку Intel x86 является FILO (First in last out), мы знаем, что наши аргументы следующие, см. ниже.

arg_0 = var_10, arg_4 = var_C и arg_8 = var_8;

arg_0 перемещается в eax, а затем eax перемещается в нашу переменную var_4, затем наша переменная var_4 сравнивается со значением 1. Давайте посмотрим блок, если это правда.

Итак, поскольку мы помним, что Dst — это наша выделенная память кучи, мы можем вызвать этот буфер. Мы перемещаем наш буфер в ecx, затем следующая инструкция добавляет наш arg_4 в ecx, затем arg_8 перемещается в младшие биты edx, который является регистром dl, затем ecx получает свое значение. Смотрите мой псевдокод ниже.

ecx[arg_4] = dl;

Теперь давайте посмотрим предыдущий блок перед выше, когда это не так.

Итак, теперь очевидно, что var_4 — это код операции, который до сих пор использовался для сравнения со значениями 1, 2. Теперь давайте посмотрим, верен ли блок, когда вышеописанное.

Это выглядит иначе, это не то, чего мы ожидали. Этот блок перемещает наш буфер (Dst) в eax, а затем добавляет arg_4 к eax, как и раньше, но теперь eax назначается cl, а cl перемещается в byte_404240, что такое byte_404240? По сути, это объявление новой переменной и присвоение ей значения eax. См. псевдокод ниже.

some_var = eax[arg_4];

Итак, что происходит, когда предыдущий блок над текущим не соответствует действительности? Давайте посмотрим, см. рисунок ниже.

var_4 снова сравнивается, но на этот раз со значением 3. Посмотрим, что произойдет, если это правда.

Это выглядит загадочно, на самом деле немного пугающе, но не волнуйтесь, мы рассмотрим, что происходит шаг за шагом.

Сначала переменная (byte_404240) вставляется в edx, помните, как она была установлена, когда var_4 сравнивалась с 2. Затем мы перемещаем наш буфер (Dst) в eax, затем добавляем arg_4 в eax, как и раньше, затем перемещаем указатель на eax в ecx и xили ecx и edx вместе. Затем мы перемещаем наш буфер (Dst) в edx и добавляем в него arg_4. Затем cl перемещается в edx, ниже я написал псевдокод.

eax[arg_4] = eax[arg_4] ^ some_var;

Итак, теперь нам нужно знать, что происходит, когда сравниваемый блок не соответствует действительности. См. рисунок ниже.

Это просто еще один прыжок, давайте проследим, куда он прыгнет.

Аль Xor зарегистрируется у себя, для этого потребуется еще один прыжок, снова мы будем следовать за ним.

Это конец, мы в основном просто возвращаемся. Теперь давайте объединим весь этот псевдокод в стиле Python.

var_10 = buffer[counter + 0xFF]
var_C = buffer[counter + 1 + 0xFF]
var_8 = buffer[counter + 2 + 0xFF]
        
if var_10 == 1:
    buf[var_C] = var_8
elif var_10 == 2:
    some_reg = buf[var_C]
elif var_10 == 3:
    buf[var_C] = buf[var_C] ^ some_reg
else:
    break

Так что теперь у нас есть хороший обзор того, что происходит.

Мы можем определить, что var_10 — это код операции, а var_C и var_8 — это операнды.

Помните, что у нас есть еще один файл с именем ram.bin, мы напишем скрипт, откроем этот файл и прочитаем его как массив байтов. Я буду писать свой скрипт на Python.

with open('ram.bin', 'rb') as ram:
    buf = bytearray(ram.read(507))
vm_reg = 0
counter = 0
flag = ''
while True:
    opcode = buf[counter + 0xFF]
    operand1 = buf[counter + 1 + 0xFF]
    operand2 = buf[counter + 2 + 0xFF]
        
    if opcode == 1:
        buf[operand1] = operand2
    elif opcode == 2:
        vm_reg = buf[operand1]
    elif opcode == 3:
        buf[operand1] = buf[operand1] ^ vm_reg
    elif opcode > 3:
        break
    
    counter += 3
for i in range(32):
    flag += chr(buf[i])
print flag

Когда мы запускаем приведенный выше скрипт, мы получаем следующий вывод, см. ниже.

Отлично, мы получили флаг. Если у вас есть какие-либо вопросы, не стесняйтесь, напишите мне в личные сообщения в Твиттере https://www.twitter.com/mov_sec.