Pull to refresh

Добавление нового семейства процессоров в IDA pro

Reading time 8 min
Views 15K
Пришлось не так давно потрошить прошивку от M16C (Mitsubishi/Renesas). С удивлением обнаружил, что оказывается IDA v6.1.xxx не «держит» данное семейство контроллеров, увы. Впрочем, SDK есть в наличии, значит, не страшно – будем исправлять ситуацию. Как показала практика, ничего сверх сложного в написании своего модуля нет (не rocket science, чай).

Отказ от ответственности


Я не являюсь специалистом по IDA pro и написанию модулей для нее. Поскольку задача состояла в анализе прошивки, модуль писался в спешке (на коленке), часть кода была надергана из SDK, без понимания того как это работает (и нужно ли сие вообще). В остальной части кода удалось разобраться и она была «творчески» переосмыслена.

У меня не было тестов и времени на их написание. Корректность работы проверялась по дизассемблированному листингу прошивки в IDE от Renesas. Таким образом, возможны (и наверняка есть, хотя мне в процессе работы и не попадались) ошибки. Если у кого-то возникнет желание написать тесты или доработать модуль, буду рад.

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

Исходники и сборка плагина


Исходники лежат здесь.
Так как я не особенно уважаю «Студию» (MSVC++), для сборки использовал MinGW и нагородил свой тулчейн. Взять его можно здесь. Данный тулчейн самодостаточен – содержит компилятор и инструментарий для сборки.

Подготовка к сборке состоит в следующем. Распаковываем IDA_Plugins.7z куда‑нибудь, клонируем репозитарий с GitHub'а и директорию m16c_xx копируем в корень директории IDA_Plugins, после чего запускаем build.cmd.

Введение


Каждый процессорный модуль, фактически являющийся обычной dll'кой (с небольшим отличием – немного изменен DOS заголовок), должен экспортировать структуру с именем LPH, с типом processor_t. В ней хранятся указатели на на ключевые функции и структуры модуля.
Из всего многообразия полей этой структуры нас, прежде всего, будут интересовать следующие указатели на функции:

processor_t LPH = {
            …
    ana,        // analyze an instruction and fill the 'cmd' structure
    emu,        // emulate an instruction
    out,        // generate a text representation of an instruction
    outop,      // generate a text representation of an operand
            …
}

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

int idaapi ana(void); //analyze one instruction and return the
                      // instruction length

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

Функция emu() предназначена для эмуляции инструкции, ее сигнатура:

int idaapi emu(void); //emulate one instruction

Задача этой функции заключается в:
  • создании перекрестных ссылок из (к) этой инструкции, как для данных так и для кода;
  • создании стековых переменных (к сожалению, я пока не разбирался, как это работает)
  • чем-то там еще.

Функция out() создает и выводит текстовое представление ассемблерной инструкции, ее сигнатура:

void idaapi out(void); //output a single disassembled instruction

Функция outop() создает и выводит текстовое представление операндов ассемблерной инструкции, ее сигнатура:

bool idaapi outop(op_t &x); //output an operand of disassembled
                            // instruction

Анализ инструкций


Анализом исходных данных занимается функция ana(), которую нам предстоит реализовать. Ее задача, читая последовательно байты прошивки, определять инструкции, их операнды и длины инструкций.

После распознавания инструкции и ее параметров, заполняем поля глобальной переменной cmd, которая имеет тип insn_t:

class insn_t { 
public: 
  ea_t cs; // Current segment base paragraph. Set by kernel 
  ea_t ip; // Virtual address of instruction (within segment).
           // Set by kernel 

  ea_t ea; // Linear address of the instruction. Set by kernel 
  uint16 itype; // instruction enum value (not opcode!).
                // Proc sets this in ana 

  uint16 size; // Size of instruction in bytes. Proc sets this in ana 
  union { 
    // processor dependent field. Proc may set this 
    uint16 auxpref; 
    struct { 
      uchar low; 
      uchar high; 
    } auxpref_chars;
  };
  char segpref;      // processor dependent field. Proc may set this 
  char insnpref;     // processor dependent field. Proc may set this 
  op_t Operands[6];  // instruction operand info. Proc sets this in ana 
  char flags;        // instruction flags. Proc may set this 
}; 

Как видно из описания, у инструкции может быть до 6 операндов (что для нас «за глаза» – в нашем случае операции содержат максимум 3 операнда).

