Написание плагина — эмулирующего отладчика для дизассемблера IDA — Архив WASM.RU

Все статьи

Написание плагина — эмулирующего отладчика для дизассемблера IDA — Архив WASM.RU

Каждой твари по паре
Каждой виртуальной машине
– по виртуальному процессору

 

1. Вступление
2. Общее построение
3. Немного об виртуальной памяти IDA
4. Как эмулировать?
5. Об обработчике команд
6. Послесловие
7. Литература

1. Вступление

Как-то в Интернете, наткнулся я на отрывок книги Криса Касперски. Вот цитата:

" !IDA DEBUGGER!

В чем новизна и удобство идеи? А в том, что можно сделать не полный, а _контекстный_ отладчик! Что это такое? Обычный дебаpег отлаживает всю программу целиком. Это хоpошо, но чаще всего нас интересуют только выбранные фрагменты. В IDA можно подогнать куpсоp к нужному месту, задать начальные значения pегистpов и пусть на выполнение эмулятоp. Такой подход пpежде всего упоpщает задачу, т.к. тут скоpость не тpубуется. А как удобно! Можно видеть pаботу кода в динамике! И не ломать голову какие будут значения pегистpов\флагов на выходе из такого и такого фpагмента и куда метнеться условный пеpеход. Можно просто прогнать чисто локальный кусок с любой его точки."

Я понимаю, что отладчик для IDA, начиная с версии 4.5 существует, но во-первых не у всех она есть, во-вторых создать отладчик своими руками очень даже интересное дело, поможет поближе ознакомится с устройством IDA и её плагинов. Информация по написанию плагинов для дизассемблера есть в примерах SDK. Сразу надо сказать, что это будет контекстный эмулирующий отладчик 32 разрядного режима. Он будет тесно взаимодействовать с пользователем по части работы. Ну например если эмуляция какой-то команды отсутствует, то предлагается, что пользователь сам проэмулирует её и скорректирует значения регистров, стека.

Предлагаю свой проект, как один из вариантов. Писал я его из-за желания поглубже узнать IDA. Если его довести до ума, то может получится очень полезный program, который может помочь при ручной распаковке и анализе программ.

2. Общее построение

Лучше всего показать на рисунке. В принципе, подойдет не только для отладчика, но и для любого другого плагина.

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

Управление ведется с консоли дизассемблера-как вызов функций. Также можно организовать "горячие" клавиши. Вывод информации на экран - посредством окна сообщений и информативных окошек. Самое интересное - эмуляция команд. После подвода курсора к нужному адресу и ввода начальных значений в регистры, начинается работа отладчика.

В качестве подопытной была взята IDA 4.5 internal (лежала на www.crackbest.com). Также необходим SDK. Без SDK в создании плагинов делать нечего. Команды управления отладчиком были введены при помощи IDC скрипта - "deb_comm.idc". Этот скрипт компилируется во время инициализации плагина (скрипт должен лежать в директории idc дизассемблера), если потерпит неудачу, то можно попытаться загрузить скрипт вручную из меню File\IDC file. Именно в этом скрипте находятся вызовы функций отладчика, можете повесить на функции "горячие клавиши", и добавить новые. Здесь показан формат вызова команды отладчика:

#define MY_FUNC_ID xx
static MY_NEW_COMMAND()
{
	RunPlugin("IDA_deb",MY_FUNC_ID);
}

Которую после добавления в "deb_comm.idc" можно будет вызывать прямо с консоли, например так: MY_NEW_COMMAND(); . Только незабудьте перед этим вставить в функцию run() плагина обработчик этой команды, которая будет обрабатываться, если аргумент будет равен MY_FUNC_ID.

3. Немного об виртуальной памяти IDA

Для написания отладчика надо знать, об виртуальной памяти. Подробнее смотрите "Образ мышления IDA" Криса Касперски. Прошу простить меня за такое представление виртуального адресного пространства IDA и её базы данных, это упрощено, но отладчик работает именно с данными в виртуальной памяти, в которой лежит база данных. На рисунке показано соотношения между адресами (названия взяты от самого создателя IDA):

