Пришлось не так давно потрошить прошивку от 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 для разбора КОПов и операндов существенно облегчает и ускоряет разработку.

Комментарии (20)


  1. ignat99
    06.10.2015 14:35

    А что за устройство было с этой прошивкой?


    1. DamonV79
      06.10.2015 14:37

      Увы, не могу сказать — секретно.
      А зачем Вам, если не секрет? Может смогу ответить по другому.


  1. REU
    06.10.2015 14:36

    Судя по тому, что вы используете 6.1 мы понимаем где она была взята ;) в таком случае нужно было там же взять 6.6 и попробовать, т.к. ида тянет этот процессор www.hex-rays.com/products/ida/gallery/m16c.shtml


    1. DamonV79
      06.10.2015 14:42
      +4

      Да не, 6.6 после купили. Только процесс покупки затянулся на пару-тройку месяцев. Быстрее оказалось написать свое. Собственно, по этому и не стал дорабатывать модуль до конца (поддержка разных семейств, автообзывание SFR и прочее).
      Да и здесь, скорее подходом к написанию анализатора поделиться хотел. Впрочем, может кому и пригодиться. Да и вообще, статей по написанию модулей для IDA в рунете не густо.


      1. REU
        06.10.2015 14:49

        В рунете вообще информации не густо, потому все давно не ищут информацию в рунете.


        1. DamonV79
          06.10.2015 14:51
          +5

          Есть такое дело. Почему это не попытаться исправить? Все в наших силах.


      1. resetnow
        07.10.2015 19:46
        -1

        У них есть документация и примеры, я не уверен, что еще и статьи какие-то нужно писать.


  1. xvilka
    06.10.2015 20:53
    +3

    1. DamonV79
      06.10.2015 21:03
      +1

      Давно слышал про Радар, но все руки не доходят попробовать. Как он в сравнении с IDA'ой? В смысле удобства реверс-инжиниринга: восстановления перекрестных ссылок, создания структур, восстановления стековых переменных и прочего? Понятно, что дизассемблировать можно во многих IDE от производителя, но сие не очень-то и удобно.
      Хотелось бы уйти от IDA'ы в сторону чего-нибудь опенсорсного. Насколько адекватно Радар заменит IDA'у?


      1. xvilka
        06.10.2015 21:07

        Вполне адектватно, в случае, если не боитесь консоли.

        Пример графового представления
        image


        1. DamonV79
          06.10.2015 21:10
          +2

          Не, не боюсь :-). Чтож, надо будет попробовать. Спасибо за ссылку.


          1. xvilka
            06.10.2015 21:15
            +2

            У нас есть:



            Советую еще посмотреть слайды нашего воркшопа на BSidesLV/ISSA 2015 — github.com/Maijin/Workshop2015
            Там лучше читать исходники на LaTeX, так как там в комментарии расписано по шагам, как демки запускать.


        1. REU
          07.10.2015 11:17

          Скриншоты у вас всегда красивые, а на деле оно не всегда работает.


          1. xvilka
            07.10.2015 12:07
            +3

            Что не работает — в Github Issue Tracker.


    1. EvilsInterrupt
      07.10.2015 12:03
      +1

      Слишком юзабилити страдает. Если долго не возвращаться к Research-задачам, то навыки использования инструментов немного забывается. Я «пробовал» забыть как юзать IDA Pro и не получилось! А вот любой другой: windbg, syser, ollydbg и др. запросто!

      Я бы порекомендовал посмотреть разработчикам radare2 посмотреть в сторону electron


      1. xvilka
        07.10.2015 12:10
        +3

        У нас есть простенький webui — cloud.rada.re/m — демка.
        Однако js/html/css разработчиков в нашей команде почти и нет, слишком не связанная область. Но если есть желание помочь — то всегда рады.


        1. EvilsInterrupt
          07.10.2015 12:43
          +1

          Если у Вас нехватает рук, то Вы же всегда можете поступить следующим образом:
          1. Создать процесс сбора donate-средств
          2. Нанять верстальщиков и frontend-разработчиков на собранные средства

          Это первое. Второе в Вашем проекте нет ясной и четкой программы куда движется проект? Любому пользователю важно знать:
          1. Если он не может решить ряд своих задач сейчас, то когда он сможет их решить?
          2. Как ему можно влиять на процесс развития проекта? Можно конечно Issue создать или убедиться, что такой уже есть. Но как тому или иному Issue придать важность? Другими словами, должны быть механизмы создания опросов, чтобы пользователи могли ставить +1 или -1 за ту или иную фичу.


          1. xvilka
            07.10.2015 15:39
            +1

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


            1. EvilsInterrupt
              07.10.2015 16:08

              Попытаюсь донести, что Вы немного не правильно собираете фидбек.

              Ситуация №1:
              Ваши пользователи это не «бухгалтеры». Это, как правило, люди умеющие хорошо проверять свою работу. Они очень часто могут отказываться от попыток Вам написать из-за мыслей «А может это я чего-то не допонял? И проблему можно решить проще?». Это первое. Второе. Надо формулировать мысль! Чтобы поняли. А еще надо контроллировать ответы, чтобы быть уверенным наверняка, что разработчики продукта поняли твою проблему. На общение тратится время!

              Ситуация №2:
              А теперь сравните с простым просмотром уже созданных запросов на фичи и возможностью поставить +1.

              В каком случае Ваши пользователи более счастливы? В каком случае Вы быстрее получите обратную связь?


              1. xvilka
                07.10.2015 17:45

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