Эмулятор исполняемого кода для архитектуры x86 (IA-32)

Эмулятор исполняемого кода для архитектуры x86 (IA-32)

В статье рассматривается принцип работы эмулятора процессора архитектуры Intel x86 (IA-32) и в качестве практики пишется простой эмулятор 32-битного кода, который (эмулятор) при некоторой доработке можно превратить в полноценный "вирутальный компьютер".

Многие наверняка считают, что создание эмулятора "железа" — это довольно сложное напраление, которым занимаются только настоящие гуру вычислительной техники.

Несомненно, это так. Реализация того, как работает настоящая железка, требует хороших знаний и понимания непосредственно её работы в реале, как ни странно =)

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

Всё, что доступно программисту, — это набор инструкций процессора — тех действий, который может выполнить процессор.

Страшно написал =)

Но на самом деле, всё намного проще, и вся эта архитектура для нашего дела практический не нужна!

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

Итак, в этой статье постараюсь максимально просто изложить принцип построения и работы программой модели 32-битного микропроцессора Intel x86.

Почему была выбрана именно эта архитектура? Просто она самая распространённая! Это почти вся линейка процессоров компании Intel, начиная с 8086 и заканчивая последними многоядерными.

Немного теории микропроцессора

Посмотрим на процессор с точки зрения программирования.

Всё, что процессор предосталяет для этих целей:

1. Восемь 32-битных регистров, которые могут использоваться для хранения данных и адресов (их еще называют регистрами общего назначения (РОН)): eax / ax / ah / al ebx / bx / bh / bl edx / dx / dh / dl ecx / cx / ch / cl ebp / bp esi / si edi / di esp / sp

2. Шесть регистров сегментов: cs, ds, ss, es, fs, gs

3. Регистры состояния и управления eflags / flags - Регистр флагов eip / ip - Регистр указателя команды

Есть ещё несколько специальных регистров, но они нам пока не интересны.

4. Набор инструкций

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

Вот пожалуй и всё, что нужно знать для написания программ на языке ассемблера, который максимально близок к "родному" для процессора языку опкодов.

Кстати о опкодах. Опкод (Операционный код) — это инструкция на машинном языке, понятном "железу". Что касательно самого машинного языка, он является ничем иным, как набором чисел.

Однако, сам процессор в отдельности, не смотря на свою "навороченность", железка совершенно бесполезная.

Всё, что он умеет, — это просто исполнять переданные ему инструкции и в соотетствии с ними, например, изменять значения регистров.

Да и для того, чтобы процессор начал выполнять код, этот код нужно сначала куда-то положить, чтобы процессор смог его прочитать.

Нетрудно догадаться, что для хранения кода используется память.

Память бывает разная, но самая близкая к процессору — оперативная. Да, та самая, которой всё больше требуется для современных игр =)

Именно симбиоз процессора и оперативной памяти является основой работы компьютера.

Микропроцессор умеет напрямую работать с памятью — читать и писать данные по нужным ему адресам.

Весь код перед выполнением помещается в оператиную память. Процессор считывает опкоды один за другим и выполняет их.

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

Единственное, что не может процессор, — это за время выполнения одной инструкции прочитать память по одному адресу и записать его в другой.

В таких операциях обязательно задействуются регистры, а сам процесс "перемещения" даных в памяти требует выполнения минимум двух инструкций.

Микропроцессор за время своего существования и разиятия накопил множество режимов работы.

Два самых используемых из них — это реальный 16-битный и защищенный 32-битный.

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

Подробнее о режимах процессора можно найти в интернете, все они хорошо описаны.

Наш эмулятор будет притворяться процессором, работающем в 32-битном "защищенном" режиме. В кавычках потому что для упрощения задачи в нём не будет реализован специальный функционал, предоставляемый настоящим процессором в этом режиме.

Однако это никак не повлияет на исполнение 32-битного кода.

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

Процессор в 32-битном режиме и оперативная память

В 32-битном режиме процессор может адресовывать пространство памяти в дапазоне от нуля до 4 гигабайт, что соотествует допустимому диапазону 32-битного числа (0x00000000 - 0xffffffff).

Операционная система для каждого приложения также выдляет именно такой диапазон памяти.

Тоесть запущенное приложение может записать/считать значения по любому адресу из этого диапазона.

Может возникнуть вопрос:

Как можно выделить 4 гигабайта памяти для каждого запущенного приложения если на комьютере всего 128 мегабайт????

Всё очень просто. Этот диапазон памяти виртуальный =) То есть в физической оперативной памяти находится только "выделенная" память — используемые и активные в данный момент блоки кода/данных. Если физической памяти нужно больше, чем имеется на компьютере, то неиспользуемые блоки сохраняются на жесткий диск и спокойно лежат там в ожидании совоего использования.

Если открыть диспетчер задач, то в нём можно посмотреть какой объем памяти занимает приложение. Для среднего приложения это примерно 5-10 мегабайт. При этом самому приложению доступно 4 гигабайта оперативки.

Технологии эмуляции кода

Существует по крайней мере 2 основных подхода к эмуляции.

Первый является полностью программным, то есть весь эмулируемый код разбирается и выполняется процедурами эмулятора.

Данный подход самый безопасный, т. к. в независимости от того, какой "злой" код ему подсунут, он всё равно не доберётся до настоящего процессора. Также этим методом можно эмулировать произвольные архитектуры процессоров. Так, например, на персональном компьютере с 32-битным процессорм можно эмулировать 64 процессор или вообще процессор мобильного телефона или игровой приставки.

Однако этот подход также самый трудоёмкий и медленный по скорости исполнения эмулируемого кода.

Трудоёмкость заключается в необходимости реализовывать в процедурах каждую инструкцию процессора.

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

Учитывая эти основные минусы, самым большим из которых является скорость исполнения, был разработан метод частичного исполнения эмулируемого кода на настоящем процессоре.

Суть его том, что в эмулируемом коде берётся блок "безопасного" кода и передаётся на исполнение реальному процессору, после чего управление снова переходит на программу эмулятора.

"Безопасного" кода в том смысле, что он не сможет навсегда перехватить управление или обрушить программу, например, попытавшись записать даные из недоступную для записи область памяти.

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

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

Второй по причине частичного исполнения на реальном процессоре эмулируемый код должен быть написан под ту же архитектуру, что и эмулятор.

В нашем эмуляторе мы будем использовать первый метод, потому что он является "самым виртуальным" и проще для понимания и реализации, чем метод частичного исполнения на реальном процессоре.

Виртуальная модель "микропроцессор — оперативная память"

Как было написано выше, процессор и оперативная память очень тесно связаны друг с другом.

Кроме того, для исполнения кода, наличие этих двух компонентов является достаточным условием.

Попробуем построить модель работы процессора и оперативки.

Процессор состоит из двух компонентов: интерпретатора инструкций и набора регистров.

Регистры являются просто ячейками памяти, встроенными в процессор.