Линейный адрес (тип ea_t) - это адрес в виртуальном пространстве IDA Базовый адрес сегмента - это линейный адрес начала сегмента в виртуальном пространстве IDA Виртуальный адрес - это смещение между линейным адресом и базовым адресом сегмента: Виртуальный адрес=Линейный адрес - Базовый адрес сегмента Как вы уже , наверно, знаете, IDA работает со своим виртуальным пространством, а для дизассемблированной программы создаются сегменты со своими атрибутами (база, селектор разрядность и.т.п.), и с ним же будет работать и наш отладчик, через функции get_byte(..), get_word(), get_long, patch_byte, patch_word, patch_long - все они определены в BYTES.HPP, и там ещё много интересного есть. Линейный адрес вычисляется как

(Base<<4+смещение), где Base - база сегмента

4. Как эмулировать?

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

Like the Intel486 CPU, integer instructions traverse a 5 stage pipe-line. The pipe-line stages are as follows:

PF Prefetch
D1 Instruction Decode
D2 Address Generate
EX Execute - ALU and Cache Access
WB Writeback

Т.е -предвыборка команды -декодирование команды -генерация адреса, для определения адреса операндов в памяти -выполнение опреаций с помощью АЛУ запись результата

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

Чтобы приступить к непосредственной эмуляции, давайте разберемся, как IDA хранит инструкции. Для этого имеются несколько структур, они описаны в файле INCLUDE\UA.hpp. Этот файл ОЧЕНЬ важен при написании эмулятора, т.к. в нем описывается формат данных команды, её операндов их типы.


// Хранит информацию о команде

idaman insn_t ida_export_data cmd;      // текущая инструкция


// Класс описывающий инструкцию (сокращено)
class insn_t
{
public:

  ushort itype;	// Код инструкции (Инициализируется в 0 ядром
// IDA  все инструкции содержатся в директории
// INCLUDE\allins.hpp 

  ulong cs;		// База сегмента, в котором находится
// инструкция (Инициализируется ядром IDA
  ulong ip;		// Виртуальный адрес инструкции (адрес ВНУТРИ 
// сегмента) блин, от этих виртуальных адресов 
// уже сам начинаешь виртуалиться //(Устанавливается ядром
// ИДА, а кем-же ещё)

  ea_t ea;		// Линейный адрес инструкции

  ushort size;	// Размер инструкции в байтах


// Сведения об операндах инструкции 
#define UA_MAXOP 6
  op_t Operands[UA_MAXOP];	// Массив содержит структуру описывающую каждый 
// операнд в команде. К нему удобно обратится
#define Op1 Operands[0]		// ,например, так: op_t x= cmd.Op1-получаем ин-
#define Op2 Operands[1]		// формацию об первом операнде (странно, что не
#define Op3 Operands[2]		// нулевой)
#define Op4 Operands[3]
#define Op5 Operands[4]
#define Op6 Operands[5]
};

// Класс - тип операнда, (показано неполностью)
class op_t
{
public:
  char n;		// Порядковый номер операнда

// Структура описывающая тип операнда (очень интересное поле)
  optype_t	    type;


// Тип значения операнда

  char          dtyp;

#define dt_byte         0       // 8 bit
#define dt_word         1       // 16 bit
#define dt_dword        2       // 32 bit
#define dt_float        3       // 4 byte
#define dt_double       4       // 8 byte
#define dt_tbyte        5       // variable size (ph.tbyte_size)
#define dt_packreal     6       // packed real format for mc68040
#define dt_qword        7       // 64 bit
#define dt_byte16       8       // 128 bit
#define dt_code         9       // ptr to code (not used?)
#define dt_void         10      // none
#define dt_fword        11      // 48 bit
#define dt_bitfild      12      // bit field (mc680x0)
#define dt_string       13      // pointer to asciiz string
#define dt_unicode      14      // pointer to unicode string
};