До сих пор я писал модуль только для RISC контроллера – там все достаточно просто. Накладываем маску на поле КОП (Код ОПерации) инструкции, в ветвях оператора switch проверяем дополнительные условия и, собственно, все (примерно так анализируется код для контроллера PIC от Microchip'а, пример можно глянуть в SDK). Преимущества RISC здесь очевидно – сокращенный набор команд и их одинаковая длина. Увы, для M16C я насчитал больше 300 уникальных команд (все зависит от точки зрения – я учитывал уникальные КОПы), да еще и команды переменной длины (CISC). Таким образом оператор switch нам не подходит по причине громоздкости и чреватости трудно уловимыми ошибками.

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

Городить автомат в ручную желания не было, зато был положительный опыт работы с Ragel – компилятором конечных автоматов со специального языка описания FSM в код на языках C, C++, C#, Objective-C, D, Java, OCaml, Go или Ruby. Он не создает никаких внешних зависимостей, только самодостаточный исходник на выбранном языке программирования.

Среди прочего, Ragel интересен тем, что позволяет разбирать входные данные «на лету». Т.е. нет необходимости формировать и передавать парсеру для анализа большой буфер, гарантировано содержащий команду, а можно ограничиться небольшими объемами данных, вплоть до одного байта, с сохранением состояния между вызовами. Нам это идеально подходит!

В результате получился некий DSL для разбора команд процессора.

DSL для разбора команд


Преимущество данного DSL перед switch, прежде всего в его линейности. Не надо скакать по веткам оператора для понимания того как это работает или модификации поведения. Вся обработка КОПов и операндов для конкретной команды сосредоточена в одном месте. Пример:
#//   0x00             0000 0000                            BRK
  M16C_BRK = 0x00 @ {
    cmd.itype = M16C_xx_BRK;
    cmd.Op1.type = o_void;
    cmd.Op1.dtyp = dt_void;
  };

Или, как вариант команда с операндами:

#//   0x01..0x03       0000 00DS                            MOV.B:S        R0L, DEST
  M16C_MOV_B_S_R0L_DEST = (0x01..0x03) @ {
      cmd.itype = M16C_xx_MOV_B_S_R0L_DEST;
      MakeSrcDest8(SRC_DEST_R0L, cmd.Op1);
      switch(*p & 0x03) {
        case 0x01:
          MakeSrcDest8(SRC_DEST_DSP_8_SB_, cmd.Op2);
          break;
        case 0x02:
          MakeSrcDest8(SRC_DEST_DSP_8_FB_, cmd.Op2);
          break;
        default:
          MakeSrcDest8(SRC_DEST_ABS16, cmd.Op2);
          break;
      }
    };

Как мне кажется, вполне наглядно и удобно. В cmd.itype помещается одно из значений перечисления enum opcodes (файл ins.hpp), которое в дальнейшем будет указывать на текстовое представление инструкции, количество операндов и описывать взаимодействие инструкции с операндами. А так же заполняются поля операндов.

Эмуляция выполнения инструкции


Сами инструкции, число операндов и воздействие на операнды описывается в массиве instruc_t instructions[ ] (ins.cpp). В принципе, формат записей прост и интуитивно понятен:

instruc_t instructions[ ] = {
    ...
{ "ADC.B",        CF_USE1|CF_CHG2                 },
{ "ADC.W",        CF_USE1|CF_CHG2                 },
{ "ADC.B",        CF_USE1|CF_CHG2                 },
{ "ADC.W",        CF_USE1|CF_CHG2                 },
{ "ADCF.B",       CF_CHG1                         },
{ "ADCF.W",       CF_CHG1                         },
{ "ADD.B:G",      CF_USE1|CF_CHG2                 },
{ "ADD.W:G",      CF_USE1|CF_CHG2                 },
{ "ADD.B:Q",      CF_USE1|CF_CHG2                 },
    ... 
};

Видно, что у инструкции "ADC.B" есть 2 операнда, первый просто используется, а второй изменяется в процессе исполнения инструкции. Что логично: ADC это сложение с переносом (ADdition with Carry) и выглядит операция так:

[ Syntax ]
  ADC.size src,dest 
        ^--- B, W

[ Operation ]
  dest <- src + dest + C

Далее эмулируется выполнение самой инструкции в функции emu().

int emu( ) {
  unsigned long feature = cmd.get_canon_feature( );

  if( feature & CF_USE1 ) TouchArg( cmd.Op1, 1 );
  if( feature & CF_USE2 ) TouchArg( cmd.Op2, 1 );

  if( feature & CF_CHG1 ) TouchArg( cmd.Op1, 0 );
  if( feature & CF_CHG2 ) TouchArg( cmd.Op2, 0 );

  if( !( feature & CF_STOP ) )
    ua_add_cref( 0, cmd.ea + cmd.size, fl_F);

  return 1;
}

Как видно, при наличии аргумента, мы преобразуем его к удобоваримому виду в функции TouchArg(). Выглядит эта функция следующим образом:

void TouchArg( op_t &x, int isload ) {
  switch ( x.type ) {
    case o_near: {
        cref_t ftype = fl_JN;
        ea_t ea = toEA(cmd.cs, x.addr);
        if ( InstrIsSet(cmd.itype, CF_CALL) )
        {
          if ( !func_does_return(ea) )
            flow = false;
          ftype = fl_CN;
        }
        ua_add_cref(x.offb, ea, ftype);
      }
      break;
    case o_imm:
      if ( !isload ) break;
      op_num(cmd.ea, x.n);
      if ( isOff(uFlag, x.n) )
        ua_add_off_drefs2(x, dr_O, OOF_SIGNED);
      break;
    case o_displ:
      if(x.dtyp == dt_byte)
    	  op_dec(cmd.ea, x.n);
      break;
    case o_mem: {
        ea_t ea = toEA( dataSeg( ),x.addr );
        ua_dodata2( x.offb, ea, x.dtyp );
        if ( !isload )
          doVar( ea );
        ua_add_dref( x.offb, ea, isload ? dr_R : dr_W );
      }
      break;
    default:
      break;
  }
}

Взависимости от типа операнда соответствующим образом заполняем поля структуры op_t («декодируем» операнд).

Вывод текстового представления инструкции


За данное действие отвечает функция out(). Выглядит она так:

void out() {
	char str[MAXSTR];  //MAXSTR is an IDA define from pro.h
	init_output_buffer(str, sizeof(str));

	OutMnem(12);       //first we output the mnemonic

	if( cmd.Op1.type != o_void )  //then there is an argument to print
		out_one_operand( 0 );

	if( cmd.Op2.type != o_void ) {  //then there is an argument to print
		out_symbol(',');
		out_symbol(' ');
		out_one_operand( 1 );
	}

	if( cmd.Op3.type != o_void ) {  //then there is an argument to print
		out_symbol(',');
		out_symbol(' ');
		out_one_operand( 2 );
	}

	term_output_buffer();
	gl_comm = 1;      //we want comments!
	MakeLine(str);    //output the line with default indentation
}

Выводим текстовое представление инструкции и, если есть, операнды с минимальным форматированием.

Вывод текстового представления операндов


Здесь код поинтереснее, но тоже все достаточно просто:

bool idaapi outop(op_t &x) {
	ea_t ea;

	switch (x.type) {
		case o_void:
			return 0;
		case o_imm:
			OutValue(x, OOF_NUMBER | OOF_SIGNED | OOFW_IMM);
			break;
		case o_displ:
			{  //then there is an argument to print
				OutValue(x, OOF_NUMBER | OOF_SIGNED | OOFW_IMM);
				switch (x.dtyp) {
					case dt_byte:
						break;
					case dt_word:
						out_symbol(':');
						out_symbol('8');
						break;
					case dt_dword:
						out_symbol(':');
						out_symbol('1');
						out_symbol('6');
						break;
					default:
						ea = toEA(cmd.cs, x.addr);
						if (!out_name_expr(x, ea, x.addr))
							out_bad_address(x.addr);
						break;
				}
				out_symbol('[');
				out_register(M16C_xx_RegNames[x.reg]);
				out_symbol(']');
			}
			break;
		case o_phrase:
			out_symbol('[');
			out_register(M16C_xx_RegNames[x.reg]);
			out_symbol(']');
			break;
		case o_near:
			ea = toEA(cmd.cs, x.addr);
			if (!out_name_expr(x, ea, x.addr))
				out_bad_address(x.addr);
			break;
		case o_mem:
			ea = toEA(dataSeg(), x.addr);
			if (!out_name_expr(x, ea, x.addr))
				out_bad_address(x.addr);
			break;
		case o_reg:
			out_register(M16C_xx_RegNames[x.reg]);
			break;
		default:
			warning("out: %a: bad optype %d", cmd.ea, x.type);
		break;
	}

	return true;
}

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

Недостатки и выявленные проблемы


На текущий момент есть одна существенная непонятка. При попытке создать строку (ASCII-Z String) в Undefined данных, строка создается только до адреса кратного четырем байтам даже если нулевой байт еще не встретился. Если нулевой байт встречается раньше, то строка заканчивается им. С массивом та же самая проблема.

Самое неприятное, что я даже не знаю куда копать в данной ситуации. Может кто подскажет?

Вывод


Таким образом, написание плагинов для IDA pro не является чем-то очень сложным. Все просто, разве что при большом количестве комманд целевого ассемблера, довольно нудно. Введение DSL для разбора КОПов и операндов существенно облегчает и ускоряет разработку.
Tags:
Hubs:
+18
Comments 20
Comments Comments 20

Articles