Взаимодействие процессора с оперативной памятью сводится к двум простым действиям: чтение и запись байт данных по указанному адресу в опреативной памяти.

Для исполнения инстукции интерпретатор команд в процессоре считывает значение регистра EIP, который хранит адрес инструкции в оперативке. Затем считывает саму инструкцию из оперативной памяти по адресу, взятому из EIP. После этого инерпретатор "разбирает" инструкцию и выполняет указанные в ней действия.

Из этого следует, что для того, чтобы исполнить некоторый код, его нужно разместить в образе оперативной памяти, занести в регистр EIP начальный адрес кода (или точки входа, если она не совпадает с началом самого кода), а затем просто позволить интерпретатору команд выполнить своё назначение.

Вот так просто функционирует модель "процессор — оператиная память".

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

Реализация модели на языке C/C++

Насколько бы не была полезна теория, она никогда не сможет заменить по пользе практику.

Для реализации эмулятора был выбран язык C/C++ как наиболее подходяший для данной цели по возможностям, скорости раработки и понятности кода.

Разобьем модель на компоненты (классы) и напишем функции каждого из них:

1. CImage — класс образ виртуальной памяти

- Чтение по указанному адресу

- Запись по указанному даресу

2. CRegisterSet — класс набора регистров процессора

- Установка значения регистра

- Получение занчения регистра

3. СStack — класс реализации FILO стёка

- Занесение в стёк значения регистра

- Занесение в стёк числа

- Извлечение из стёка значения регистра

- Извлечение из стёка числа

4. CImulInstance — класс, объединяющйи в себе предыдущие 3 класса и содержащий интерпретатор кода

- Инициализация классов памяти, регистров и стёка.

- Интерпретация кода

Класс стёка не является непосредственным компонентом модели, он всего лишь реализация части одной из возможностей процессора.

Однако был выделен в отдельный класс как функциональная единица.

Данный класс для своей работы использует классы образа виртуальной памяти и набор регистров.

Последний класс CImulInstance объединяет в себе остальные классы и является главным объектом, предсталяющим саму модель "процессор — оперативная память".

В дальнейшем в него могут быть добавлены и другие компоненты, например жесткий диск, CD-ROM и т. д.

Разбор опкодов будем проводить при помощи движка Hacker Dissasembler Engine - HDE от Вячеслава Патькова.

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

На рисунке ниже представлена архитектура эмулятора

Начем реализацию с перечисления особенностей архитектуры Intel x86 (IA-32)

Они будут раполагаться в файле intel. h

Для реализации модели нам понадобится перечислить все регистры и размеры битности, поддерживаемые процессором.

Пуcть в этой статье рассматриваем только эмуляцию 32-битного кода, в эмулятор заложим поддержку 16-битного кода, реализовать которую вы сможете уже самостоятельно.

Листинг : intel. h - Заголовочный файл процессора

#ifndef _INTEL_H__INCLUDED_

#define _INTEL_H__INCLUDED_

// - Перечисление режимов битности typedef enum{

EBM_16,

EBM_32

} E_BIT_MODE;

// - Перечисление регистров typedef enum{

ERN_EAX, ERN_AX, ERN_AL, ERN_AH,

ERN_EBX, ERN_BX, ERN_BL, ERN_BH,

ERN_ECX, ERN_CX, ERN_CL, ERN_CH,

ERN_EDX, ERN_DX, ERN_DL, ERN_DH,

ERN_EBP, ERN_BP,

ERN_ESP, ERN_SP,

ERN_ESI, ERN_SI,

ERN_EDI, ERN_DI,

ERN_EIP, ERN_IP,

ERN_CS,

ERN_DS,

ERN_SS,

ERN_ES,

ERN_FS,

ERN_GS,

ERN_EFLAGS, ERN_FLAGS

} E_REGISTER_NAME;

#endif

Теперь создадим заголовочный файл эмулятора imul. h

В этом файле будут содержаться объявления упомянутых выше компонентов модели.

Сначала объявим структуру SMemoryBlock, которая будет описывать выделенный блок памяти, а точнее его смещение в адресном пространстве эмулятора, размер блока и указательна "физическую" память — ту, что выделяет нам система.

Листинг : imul. h - SMemoryBlock - Блок памяти

//! Блок памяти образа typedef struct SMemoryBlock{ public:

SMemoryBlock(){ this->Offset = 0; this->Size = 0; this->hMem = NULL;

}

~SMemoryBlock(){ if(this->hMem!= NULL) free(this->hMem);

}

DWORD Offset; // - Смещение блока

DWORD Size; // - Размер блока

PBYTE hMem; // - Указатель на память

} * PSMemoryBlock;

Теперь объявим класс образа памяти эмулятора.

Листинг : imul. h - CImage - Образ памяти эмулятора

//! Образ памяти (адресного пространства) typedef class CImage{ private:

PSMemoryBlock * MemBlock; // - Таблица блоков

DWORD MemBlockCount; // - Число активных блоков

DWORD MaxMemBlockCount; // - Максимальное число блоков (размер таблицы)

//! Поиск блока по адресу

PSMemoryBlock findBlockByAddress(DWORD VirtualAddress); public:

CImage(){

// - Расчёт числа блоков памяти this->MaxMemBlockCount = 0xFFFFFFFF / MEMORY_BLOCK_SIZE; this->MemBlock = (PSMemoryBlock*)calloc(this->MaxMemBlockCount, sizeof(PSMemoryBlock)); this->MemBlockCount = 0;

}

~CImage(){ if(this->MemBlock!= NULL){ for(DWORD I = 0; I < this->MaxMemBlockCount; I++) if(this->MemBlock<I> != NULL) this->MemBlock<I>; free(this->MemBlock);

}

}

// -

//! Добавление блока в образ

PSMemoryBlock addBlock(IN DWORD Offset, IN DWORD Size);

//! Запись в память bool writeMem(IN DWORD VirtualAddress, IN PVOID Data, IN DWORD Size);

//! Чтение из памяти bool readMem(IN DWORD VirtualAddress, OUT PVOID Data, IN DWORD Size);

} * PCImage;

При иницилизации класса сразу выделяется память для всех блоков памяти. Это, конечно, не экономично (для блоков размером 0x1000 байт нужно ~12 Мб памяти), но зато наглядно и просто.

По-хорошему здесь нужно применять алгоритмы для быстрого добавления и поиска блоков.

Кстати о поиске. Класс имеет приватную функцию findBlockByAddress, которая принимает произвольный адрес, а в ответ выдает ссылку на блок памяти, описывающий выделенную память, в которую попадает адрес или возвращает NULL, если память по указанному адоресу не выделена.

Функция просто перебирает все выделенные блоки в таблице блоков и проверяет, попадает ли переданный адрес во внутрь блока.

Ниже её реализация.

Листинг : CImage. cpp - findBlockByAddress

//! Поиск блока по адресу