Ну и, наконец, описание типа операнда
typedef uchar optype_t;
const optype_t     // 		                           Поле в op_t
  o_void     =  0, // нет операнда                        ----------
  o_reg      =  1, // Основной регистр (al,ax,es,ds...)    reg
  o_mem      =  2, // Прямое обращение к памяти            addr
  o_phrase   =  3, // Memory Ref [Base Reg + Index Reg]    phrase
  o_displ    =  4, // Memory Reg [Base Reg + Index Reg + Displacement] phrase+addr
  o_imm      =  5, // Непосредственное значение            value
  o_far      =  6, // Непосредственный дальний адрес       addr
  o_near     =  7, // Непосредственный ближний адрес       addr
  o_idpspec0 =  8, // IDP specific type
  o_idpspec1 =  9, // IDP specific type
  o_idpspec2 = 10, // IDP specific type
  o_idpspec3 = 11, // IDP specific type
  o_idpspec4 = 12, // IDP specific type
  o_idpspec5 = 13, // IDP specific type
  o_last     = 14; // first unused type

  char          specflag1;
  char          specflag2;
  char          specflag3;
  char          specflag4;
}

В структуре optype_t есть следующие поля:

char specflag1;
char specflag2;
char specflag3;
char specflag4;

Эти флаги заполняются ядром ИДА (точнее процессорным модулем). Как заполняются не сказано, но, проведя эксперименты, с различными инструкциями и способами адресации, удалось выяснить вот что:

Флаг specflag1 и все остальные равен нулю если:
1. Имеется прямая адресация (OpX.type=o_mem). Т.е. например mov eax,[401000]
2. Косвенная адресация (OpX.type=o_phrase) mov eax,[eax]
3. Косвенная адресация со сдвигом (OpX.type=o_displ) mov eax,[eax+402031]
Если specflag1==1, то в specflag2 содержится информация об составляющих операнда (например при индексной адресации с масштабированием) Если OpX.type=o_phrase, то specflag2 будет расшифровываться так:

7
6
5
4
3
2
1
0
Коэффициент масштабирования
Индексный регистр
Базовый регистр


Кодировка регистров в точности, как принята у Intel:

EAX
0
ECX
1
EDX
2
EBX
3
ESP
4
EBP
5
ESI
6
EDI
7

Коэффициент масштабирования определяет степень двойки, на которую нужно умножить значение индексного регистра:

 
Коэф.Умножения
00
1
01
2
10
4
11
8

Например :


lea     eax, [ebx+ecx*4]

Op2 type is: 3
Op2 dtyp is: 0
Op2 value is: 0
Op2 reg is: 4
Op2 phrase is: 4
Op2 address is: 0
Op2 offb is: 0
Op2 offo is: 0
Op2 specflag1 is: 1
Op2 specflag2 is: 9B
Op1 specflag3 is: 0

Op2 specflag2 is: 9B  10001011
10-4
011-ebx
001-ecx

Если OpX.type=o_mem, то specflag2 будет расшифровываться так:

7
6
5
4
3
2
1
0
         
1
0
1
Коэффициент масштабирования
Регистр
Точно не выяснил, но всегда равно константе

Если OpX.type=o_displ, то specflag2 будет расшифровываться так:

7
6
5
4
3
2
1
0
Коэффициент масштабирования
Индексный регистр
Базовый регистр

Также в этом случае, в поле OpX.addr, будет находится значение смещения

Однако здесь есть одно исключение для обращения к памяти вида: [esp+xxx] В этом случае specflag1=1; specflag2=0x24

Разберемся с сегментами IDA. Сетменты в IDA-один из её краеугольных камней. И надо ознакомится со структурами, которые их описывают: Нам они понадобятся для создания сегмента стека отладчика. Без него работать, мягко говоря некорректно. Каждый сегмент имеет базовый сегментный адрес, который определяет положение сегмента в виртуальной памяти IDA. В поле Base может находится базовый адрес, а может находится индекс селектора. Селектор содержит 32-битный базовый адрес сегмента в виртуальном пространстве IDA. Подробнее смотрите "Образ мышления IDA" Криса Касперски.

Данные и функции работающие с сегментами содержатся в заголовочном файле SEGMENT.HPP . К сегментам можно обращаться разными способами. Тип структуры, описывающею сегмент segment_t. В ней содержится вся информация о сегменте: База, начальный адрес, конечный адрес, имя сегмента, выравнивание сегмента, разрядность 16 или 32 бит, и.т.д.


