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.
Ненадо требовать от контекстного отладчика сверхъестественного.
Я писал его лишь из-за интереса к внутреннему устройства IDA. Если чего-то нехватает,
добавьте сами - исходники есть.
Хотелось бы сказать большое спасибо Крису Касперски, за его отличные книги и статьи.
|