PSMemoryBlock CImage::findBlockByAddress(DWORD VirtualAddress){

// - Ищем блок, расположенный по этому адресу for(DWORD I = 0; I < this->MaxMemBlockCount; I++){ if(this->MemBlock<I> == NULL) continue; if( VirtualAddress >= this->MemBlock<I>->Offset && VirtualAddress < (this->MemBlock<I>->Offset + this->MemBlock<I>->Size)){ return this->MemBlock<I>;

}

} return NULL;

}

Переходим к публичным функциям.

Первая из них — addBlock

Эта функция выделяет память (создает блок памяти) по указанному адресу.

Однако адрес, по которому может располагаться блок, должен быть кратен размеру блока.

Также и размер блока должен быть кратен размеру минимальному размеру блока.

То есть если размер блока у на 0x1000 и мы хотим создать блока по адресу 0x4500 и размером 0x1568 байт, то блок будет создан по адресу 0x4000 и размером 0x2000 байт.

Листинг : CImage. cpp - addBlock

// -

//! Добавление блока в образ

PSMemoryBlock CImage::addBlock(IN DWORD Offset, IN DWORD Size){

// - Проверям сущестование блока

PSMemoryBlock Block = this->findBlockByAddress(Offset); if(Block!= NULL) return Block;

// - Выравнивание размера if(Size < MEMORY_BLOCK_SIZE) Size = MEMORY_BLOCK_SIZE; if(Size > MEMORY_BLOCK_SIZE){

Size = (Size / (DWORD)MEMORY_BLOCK_SIZE) * MEMORY_BLOCK_SIZE + MEMORY_BLOCK_SIZE;

}

// - Выравнивание смещения

Offset = (Offset / (DWORD)MEMORY_BLOCK_SIZE) * MEMORY_BLOCK_SIZE;

// - Создание нового блока

Block = new SMemoryBlock;

Block->Offset = Offset;

Block->Size = Size;

Block->hMem = (PBYTE)calloc(Block->Size, sizeof(char)); printf("> New BlocktAddr: 0x%x / Size: 0x%xn", Block->Offset, Block->Size); this->MemBlock[this->MemBlockCount] = Block; this->MemBlockCount++; return Block;

}

Остались только функции чтения записи в память.

WriteMem записывает данные в образ памяти эмулятора.

Так как писать и читать можно только в выделеную память (невыделенной памяти физически не существует), то прежде чем произвести операцию чтения/записи, нужно убедиться, что по указанному адресу находится выделенная память.

В случае с записью можно при обращении в нераспределённую память автоматически выделять блок.

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

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

Ниже код функции записи в память.

Листинг : CImage. cpp - writeMem

//! Запись в память bool CImage::writeMem(IN DWORD VirtualAddress, IN PVOID Data, IN DWORD Size){

// - Ищем блок. Если нет, то создаем

PSMemoryBlock Block = this->findBlockByAddress(VirtualAddress); if(Block == NULL) Block = this->addBlock(VirtualAddress, Size); if(Block == NULL) return false;

// - Получаем смещение относительно границы блока

DWORD Offset = VirtualAddress - Block->Offset;

// - Свободное место до конца блока

DWORD BytesToWrite = Size;

DWORD BytesWritten = 0;

BOOL SwitchBlock = FALSE; while(BytesWritten < Size){

// - Если не хватает места в блоке, то пишем до конца блока if(BytesToWrite > (Block->Size - Offset)){

BytesToWrite = Block->Size - Offset;

SwitchBlock = TRUE;

}

// - Пишем данные в память эмулятора memcpy((PVOID)((DWORD)Block->hMem + Offset), (PVOID)((DWORD)Data + BytesWritten), BytesToWrite);

BytesWritten += BytesToWrite;

// - Переходим с следующий блок if(SwitchBlock){

Block = this->findBlockByAddress(Block->Offset + Block->Size); if(Block == NULL) this->addBlock(Block->Offset + Block->Size, MEMORY_BLOCK_SIZE);

Offset = 0; // - Пишем от начала блока

SwitchBlock = FALSE;

}

}

//printf("> Access WritetAddr: 0x%x / Size: 0x%x / Block: 0x%xn", VirtualAddress, Size, Block->Offset); return true;

}

Не сильно отличается и код чтения.

Здесь также осуществляется переключение на следующий блок при выходе за границу текущего.

Если блок не существует, то он создается.

Листинг : CImage. cpp - readMem

//! Чтение из памяти bool CImage::readMem(IN DWORD VirtualAddress, OUT PVOID Data, IN DWORD Size){

PSMemoryBlock Block = this->findBlockByAddress(VirtualAddress); if(Block == NULL){ printf("> Read EmptytAddr: 0x%x / Size: 0x%xn", VirtualAddress, Size); return false;

}

// - Получаем смещение относительно границы блока

DWORD Offset = VirtualAddress - Block->Offset;

// - Свободное место до конца блока

DWORD BytesToRead = Size; // - Сколько нужно прочитать

DWORD BytesReaded = 0; // - Сколько уже прочитано

BOOL SwitchBlock = FALSE; // - Необходимо сменить блок while(BytesReaded < Size){

// - Если запрошено больше чем есть в блоке то читаем только из это блока if(BytesToRead > (Block->Size - Offset)){

BytesToRead = Block->Size - Offset;

SwitchBlock = TRUE;

}

// - Читаем данные из памяти эмулятора memcpy((PVOID)((DWORD)Data + BytesReaded), (PVOID)((DWORD)Block->hMem + Offset), Size);

BytesReaded += BytesToRead;

// - Переходим в следующий блок if(SwitchBlock){

Block = this->findBlockByAddress(Block->Offset + Block->Size); if(Block == NULL) this->addBlock(Block->Offset + Block->Size, MEMORY_BLOCK_SIZE);

Offset = 0; // - Читаем от начала блока

SwitchBlock = FALSE;

}

}

//printf("> Access ReadtAddr: 0x%x / Size: 0x%x / Block: 0x%xn", VirtualAddress, Size, Block->Offset); return true;

}

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

Серым цветом обозначено всё адресное пространство, доступное приложению. Физически память для серой области не выделяется. То есть, если просто создать образ памяти в 4 гигабайта, то реально он будет занимать 0 байт.

Память выделяется только для блоков, омеченых желтым цветом.

Блок 1 и Блок 2 например могут содержать код и данные.

Блок 3 на картинке используется стёком, который как известно растёт к меньшим адресам. То есть при каждом PUSH значение регистра, указателя верхушки стёка, уменьшается на размер занесённых в данных.

И наоборот, POP увеличивает значение ESP.

Именно в связи с таким поведением стёка, необходимо выделять ему блок памяти не по адресу из ESP, а по адресу ESP минус размер стёка, чтобы ESP при пустом стёке указывал на конец блока памяти, а не на его начало. Иначе при первом же PUSH произойдет выход за границу выделенной памяти — в серую область.

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