class segment_t : public area_t
{
public:

	long name;	    // имя сегмента
	long sclass;	// класс сегмента
	long orgbase;	// это поле зависит от IDP 
	uchar align;
	uchar comb;	    // Код комбинирования сегмента с другими
	uchar perm;	    // "Разрешение" сегмента (EXE, READ, WRITE) 
	uchar use32;	// разрядность сегмента
	ushort flags;	// флаги сегмента
	sel_t sel;	    // селектор сегмента
	uchar type;	    // Тип сегмента
}; 

Вот как примерно выглядит создание сегмента стека для отладчика. Подробное описание функций смотрите в SEGMENT.HPP


// Находим пустой селектор и проецируем на виртуальную память
// N - селектор для сегмента
	sel_t N=allocate_selector(0);
// Создаем сегмент 
	if(add_segm(N,0xFFFF0000,0xFFFFFFFE,"STCK","STACK"))
	{
		segment_t DEBUGGER_STACK_SEGMENT, *pDSS;
		pDSS = &DEBUGGER_STACK_SEGMENT;

		pDSS = get_segm_by_sel(N);
		set_segm_addressing(pDSS,1);

		msg("Debugger stack created\n");
	}
	else warning("Cant create segment\n");

Как видите, ничего сложного нет.

5. Об обработчике команд

В эмуляторе всю работу по исполнению инструкций выполняют обработчики команд. Было решено в обработчик передавать тип соотношения операндов (например: один операнд - регистр, один операнд в памяти, один регистр - другой в памяти). В исходниках это выполняет функция get_operand_info(OPER &), которая заполняет структуру OPER. Также она заполняет поля структуры: вычисленными значениями операндов (например, при косвенной адресации). Сам обработчик команды знает о том какие операнды и какие сочетания операндов возможны и в зависимости от этого выполняется. Покажу на примере инструкции push:


//  push instruction
BYTE emPUSH(OPER &O){
//  Выводить о работе команды, как вы понимаете не обязательно.
//  Просто удобно при отладке обработчика
	msg("This is a PUSH cmd emulation\n");

//  В стеке есть место??
	if(!checkESP(-4)) return false;

//  Проверка, если один из операндов в памяти, на принадлежность к 
//  несуществующему адресу.
	if(!checkEA(O))   return false;

	EIP2NextCmd(O);
//  Операнд один - непосредственное значение 32бит
	if(O.rel==i32) {
		*pemESP-=4;			//Уменьшили ESP
		return (WriteVMDWORD(*pemESP,O.op0_i32));//Пишем в вирт. память
	}

//  Операнд один - непосредственное значение 16бит
	if(O.rel==i16) {
		*pemESP-=2;
		return (WriteVMWORD(*pemESP,O.op0_i16));
	}
// Операнд один - непосредственное значение 8бит
	if(O.rel==i8) {
		*pemESP-=2;
		return (WriteVMWORD(*pemESP,(WORD)O.op0_i8));
	}
// Здесь должна быть проверка на опернды - регистры и операнды, находящиеся в // памяти
//    if(O.rel==r32). . .
//    if(O.rel==r16). . .

//    if(O.rel==m32). . .
//    if(O.rel==m16). . .

// Если мы здесь, то операнды неопознаны
	warning("UNKNOWN OPERAND IN PUSH CMD");
	return INS_BAD_OP;
}

Это одна из "частных" инструкций и для других писать обработчик намного проще. Например эмуляция инструкции cmp


// 	cmp
void __declspec(naked) emCMP8() {_asm {cmp al,bl
				       retn}
				}
void __declspec(naked) emCMP16(){_asm {cmp ax,bx
				       retn}
				}
void __declspec(naked) emCMP32(){_asm {cmp eax,ebx
				       retn}
				}

///////////////////////////////////////////////////////
// cmp
///////////////////////////////////////////////////////
bool emCMP(OPER &O){
	if(!checkEA(O))   return false;

	EIP2NextCmd(O);
	return two_op_handle(O, emCMP8, emCMP16, emCMP32);
}