У нас же такого исключения не произойдет по причине автоматического выделения нового блока и отсуствия прав на доступ к блоку.

Кстати, такой нюанс: если при выходе за границу существующего блока не выделить новый и не остановить операцию чтения/записи (например, создав исключение), то упадёт уже не программа внтури эмулятора, а сам эмулятор, потому что исключение всё-таки будет вызвано, но уже операционной системой для нашего эмулятора :)

Так что будьте внимательней при придоставлении памяти эмулируемой программе. Она не должна валить эмулятор ни при каких условиях.

С образом памяти закончено.

Теперь переходим к следующему компоненту — набору регистров CRegisterSet

Сначала объявим его в imul. h

Листинг : imul. h - CRegisterSet - набор регистров

//

//! Регистры

// typedef class CRegisterSet{ public:

CRegisterSet(){

// - Инциализируем регистры нулями memset(this->EAX, 0, sizeof(this->EAX)); memset(this->EBX, 0, sizeof(this->EBX)); memset(this->ECX, 0, sizeof(this->ECX)); memset(this->EDX, 0, sizeof(this->EDX)); memset(this->ESI, 0, sizeof(this->ESI)); memset(this->EDI, 0, sizeof(this->EDI)); memset(this->EBP, 0, sizeof(this->EBP)); memset(this->ESP, 0, sizeof(this->ESP)); memset(this->EIP, 0, sizeof(this->EIP)); memset(this->CS, 0, sizeof(this->CS)); memset(this->DS, 0, sizeof(this->DS)); memset(this->SS, 0, sizeof(this->SS)); memset(this->ES, 0, sizeof(this->ES)); memset(this->FS, 0, sizeof(this->FS)); memset(this->GS, 0, sizeof(this->GS)); memset(this->FLAGS, 0, sizeof(this->FLAGS));

}

//! Установка значения регистра bool setValue(IN E_REGISTER_NAME Reg, IN PVOID Value);

//! Получение значения регистра bool getValue(IN E_REGISTER_NAME Reg, OUT PVOID Value);

//! Получение размера регистра

BYTE getRegisterSize(E_REGISTER_NAME Reg);

//! Получение регистра по его ID

E_REGISTER_NAME CRegisterSet::getRegByID(BYTE RegID, E_BIT_MODE Mode); protected:

// - РОН / Указатель

BYTE EAX[4];

BYTE EBX[4];

BYTE ECX[4];

BYTE EDX[4];

BYTE EBP[4];

BYTE ESP[4];

BYTE ESI[4];

BYTE EDI[4];

BYTE EIP[4];

// - Сегментные

BYTE CS[2];

BYTE DS[2];

BYTE SS[2];

BYTE ES[2];

BYTE FS[2];

BYTE GS[2];

// - Флаги

BYTE FLAGS[4]; // eflags / flags

} * PCRegisterSet

И реализация функций setValue, getValue, getRegisterSize

Листинг : CRegisterSet. cpp - Реализация функций класса

//! Установка значения регистра bool CRegisterSet::setValue(IN E_REGISTER_NAME Reg, IN PVOID Value){ if(Value == NULL) return false; switch(Reg){ case ERN_EAX: memcpy(this->EAX, Value, 4); break; case ERN_EBX: memcpy(this->EBX, Value, 4); break; case ERN_ECX: memcpy(this->ECX, Value, 4); break; case ERN_EDX: memcpy(this->EDX, Value, 4); break; case ERN_EBP: memcpy(this->EBP, Value, 4); break; case ERN_ESP: memcpy(this->ESP, Value, 4); break; case ERN_ESI: memcpy(this->ESI, Value, 4); break; case ERN_EDI: memcpy(this->EDI, Value, 4); break; case ERN_EIP: memcpy(this->EIP, Value, 4); break; case ERN_AX: memcpy(this->EAX, Value, 2); break; case ERN_BX: memcpy(this->EBX, Value, 2); break; case ERN_CX: memcpy(this->ECX, Value, 2); break; case ERN_DX: memcpy(this->EDX, Value, 2); break; case ERN_BP: memcpy(this->EBP, Value, 2); break; case ERN_SP: memcpy(this->ESP, Value, 2); break; case ERN_SI: memcpy(this->ESI, Value, 2); break; case ERN_DI: memcpy(this->EDI, Value, 2); break; case ERN_IP: memcpy(this->EIP, Value, 2); break; case ERN_CS: memcpy(this->CS, Value, 2); break; case ERN_DS: memcpy(this->DS, Value, 2); break; case ERN_SS: memcpy(this->SS, Value, 2); break; case ERN_ES: memcpy(this->ES, Value, 2); break; case ERN_FS: memcpy(this->FS, Value, 2); break; case ERN_GS: memcpy(this->GS, Value, 2); break; case ERN_EFLAGS: memcpy(this->FLAGS, Value, 2); break; case ERN_FLAGS: memcpy(this->FLAGS, Value, 2); break; default: return false;

}; return true;

}

//! Получение значения регистра bool CRegisterSet::getValue(IN E_REGISTER_NAME Reg, OUT PVOID Value){ if(Value == NULL) return false; switch(Reg){ case ERN_EAX: memcpy(Value, this->EAX, 4); break; case ERN_EBX: memcpy(Value, this->EBX, 4); break; case ERN_ECX: memcpy(Value, this->ECX, 4); break; case ERN_EDX: memcpy(Value, this->EDX, 4); break; case ERN_EBP: memcpy(Value, this->EBP, 4); break; case ERN_ESP: memcpy(Value, this->ESP, 4); break; case ERN_ESI: memcpy(Value, this->ESI, 4); break; case ERN_EDI: memcpy(Value, this->EDI, 4); break; case ERN_EIP: memcpy(Value, this->EIP, 4); break; case ERN_AX: memcpy(Value, this->EAX, 2); break; case ERN_BX: memcpy(Value, this->EBX, 2); break; case ERN_CX: memcpy(Value, this->ECX, 2); break; case ERN_DX: memcpy(Value, this->EDX, 2); break; case ERN_BP: memcpy(Value, this->EBP, 2); break; case ERN_SP: memcpy(Value, this->ESP, 2); break; case ERN_SI: memcpy(Value, this->ESI, 2); break; case ERN_DI: memcpy(Value, this->EDI, 2); break; case ERN_IP: memcpy(Value, this->EIP, 2); break; case ERN_CS: memcpy(Value, this->CS, 2); break; case ERN_DS: memcpy(Value, this->DS, 2); break; case ERN_SS: memcpy(Value, this->SS, 2); break; case ERN_ES: memcpy(Value, this->ES, 2); break; case ERN_FS: memcpy(Value, this->FS, 2); break; case ERN_GS: memcpy(Value, this->GS, 2); break; case ERN_EFLAGS: memcpy(Value, this->FLAGS, 2); break; case ERN_FLAGS: memcpy(Value, this->FLAGS, 2); break; default: return false;

}; return true;

}

//! Получение размера регистра

BYTE CRegisterSet::getRegisterSize(E_REGISTER_NAME Reg){ switch(Reg){ case ERN_EAX: case ERN_EBX: case ERN_ECX: case ERN_EDX: case ERN_ESI: case ERN_EDI: case ERN_ESP: case ERN_EBP: case ERN_EIP: return 4; break; case ERN_AX: case ERN_BX: case ERN_CX: case ERN_DX: case ERN_SI: case ERN_DI: case ERN_SP: case ERN_BP: case ERN_IP: case ERN_CS: case ERN_DS: case ERN_SS: case ERN_ES: case ERN_FS: case ERN_GS: case ERN_EFLAGS: case ERN_FLAGS: return 2; break; case ERN_AL: case ERN_AH: case ERN_BL: case ERN_BH: case ERN_CL: case ERN_CH: case ERN_DL: case ERN_DH: return 1; break; default: return 0; break;

}

}

//! Получение регистра по его ID

E_REGISTER_NAME CRegisterSet::getRegByID(BYTE RegID, E_BIT_MODE Mode){ switch (Mode){ case EBM_16: switch (RegID){ case 0: return ERN_AX; break; case 1: return ERN_CX; break; case 2: return ERN_DX; break; case 3: return ERN_BX; break; case 4: return ERN_SP; break; case 5: return ERN_BP; break; case 6: return ERN_SI; break; case 7: return ERN_DI; break;

} break; case EBM_32: switch (RegID){ case 0: return ERN_EAX; break; case 1: return ERN_ECX; break; case 2: return ERN_EDX; break; case 3: return ERN_EBX; break; case 4: return ERN_ESP; break; case 5: return ERN_EBP; break; case 6: return ERN_ESI; break; case 7: return ERN_EDI; break;

} break;

}

}

Эти функции очень простые и не нуждаются в объяснениях.

Есть более интересный компонент — класс CStack, реализующий работу стёка.

Объявим его в imul. h

Листинг : imul. h - CStack

//

//! Стёк

// typedef class CStack{ protected:

DWORD Size; // - Размер стёк

DWORD Base; // - База стёка

PCRegisterSet RegSet; // - Регистры

PCImage Image; // - Образ памяти

E_BIT_MODE BitMode; // - Режим битности public:

CStack(PCRegisterSet RegisterSet, PCImage Image, E_BIT_MODE Mode){ this->RegSet = RegisterSet; this->Image = Image; this->BitMode = Mode; this->Size = 0; this->Base = 0;

}

//! Инициализаця стёка bool initialize(DWORD VirtualAddress, DWORD Size);

//! Удаление стека bool ();

//! PUSH [value] bool push(IN PVOID Value, DWORD Size);

//! POP [value] bool pop(OUT PVOID Value, IN DWORD Size);

//! PUSH [reg] bool pushReg(E_REGISTER_NAME Reg);

//! POP [reg] bool popReg(E_REGISTER_NAME Reg);

} * PCStack;

Как видно, для работы класса необходимы образ памяти и набор регистров.

Прежде чем использовать стёк, его нужно инициализировать функией initialize.

Эта функция принимает адрес начала стёка и его размер.

Смысл функции очень прост: выделить блок памяти по адресу (VirtualAddress - Size) размером в Size, естественно, с выравниванием по размеру блока.

После этого нужно устновить значение регистра ESP на начало стёка.

Листинг : CStack. cpp - initialize

//! Инициализаця стёка bool CStack::initialize(DWORD VirtualAddress, DWORD Size){ this->Base = VirtualAddress; this->Size = Size; switch(this->BitMode){ case EBM_16:

SHORT SP;

SP = (SHORT)this->Base; this->RegSet->setValue(ERN_SP, &SP); case EBM_32: this->RegSet->setValue(ERN_ESP, &this->Base); break;

} printf("> Stack InittAddr: 0x%x / Size: 0x%xn", this->Base, this->Size); return true;

}

Следующие 2 функции — push / pop — являются реализацией работы одноименных ассемблерных команд для данных.

На вход функции принимают ссылку на буфер с данными и размер данных, которые нужно заслать или считать из стёка.

Листинг : CStack. - Реализация push / pop для данных

//! PUSH [value] bool CStack::push(IN PVOID Value, DWORD Size){ if(this->Base == 0) return 0; switch(this->BitMode){ case EBM_16:

SHORT SP; this->RegSet->getValue(ERN_SP, &SP);

SP -= Size; this->Image->writeMem((DWORD)SP, Value, Size); this->RegSet->setValue(ERN_SP, &SP); break; case EBM_32:

DWORD ESP; this->RegSet->getValue(ERN_ESP, &ESP);

ESP -= Size; this->Image->writeMem(ESP, Value, Size); this->RegSet->setValue(ERN_ESP, &ESP); break; default: return false; break;

}; return true;

}

//! POP [value] bool CStack::pop(OUT PVOID Value, IN DWORD Size){ if(this->Base == 0) return false; switch(this->BitMode){ case EBM_32:

DWORD ESP; this->RegSet->getValue(ERN_ESP, &ESP); this->Image->readMem(ESP, Value, Size);

ESP += Size; this->RegSet->setValue(ERN_ESP, &ESP); break;

} return true;

}

Ещё две функии, pushReg и popReg, предназначены для работы с регистрами.

Эти функции принимают только имя регистра, с которым нужно совершить операцию.

Листинг : CStack. cpp - pushReg / popReg

//! PUSH [reg] bool CStack::pushReg(E_REGISTER_NAME Reg){ if(this->Base == 0) return false; switch(this->BitMode){ case EBM_32:

DWORD ESP, RegSize;

BYTE Data[4]; this->RegSet->getValue(ERN_ESP, &ESP); // - Получаем ESP

RegSize = (DWORD)this->RegSet->getRegisterSize(Reg); // - Получаем размер регистра

ESP -= RegSize; this->RegSet->setValue(ERN_ESP, &ESP); // - Обновляем ESP; this->RegSet->getValue(Reg, Data); // - Получаем значение регистра this->Image->writeMem(ESP, Data, RegSize); // - Пишем в память break;

} return true;

}

//! POP [reg] bool CStack::popReg(E_REGISTER_NAME Reg){ if(this->Base == 0) return false; switch(this->BitMode){ case EBM_32:

DWORD ESP, RegSize;

BYTE Data[4]; this->RegSet->getValue(ERN_ESP, &ESP); // - Получаем ESP

RegSize = (DWORD)this->RegSet->getRegisterSize(Reg); // - Получаем размер регистра this->Image->readMem(ESP, &Data, RegSize); // - Читаем память this->RegSet->setValue(Reg, &Data); // - Обновляем значение регистра

ESP += RegSize; this->RegSet->setValue(ERN_ESP, &ESP); // - Обновляем ESP break;

} return true;

}

Вот и всё, что касается стёка.