Вот и весь обработчик инструкции. Вся работа будет происходить в two_op_handle, которая и учтет типы операдов их типы, и воздействие на флаги.
Перед написанием обработчика инструкции, неплохо бы узнать, что возвращает структура cmd , так как может отличаться для разных инструкций. Например для инструкции lea eax,[ebx] cmd.Op1.dtyp = dt_dword, а cmd.Op1.dtyp = dt_byte. Поэтому ,возможно, придется добавить обработку ситуации в функцию get_operand_info.

При написании обработчиков ОЧЕНЬ неплохо помогает справочник из книги Юрова "Ассемблер", там расписаны все типы операндов команд и алгоритм их работы.

6. Послесловие

На данный момент поддерживается:
1) Точки останова BPX,
2) BPM_R, BPM_W, BPM_RW
3) Эмулировано 122 инструкции x86 (кроме 3-х операндных imul, shld, shrd, enter, leave и др., их можете реализовать сами.)

Команды управления отладчиком можно уточнить как help(); из консоли IDA.

Надо добавить: 1) Создать нормальный показ регистров в отладчике в виде отдельного диалогового окна
2) При желании можно добавить эмуляцию FPU, MMX
Самое серьезное, что непонятно, как эмулировать вызовы WinAPI функций. Похоже, что неполучится, но в принципе в контекстном отладчике это и ненужно, хотя было бы неплохо.

В приложении содержится исходный код отладчика, а также скомпилированная версия плагина для IDA 4.5 Похоже, что файл plugins.cfg необязателен (ИДА 4.5), т.к. и без него плагин вызывался нормально. Подозреваю, что ИДА сканирует директорию с плагинами и узнает параметры через структуру PLUGIN. Если неполучится, то добавьте следующую строку:
CDEx86 CDEx86 F11 0
, также можно добавить горячую клавишу к плагину.

Первые шаги по работе:
1) Скопируйте CDEx86.plw в папку \plugins дизассемблера.
2) Запустите дизассемблер
3) Приведите курсор к куску кода, который хотите изучить, и начинаите трассировку ( по умолчанию клавиша F11)
4) Значения регистров можно посмотреть вызвав r(); из консоли (Shift+F2). В следующей версии ,возможно, будет отдельное окно с регистрами.
5) Если хотите автоматическую трассировку, то подведите курсор к месту, где должна остановится трассировка и введите из консоли точку останова.
bpx();
at();

Точно так устанавливаются виртуальные бряки на чтение\запись в память bpm();
Поработав со своим творением, у меня появились некоторые рекомендации по работе с ним:
1) Отладчик предполагает хорошие навыки работой с IDA.
2) Не надо лезть в вызов WinAPI функции, т.к. ее кода нет в дизассемблере и соответственно нельзя ее трассировать. Лучше всего перепрыгнуть её ( cheip(); ), сбалансировав стек.
3) Отладчик получает инструкцию по адресу курсора IDA. Поэтому если вы прыгнете на середину команды, то получите сообщение Not code. Stay at code first. В этом случае надо сперва превратить в Unknown, а затем превратить в Code.
4) Необходимо создать окно, в которое выводить информацию. В первую очередь - значения регистров Т.к. неудобно каждый раз вызывать команду r();

Ненадо требовать от контекстного отладчика сверхъестественного. Я писал его лишь из-за интереса к внутреннему устройства IDA. Если чего-то нехватает, добавьте сами - исходники есть.

Во время работы с плагином, появилась идея использовать его для ручной распаковки програм:
Алгоритм примерно выглядит следующим образом:
1. Загружаете файл как бинарный
2. Применяете PE_scripts by Atli Gudmundsson (есть на wasm.ru)
3. Пробуете трассировать его с точки входа
4. Сохраняете на диск заголовок и секции при помощи скрипта pe_write.
(А еще лучше написать свой. Т.к. больше шансов получить работоспособный файл)

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

7. Литература

1. pilorama.com.ru
2. Юров В., Хорошенко С. Ассемблер: учебный курс
3. Зубков С.В. Ассемблер язык неограниченных возможностей

Жду поправок и более крутых решений

Исходник к статье.

2002-2013 (c) wasm.ru