Можно переходить к гланому классу — CImulInstance.

Этот класс, как было написано выше, объяединяет отдельные классы в одну систему — эмулятор.

Объявим этот класс так же в imul. h

Листинг : imul. h - CImulInstance

//

//! Экземпляр эмулятора

// typedef class CImulInstance{ public:

CImulInstance(E_BIT_MODE Mode){ this->Image = new CImage; this->RegSet = new CRegisterSet; this->Stack = new CStack(this->RegSet, this->Image, Mode); printf("> Emulator InittBits: %sn",(Mode == EBM_32 ? "32" : "16"));

}

~CImulInstance(){ this->Image; this->RegSet; this->Stack;

}

//! Выполнение инструкции по текущему EIP bool IntructionEIP();

PCImage Image; // - Образ памяти

PCRegisterSet RegSet; // - Набор регистров

PCStack Stack; // - Стёк

} * PCImulInstance;

Вот такой простой этот класс.

Имеет всего одну функцию... хотя в этой функции и скрывается весь объем работы, необходимой для реализации "нормального" эмулятора =)

Рассмотрим эту функцию подробней.

Данная функция предназначена для выполнения кода по текущему значению регистра EIP.

Прежде чем выполнить команду, её необходимо разобрать (дизассемблировать). Это будет делать HDE. Можно было сделать собсвтенную реалиацию разбора команды, но это лишний код. Тем более, что есть такой мощный и простой проект, как HDE.

Реализация функции IntructionEIP находится в файле imul. cpp

Скелет функции будет выглядеть так:

Листинг : imul. cpp - Скелет IntructionEIP

//! Выполнение инструкции по текущему EIP bool CImulInstance::IntructionEIP(){

BYTE Buffer[16]; // - Буффер инструкции

DWORD EIP = 0; // - Текущий EIP

DWORD NextEIP; // - Адрес следующей инструкции

// - Получаем EIP this->RegSet->getValue(ERN_EIP, &EIP);

// - Считываем инструкцию в буффер this->Image->readMem(EIP, Buffer, sizeof(Buffer));

// - Дизассемблирование hde32s opInfo;

DWORD InstrLen = hde32_disasm(Buffer, &opInfo);

// - Проверка корректности инструкции if(opInfo. flags & F_ERROR){ printf("> Wrong instruction at address 0x%xn", EIP); printf("> ution stopped. n"); printf("n - flags:n"); if(opInfo. flags & F_ERROR_OPCODE) {printf("F_ERROR_OPCODEn"); } if(opInfo. flags & F_ERROR_LENGTH) {printf("F_ERROR_LENGTHn"); } if(opInfo. flags & F_ERROR_LOCK) {printf("F_ERROR_LOCKn"); } if(opInfo. flags & F_ERROR_OPERAND) {printf("F_ERROR_OPERANDn"); } printf("n"); return false;

}

// -

// - Выполнение

// -

DWORD Value = 0; // - Переменная под различные значения

E_REGISTER_NAME Src;

E_REGISTER_NAME Dst; switch(opInfo. opcode){

// - TODO: Обработка команды процессора

// - Обработка исключения неизвестной команды default: printf("> Unknown instruction at address 0x%xn", EIP); printf("> ution stopped. nn"); printf("- Opcode: 0x%xn", opInfo. opcode); printf("- Size: 0x%xn", opInfo. len); printf("n - rel8: 0x%xn", opInfo. rel8); printf("- rel16: 0x%xn", opInfo. rel16); printf("- rel32: 0x%xn", opInfo. rel32); printf("n - imm8: 0x%xn", opInfo. imm8); printf("- imm16: 0x%xn", opInfo. imm16); printf("- imm32: 0x%xn", opInfo. imm32); printf("n - flags:n"); if(opInfo. flags & F_MODRM) {printf("tF_MODRMn"); } if(opInfo. flags & F_SIB) {printf("tF_SIBn"); } if(opInfo. flags & F_IMM8) {printf("tF_IMM8n"); } if(opInfo. flags & F_IMM16) {printf("tF_IMM16n"); } if(opInfo. flags & F_IMM32) {printf("tF_IMM32n"); } if(opInfo. flags & F_DISP8) {printf("tF_DISP8n"); } if(opInfo. flags & F_DISP16) {printf("tF_DISP16n");} if(opInfo. flags & F_DISP32) {printf("tF_DISP32n");} if(opInfo. flags & F_REL8) {printf("tF_REL8n"); } if(opInfo. flags & F_REL16) {printf("tF_REL16n"); } if(opInfo. flags & F_REL32) {printf("tF_REL32n"); } if(opInfo. flags & F_2IMM16) {printf("F_2IMM16n"); } if(opInfo. p_66 != 0) printf("n - p_66: 0x%xn", opInfo. p_66); return false; break;

}

Если инструкция выполнилась успешно, функция должна возвратить true, иначе false.

В случае возрата true эмулятор может продолжать выполнять инструкции. Если же ответ от функции был false, то это сигнал эмулятору прекратить выполнение кода, т. к. возникла ошибка.

Вобщем то, это всё, что нужно для реализации эмулятора. Теперь осталость только его инициализировать, разметить стёк, загрузить в образ код, занести в EIP адрес точки входа и вызывать функцию IntructionEIP, пока она не вернёт false =)

А чтобы значение false появилось как можно позже, нужно добавить в функцию IntructionEIP реализацию обработки как можно большего числа инструкций процессора.

Мы добавим только несколько основных, исключительно для примера.

Но прежде давайте допишем и соберём эмулятор.

Сначала осноной заголовочный файл:

Листинг : main. h - Основной хидер

#ifndef _MAIN_H__INCLUDED_

#define _MAIN_H__INCLUDED_

#include <stdlib. h>

#include <iostream>

#include <windows. h>

#pragma comment (lib, "hde32.lib") using namespace std;

#endif

И теперь собстенно сам код эмулятора для инициализации и загрузки кода.

Эмулятор будет принимать на вход один параметр - путь к файлу с кодом который нужно выполнить.

Разместим реализацию в main. cpp

Листинг : main. cpp - Реализация эмулятора

#include "imul. h"

#include <fstream>

#define MAX_IMAGE_SIZE 0x1000

#define VIRTUAL_IMAGE_BASE 0x00400000 void debugReg(PCRegisterSet RegSet){ if(RegSet == NULL) return;

DWORD eax, ebx, ecx, edx, esp, eip, esi, edi, ebp;

RegSet->getValue(ERN_EAX, &eax);

RegSet->getValue(ERN_EBX, &ebx);

RegSet->getValue(ERN_ECX, &ecx);

RegSet->getValue(ERN_EDX, &edx);

RegSet->getValue(ERN_ESP, &esp);

RegSet->getValue(ERN_EIP, &eip);

RegSet->getValue(ERN_ESI, &esi);

RegSet->getValue(ERN_EDI, &edi);

RegSet->getValue(ERN_EBP, &ebp); printf("nEIP = 0x%x", eip); printf("nEAX = 0x%x / EBX = 0x%x / ECX = 0x%x / EDX = 0x%x", eax, ebx, ecx, edx); printf("nEBP = 0x%x / ESP = 0x%x / ESI = 0x%x / EDI = 0x%xnn", ebp, esp, esi, edi);

} int main(int argc, PCSTR argv[]){

PCSTR FileName = argv[1]; ifstream hFile; hFile. open(FileName, ios::in | ios::binary); if(!hFile. is_open()){ cout << "Can't open file" << endl; return 0;

}

// - Загрузка образа во временный буфер

PBYTE ImageBuff = (PBYTE)calloc(MAX_IMAGE_SIZE, sizeof(char));

DWORD ImageSize = 0; while(!hFile. eof() && ImageSize < MAX_IMAGE_SIZE ){ if(hFile. read((PSTR)&ImageBuff[ImageSize], sizeof(char))) ImageSize++;

} hFile. close(); printf("> Size of code: 0x%x (%d bytes)n", ImageSize, ImageSize);

// - Установка базы образа

DWORD ImageBase = VIRTUAL_IMAGE_BASE;

//

// - Создаем объект эмулятора

//

CImulInstance Imul(EBM_32);

// - Stack: 0x00405000 - 0x00410000

Imul. Stack->initialize(0x00410000, 0x5000);

// - Пишем код из временного буфера в образ памяти эмулятора

Imul. Image->writeMem(ImageBase, ImageBuff, ImageSize);

// - Освобождаем временный буфер free(ImageBuff);

// - Устанавливаем EIP на точку входа

Imul. RegSet->setValue(ERN_EIP, &ImageBase);

// -

// - Выполняем пока выполняется =) while(Imul. IntructionEIP()){ debugReg(Imul. RegSet);

}

// - return 0;

}

Объявление MAX_IMAGE_SIZE — это максимальный размер кода, который можно загрузить в эмулятор.

Объявление VIRTUAL_IMAGE_BASE сождержит адрес, по которму будет расположен эмулируемый код. Также этот адрес будет и точкой входа.

Эти объявления созданы просто для наглядности.

Функиция debugReg будет распечатывать состояние регистров после каждого выполнения инструкции.

Теперь скомпилируем проект (не забыв при этом добавить #include "imul. h" в файлы CImage. cpp, CRegisterSet. cpp и CImulInstance. cpp)

Для тестирования напишем простенькую программу на ассемблере.

Я пользуюсь FASM, поэтому код предосталю на нём.

Листинг : code. asm - Тестовая программа для эмулятора use32 org 0x40000000 start: nop nop nop mov dword eax, 0x1000 push eax call process jmp endprogramm process: nop ret endprogramm:

Компилируем.

Листинг : Компиляция

N:x86ImulSandbox>fasm code. asm flat assembler version 1.67.27 (981047 kilobytes memory)

2 passes, 18 bytes.

Итого тестовая программа получилась на 18 байт =)

Откроем в HIEW чтобы посмотреть на её код.

Впринципе код практически не отличается от исходника.

Стрелками показаны переходы.

Попробуем выполнить.

На скриншоте видно, что эмулятор считывает код из файла, инициализирует стёк, выделяет память по адресу точки входа и начинает выполнение.

Однако сразу после начала выполнения выпадает ошибка Unknown instruction.

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

Что-ж, начнём долгий путь в реализации инструкций процессора =)

Все реализованные опкоды будут перечиляться в файле opcode. h в перечислении E_OPCODE.

Создадим его пока с одним опкодом — EO_NOP.

[code-cpp-opcode. h]

#ifndef _OPCODE_H__INCLUDED_

#define _OPCODE_H__INCLUDED_

// - Перечисление опкодов typedef enum{

EO_NOP = 0x90

} E_OPCODE;

#endif

[/code]

Как известно, 0x90 это значение опкода NOP в 16-ричной системе. Номер опкода также выводится при возникновении ошибки "Unknown instruction".

Заинклудим этот файл в imul. cpp и напишем обработчик инструкции.

Листинг : imul. cpp - Обработчик инструкции NOP case EO_NOP:

EIP++; this->RegSet->setValue(ERN_EIP, &EIP); return true; break;

Инструкция NOP не делает ничего, соотвесвтенно после её выполнения EIP просто увеличивается на размер инструкции.

Размер инструкции NOP равен 1 байту.

Поэтому просто инкрементируем занчение EIP и возращаем true.

Пробуем проэмулировать код заного.

Видно, что три раза отработала функция debugReg, пока эмулятор не наткнулся на неизвестный опкод.

На этот раз опкод равен 0xB8, имеет размер 5 байт и содержит флаг F_IMM32 и значение 0x1000 в поле imm32.

Поле imm32 — это непосредственное 32-битное значение. Флаг F_IMM32 указыват, что инструкция использует поле imm32.

Смотрим в исходик на предмет того, что идёт после NOP'ов.

Это инструкция: mov dword eax, 0x1000

Если заглянуть в справочник инструкций от Intel, то в возможных опкодах инструкции MOV можно найти следующие вариации с опкодом 0xB8:

Значения +rw, +rd и т. д. описаны в таблице 3-1.

Смотрим в неё (приведена часть таблицы)

В случае с +rd нас инстерисует Dword register.

Смотрим на графы Reg Field и Register

Самое верхнее значение в Reg Field = 0, а самый верхний регистр в графе = EAX.

Это значит, что для опкод (0xB8 + 0) работает с регистром EAX, опкод 0xB9 = (0xB8 + 1) с регистром ECX, и наконец, опкод 0xBA (0xB8 + 2) с регистром EDX.

Это разные опкоды, но они находятся в одной группе. Есть базовый опкод 0xB8 для регистров EAX / AX, а следующие за ним — это 0xB8 + ID регистра.

Так что наткнувшись на опкод 0xB8 и заглянув в справочник, мы реализуем сразу группу из 8 опкодов.

Добавим в перечисление опкодов строку "OP_MOV__REG16_REG32___IMM16_IMM32 = 0xB8" и напишем обработчик к ней.

Листинг : imul. cpp - Обработчик OP_MOV__REG16_REG32___IMM16_IMM32

// -

// OP_MOV__REG16_REG32___IMM16_IMM32

// - case (OP_MOV__REG16_REG32___IMM16_IMM32 + 0): case (OP_MOV__REG16_REG32___IMM16_IMM32 + 1): case (OP_MOV__REG16_REG32___IMM16_IMM32 + 2): case (OP_MOV__REG16_REG32___IMM16_IMM32 + 3): case (OP_MOV__REG16_REG32___IMM16_IMM32 + 4): case (OP_MOV__REG16_REG32___IMM16_IMM32 + 5): case (OP_MOV__REG16_REG32___IMM16_IMM32 + 6): case (OP_MOV__REG16_REG32___IMM16_IMM32 + 7): if(opInfo. flags & F_IMM16){

Dst = this->RegSet->getRegByID((opInfo. opcode - OP_MOV__REG16_REG32___IMM16_IMM32), EBM_16);

Value = opInfo. imm16;

}else if (opInfo. flags & F_IMM32){

Dst = this->RegSet->getRegByID((opInfo. opcode - OP_MOV__REG16_REG32___IMM16_IMM32), EBM_32);

Value = opInfo. imm32;

}else return false;

// Устанавливаем значение регистра this->RegSet->setValue(Dst, &Value);

EIP = EIP + InstrLen; this->RegSet->setValue(ERN_EIP, &EIP); return true; break;

Вот таким хитрым кодом мы реализовали группу опкодов "MOV r16/r32, imm16/imm32" (!)

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

Плюс используется только одно объявление, также для компактности.

Компилируем и выполняем.

Видим что на последнем выводе состояние регистров в EAX лежит чисто 0x1000.

Значит, работает =)

Новый неизвестный опкод — 0x50. Это ничто иное, как PUSH EAX.

Смотрим в справочник опкодов.

Это также группа опкодов, в том числе в неё входит и значение для 64-битного регистра.

Т. к. мы занимаемся только 32-битным кодом, реализовыать 64-битный нам не нужно.

Добавляем объяление: OP_PUSH___REG16_REG32 = 0x50 и соответствующий обработчик

Листинг : imul. cpp - Обработчик OP_PUSH___REG16_REG32

// -

// OP_PUSH___REG16_REG32

// - case (OP_PUSH___REG16_REG32 + 0): case (OP_PUSH___REG16_REG32 + 1): case (OP_PUSH___REG16_REG32 + 2): case (OP_PUSH___REG16_REG32 + 3): case (OP_PUSH___REG16_REG32 + 4): case (OP_PUSH___REG16_REG32 + 5): case (OP_PUSH___REG16_REG32 + 6): case (OP_PUSH___REG16_REG32 + 7):

Src = this->RegSet->getRegByID((opInfo. opcode - OP_PUSH___REG16_REG32), (opInfo. p_66 == 0x66 ? EBM_16 : EBM_32)); this->Stack->pushReg(Src);

EIP = EIP + InstrLen; this->RegSet->setValue(ERN_EIP, &EIP); break;

Размер операндов этой инструкции определяется наличием префикса определения размера операнда 0x66.

Проверяем.

При первом обращении к памяти произошло выделение блока под стёк (напомню, что при инициализации CStack память не выделяется, а только устаналиает значение ESP)

После выполнения инструкци значение регистра ESP уменьшилось на 4, что равно размеру регистра EAX.

По аналогичной схеме реализуем оставшиеся инструкии (call, jmp, pop, ret).

И проверяем их работу.

Листинг : imul. cpp - OP_CALL___REL16_REL32

// -

// OP_CALL___REL16_REL32

// - case OP_CALL___REL16_REL32:

EIP += InstrLen; this->Stack->push(&EIP, this->RegSet->getRegisterSize(ERN_EIP)); if(opInfo. flags & F_REL16) EIP = EIP + opInfo. rel16; else if(opInfo. flags & F_REL32) EIP = EIP + opInfo. rel32; this->RegSet->setValue(ERN_EIP, &EIP); return true; break;

Листинг : imul. cpp - OP_POP___REG16_REG32

// -

// OP_POP___REG16_REG32

// - case (OP_POP___REG16_REG32 + 0): case (OP_POP___REG16_REG32 + 1): case (OP_POP___REG16_REG32 + 2): case (OP_POP___REG16_REG32 + 3): case (OP_POP___REG16_REG32 + 4): case (OP_POP___REG16_REG32 + 5): case (OP_POP___REG16_REG32 + 6): case (OP_POP___REG16_REG32 + 7):

Dst = this->RegSet->getRegByID((opInfo. opcode - OP_POP___REG16_REG32), (opInfo. p_66 == 0x66 ? EBM_16 : EBM_32)); this->Stack->popReg(Dst);

EIP = EIP + InstrLen; this->RegSet->setValue(ERN_EIP, &EIP); break;

Листинг : imul. cpp - OP_RETN / OP_RETF / OP_RET___IMM16

// -

// OP_RETN / OP_RETF / OP_RET___IMM16

// - case OP_RETN: case OP_RETF: this->Stack->pop(&EIP, this->RegSet->getRegisterSize(ERN_EIP)); this->RegSet->setValue(ERN_EIP, &EIP); return true; break; case OP_RET___IMM16: this->Stack->pop(&EIP, sizeof(EIP)); this->RegSet->getValue(ERN_ESP, &Value);

Value -= opInfo. imm16; this->RegSet->setValue(ERN_ESP, &Value); this->RegSet->setValue(ERN_EIP, &EIP); return true; break;

Листинг : imul. cpp - OP_JMP___REL8 / OP_JMP___REL16_REL32

// -

// OP_JMP___REL8 / OP_JMP___REL16_REL32

// - case OP_JMP___REL8:

EIP = EIP + InstrLen + opInfo. rel8; this->RegSet->setValue(ERN_EIP, &EIP); return true; break; case OP_JMP___REL16_REL32:

EIP = EIP + InstrLen; if(opInfo. flags & F_REL16) EIP += opInfo. rel16; else if(opInfo. flags & F_REL32) EIP += opInfo. rel32; this->RegSet->setValue(ERN_EIP, &EIP); return true; break;

Теперь, если запустить, программа должна будет упасть с неизвестной инструкцией по адресу 0x00400012, т. к. там уже нет кода, а всё прострнаство забито нулями.

Проверяем.

Всё верно. По логам состояний регистров видно, что программа отэмулировалась корректно!

Эмулятор готов, осталось только добавлять поддерживаемые инструкции.

Подводя итоги

В этой статье мы познакомились с устройством и принципом работы эмулятора, а также проверили его работу на практике.

Конечно, эмулятор, реализоанный в статье, очень простой, но он может послужить базой для дальнейших исследований.

Его можно прератить как и в эмулятор компьютера, так и в эмулятор какой-либо операционной системы, работающей на архитектуре IA-32.

Например можно написать разборщик PE формата, подгрузить им в эмулятор исполняемый файл, подгрузить системные библиотеки, реализовать эмуляцию функций ядра Windows, и мы получим эмулятор исполняемых файлов для Windows =)

Можно реализовать 16-битный режим прерывания, загрузить в память образ BIOS'a, и получится среда, достаточная для запуска DOS'a.

А можно отображать в память только код, который необходимо выполнить, и использовать это, например, для расскриптовки вирусов или снятия обфускации с кода.

Да много всего можно... Эмулятор — это средство, найти применение которому можно практически везде, особенно в областях, связанных с исследованием / реверсингом кода.


Карта сайта


Информационный сайт Webavtocat.ru