У нас в «Лаборатории Касперского» есть команда анализа защищенности, занимающаяся поиском уязвимостей в самых разнообразных системах. В ней работают эксперты, способные исследовать практически любое устройство (и публикующие технические заметки о своих находках). Но в жизни практически каждого исследователя безопасности прошивок однажды наступает момент, когда он или она сталкивается с новым или не особо известным микроконтроллером или свежей процессорной архитектурой с кастомными расширениями. В последнее время такие моменты наступают все чаще — за прошедшие несколько лет рынок наполнился огромным количеством новых чипов из Поднебесной, в частности, на базе RISC-V, со своими собственными расширениями и реализациями ядер. И вот не так давно на анализ нашим исследователям попало устройство c таким чипом на базе RISC-V, c базовым набором инструкций RV32I и расширением P (причем еще и не последней версии), добавляющим короткие SIMD-операции (Packed-SIMD Instructions).
То, что наши эксперты видели его впервые — абсолютно нормально. Но, по всей видимости, его впервые видел и IDA Pro — инструмент, которым пользуются наши исследователи. Поэтому им пришлось не только изучить ранний черновик расширения P (оно же Packed-SIMD Extension), но также реализовать поддержку IDA Pro ряда инструкций из него и произвести лифтинг, то есть трансляцию инструкций в промежуточное представление или язык, понятные декомпилятору. Именно этим опытом они и решили поделиться в данной статье.
Но прежде чем переходить к описанию решения этих задач, стоит понять, с чем мы имеем дело, поэтому начать следует со знакомства с документацией по архитектуре RISC-V.
Особенности формата инструкций RISC-V
В базовом наборе инструкций архитектуры RV32I четыре формата: R, I, S, U. Информацию о каждом из них, а также о других можно найти в руководстве The RISC-V Instruction Set Manual Volume I: Unprivileged ISA.

Внимательный читатель сразу заметит особенности кодировки инструкций: опкоды и регистры имеют фиксированное расположение. Это сделано неслучайно, при подобном исполнении устройство декодера инструкций сильно облегчается и упрощается. Кроме того, декодер инструкций RISC-V построен на базе таблиц, что благотворно влияет на процесс разбора инструкций процессором.
Взгляд на декодер
Точкой входа процесса декодирования является поле opcode, которое занимает младшие 7 битов 32-битной инструкции, — inst[6:0]. Декодер — табличный, его схема представлена ниже.

У 32-битных и более широких инструкций младшие 2 бита всегда имеют значение 11, остальные 5 битов кодируют индекс внутри таблицы. В каждой ячейке таблицы может быть либо указатель на другую таблицу, либо декодер конечной инструкции, либо ничего (как водится — Reserved).
P Extension
С основами декода разобрались, осталось понять, где обитают инструкции из P Extension и как их парсить (декодировать). Для этого необходимо обратиться к черновику расширения, который можно найти в репозитории riscv/riscv-p-spec на GitHub. В нашем случае требуется одна из первых публичных версий — 0.5, поэтому запасаемся терпением, пуэром и поворачиваем время вспять — отматываем историю коммитов назад.
Версия 0.5, как и ряд последующих, примечательна тем, что использует значение 0x7F или 0b1111111 для опкода. В этом и был нюанс, который в итоге доставил нам немало хлопот. Дело в том, что данное значение используется для кодирования инструкций 80+ бит. Декодер тупой как пробка. Если не внести в его конструкцию изменения, он так и будет вычитывать по 80+ бит, что конкретно в этом случае не соответствует де��ствительности — инструкции данного расширения имеют длину 32 бита. Ибо в ранней версии, с которой мы столкнулись, использовалось именно это значение для опкода — 0x7F (хотя не должно было, потому что это ломает «базу» декодера, идет вразрез с оригинальной спецификацией ISA).
Более подробно об устройстве кодировки длин инструкций можно прочитать в соответствующем разделе документации. А кратко — в таблице ниже.

Лирическое отступление: знакомство с этим расширением началось с недекодируемых инструкций в ассемблерных листингах IDA Pro, а опкод 0x7F только подливал масла в огонь и сбивал с толку. Определение конечного расширения и его точной версии вылилось в отдельный квест, интуиция подсказывала, что это инструкции формата R-Type, а впрочем, это уже совсем другая история...
Добавление поддержки инструкций
Но вернемся от теории к практике. Для исследования устройства необходимо добавить поддержку инструкций umar64, maddr32, msubr32, mulsr64, mulr64. К счастью, все они находятся в рамках одной таблицы декода, о чем сказано в главе Instruction Encoding Table черновика расширения P Extension. Открываем любимый текстовый редактор или же блокнот, вспоминаем Python и приступаем.
База и абстракции
Как упоминалось ранее, внутри таблицы декода могут содержаться либо вложенные таблицы, либо декодеры конечных инструкций. Держим эту мысль в уме и реализуем несколько абстрактных и базовых классов:
class ITableEntry(metaclass=ABCMeta): pass class InstructionTable(ITableEntry): def __init__(self, rows: int, cols: int, get_row: Callable[[insn_t], int], get_col: Callable[[insn_t], int], *entries): if rows * cols != len(entries): raise ValueError(f"Rows ({rows}) and cols ({cols}) don't match entries length ({len(entries)})") self._rows = rows self._cols = cols self._get_row = get_row self._get_col = get_col self._entries = entries def _index(self, row: int, col: int) -> int: return row * self._cols + col def lookup(self, insn: insn_t) -> Optional[ITableEntry]: row = self._get_row(insn) col = self._get_col(insn) idx = self._index(row, col) entry = self._entries[idx] return entry.lookup(insn) if isinstance(entry, InstructionTable) else entry class ADecoder(ITableEntry): @staticmethod def _b2m(bits: int) -> int: return (1 << bits) - 1 @classmethod def _bits(cls, value: int, hi: int, lo: int) -> int: return (value >> lo) & cls._b2m(hi - lo + 1) @classmethod def decode(cls, insn: insn_t) -> bool: ...
Класс ITableEntry описывает сущность таблицы декода; дочерний класс InstructionTable — саму таблицу, а ADecoder — базовый класс, от которого наследуются другие, в том числе и декодеры конечных инструкций.
Чуть подробнее остановимся на классе InstructionTable: аргументы rows и cols определяют геометрию таблицы декодирования (согласно описанию), get_row и get_col — функции, которые извлекают индексы в рамках таблицы из инструкции, entries — элементы таблицы.
База R-Type
Так как интересующие нас инструкции имеют формат R-Type, создадим для этого отдельный вспомогательный класс.
class RTypeDecoder(ADecoder, ITableEntry): _itype: ClassVar[int] = -1 @classmethod def decode(cls, insn: insn_t) -> bool: opcode = get_bytes(insn.ea, 1)[0] # GE80B encoding is used for v0.5.x if opcode & 0x7F != 0x7F: print(f"Invalid opcode {opcode & 0x7F}") return False data = int.from_bytes(get_bytes(insn.ea, 4), byteorder="little") insn.size = 4 insn.Op1.type = o_reg insn.Op1.reg = cls._bits(data, 11, 7) # Rd insn.Op2.type = o_reg insn.Op2.reg = cls._bits(data, 19, 15) # Rs1 insn.Op3.type = o_reg insn.Op3.reg = cls._bits(data, 24, 20) # Rs2 insn.itype = cls._itype return True
Необходимо переопределить поведение функции decode, оно будет общим для всех инструкций. Алгоритм следующий:
проверяем, что опкод соответствует нужному значению;
вычитываем инструкцию целиком (32 бита или 4 байта);
заполняем информацию об аргументах — 3 регистра и их номера.
Наш квинтет
Завершающий маневр декодирования инструкций — указание конкретных внутренних идентификаторов. Для этого создаем перечисление идентификаторов инструкций (здесь их получается пять) и конечные классы, в которых доопределяется поведение функции decode.
class PExtension: maddr32 = CUSTOM_INSN_ITYPE msubr32 = CUSTOM_INSN_ITYPE + 1 mulr64 = CUSTOM_INSN_ITYPE + 2 umar64 = CUSTOM_INSN_ITYPE + 3 mulsr64 = CUSTOM_INSN_ITYPE + 4 name_mapping = { maddr32: "maddr32", msubr32: "msubr32", mulr64: "mulr64", umar64: "umar64", mulsr64: "mulsr64", } @classmethod def values(cls) -> Set[int]: return set(list(cls.name_mapping.keys())) class Maddr32(RTypeDecoder): _itype: ClassVar[int] = PExtension.maddr32 class Msubr32(RTypeDecoder): _itype: ClassVar[int] = PExtension.msubr32 class Mulr64(RTypeDecoder): _itype: ClassVar[int] = PExtension.mulr64 @classmethod def decode(cls, insn: insn_t) -> bool: res = super().decode(insn) if not res: return False insn.Op1.reg = insn.Op1.reg & ~1 # Rd - pair of registers return True class Umar64(RTypeDecoder): _itype: ClassVar[int] = PExtension.umar64 class Mulsr64(RTypeDecoder): _itype: ClassVar[int] = PExtension.mulsr64
Таблицы декода
Финальный штрих — таблицы декода, их всего 3 штуки.
def b2m(bits: int) -> int: return (1 << bits) - 1 def bits(value: int, hi: int, lo: int) -> int: return (value >> lo) & b2m(hi - lo + 1) def insn_data(insn: insn_t, length: int) -> int: return int.from_bytes(get_bytes(insn.ea, length), byteorder="little") p_funct3_001 = InstructionTable( 16, 8, lambda insn: bits(insn_data(insn, 4), 31, 28), lambda insn: bits(insn_data(insn, 4), 27, 25), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Umar64, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, Maddr32, Msubr32, None, None, None, None, None, None, None, None, None, None, None, None, Mulsr64, None, None, None, None, None, None, None, Mulr64, None, None, None, None, None, None, None, ) ge80b_table = InstructionTable( 1, 8, lambda insn: 0, lambda insn: bits(insn_data(insn, 4), 14, 12), None, p_funct3_001, None, None, None, None, None, None, ) rv_table = InstructionTable( 4, 8, lambda insn: bits(insn_data(insn, 4), 6, 5), lambda insn: bits(insn_data(insn, 4), 4, 2), None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, ge80b_table, )
Пробежимся по таблицам: rv_table — базовая таблица RISC-V, ge80b_table — таблица декода инструкций с опкодом 0x7F или 0b1111111, p_funct3_001 — таблица декода P Extension с funct3 = 001 в соответствии с описанием, из этой таблицы выход на декодеры конечных инструкций.
Интегрируем в IDA Pro
На этом описание логики декода заканчивается, остается лишь «подружить» наше творчество с IDA Pro. Для этого наследуем класс от IDP_Hooks и переопределяем в нем поведение функций анализа инструкций (ev_ana_insn) и отображения мнемоник и операндов (ev_out_mnem, ev_out_operand).
class PExtensionIdpHook(IDP_Hooks): def __init__(self): IDP_Hooks.__init__(self) @staticmethod def _b2m(bits: int) -> int: return (1 << bits) - 1 @classmethod def _bits(cls, value: int, hi: int, lo: int) -> int: return (value >> lo) & cls._b2m(hi - lo + 1) def _decode(self, insn: insn_t) -> "bool": opcode = get_bytes(insn.ea, 1)[0] if opcode & 0x7F != 0x7F: return False entry: ADecoder = rv_table.lookup(insn) if entry is None: return False return entry.decode(insn) def ev_ana_insn(self, out: "insn_t *") -> "bool": return self._decode(out) def ev_out_mnem(self, outctx: "outctx_t *") -> "int": typ = outctx.insn.itype if typ >= CUSTOM_INSN_ITYPE and typ in PExtension.name_mapping: mnem = PExtension.name_mapping[typ] outctx.out_tagon(COLOR_INSN) outctx.out_line(mnem) outctx.out_tagoff(COLOR_INSN) width = max(1, 16 - len(mnem)) outctx.out_line(" " * width) return 1 return 0 def ev_out_operand(self, outctx: "outctx_t *", op: "op_t const *") -> "bool": insn = outctx.insn if insn.itype in PExtension.values(): if op.type == o_displ: outctx.out_value(op, OOF_ADDR) outctx.out_register(ph_get_regnames()[op.reg]) return True return False
Сравнение до и после применения нашего расширения представлено ниже.

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

Проблема разрешается за счет разработки лифтера (Lifter): вынесем поведение в отдельный класс (наследуя от microcode_filter_t), в функциях которого опишем процедуру лифтинга с использованием микрокода декомпилятора Hex-Rays. Информацию по микрокоду можно почерпнуть здесь, а также из SDK декомпилятора. Для отладки подобных плагинов рекомендуется использовать Lucid — интерактивный плагин для просмотра микрокода.
Лифтинг: великий и ужасный
Базовая идея следующая: необходимо описать работу каждой добавленной инструкции, используя микроинструкции и маппинг (отображения) регистров в микрокод. Иными словами, надо рассказать декомпилятору, что за зверь такой перед ним и с чем его едят. Для этого нам также потребуется обратиться к черновику P Extension, в котором описывается поведение интересующих нас инструкций, и перевести это на язык, понятный конечному инструменту, — тот самый процесс лифтинга.
Вот фрагмент кода класса лифтера с примером самой «зубодробительной» реализации — для инструкций maddr32 и msubr32. Если интересно, то весь код с пояснениями можно найти на гитхабе.
class PExtensiionLifter(microcode_filter_t): def __init__(self): super(PExtensiionLifter, self).__init__() self._p_ext_handlers = { PExtension.maddr32: self._maddr32, PExtension.msubr32: self._msubr32, PExtension.mulr64: self._mulr64, PExtension.umar64: self._umar64, PExtension.mulsr64: self._mulsr64, } self._NO_MOP = mop_t() def install(self): install_microcode_filter(self, True) print(f"Installed P-Extension lifter... ({len(self._p_ext_handlers)} instruction(s) supported)") def remove(self): install_microcode_filter(self, False) print("Removed P-Extension lifter...") def match(self, cdg): return cdg.insn.itype in self._p_ext_handlers def apply(self, cdg): return self._p_ext_handlers[cdg.insn.itype](cdg, cdg.insn) def _mac_common(self, cdg, insn, op: m_add | m_sub): rd = reg2mreg(insn.Op1.reg) rs1 = reg2mreg(insn.Op2.reg) rs2 = reg2mreg(insn.Op3.reg) # Временный регистр для промежуточного итога умножения tmp64 = cdg.mba.alloc_kreg(8) # 64 bits tmp64_mop = mop_t(tmp64, 8) # Временный регистр для маскированного результата умножения tmp32 = cdg.mba.alloc_kreg(8) # 32 bits tmp32_mop = mop_t(tmp32, 8) imm_mop = mop_t() imm_mop.make_number(0xFFFF_FFFF, 8) # Hex-Rays не поддерживает операнды разного размера, поэтому мы используем расширение до 8 байтов # Надеемся, что старшие биты регистров rs1 и rs2 нулевые :) cdg.emit(m_mul, 8, rs1, rs2, tmp64, 0) # Как и раньше, маскируем младшую часть умножения 64-битной маской 0x0000_0000_FFFF_FFFF cdg.emit(m_and, imm_mop, tmp64_mop, tmp32_mop) # На данном этапе нужно уменьшить разрядность данных до 32 бит. Это задается вторым аргументом (4). cdg.emit(op, 4, rd, tmp32, rd, 0) # Удаляем временные регистры и освобождаем ресурсы. cdg.mba.free_kreg(tmp64, 8) cdg.mba.free_kreg(tmp32, 4) return MERR_OK def _maddr32(self, cdg, insn): return self._mac_common(cdg, insn, m_add) def _msubr32(self, cdg, insn): return self._mac_common(cdg, insn, m_sub)
Каждое действие в рамках микроопераций подписано в коде, нюансы умножения в функции maccommon двух 32-битных чисел с 64-битным результатом с точки зрения лифтера и его API также описаны в соответствующих комментариях.
Теперь можем насладиться плодами наших трудов, результат декомпиляции без потерь и ассемблерных вставок представлен ниже :)

Заключение
Вот таким получилось приключение по улучшению и прокачиванию инструментария :) (отдельный привет опкоду для P Extension ранней редакции чер��овика). Уважаемому читателю желаем не пугаться страшных документаций и куце описанных API, нередко за ними стоят весьма стройные концепции, понять и прочувствовать которые удается лишь при непосредственном использовании, а их разработчиков — разве что простить...
Кстати, если вам тоже по душе аккуратно ковыряться в разных системах и безжалостно это документировать — вливайтесь в наши ряды в команды тестирования на проникновение и анализа защищенности АСУ ТП.
Ну а если у вас был классный (или не очень) опыт столкновения с необычными расширениями и с их анализом — пишите свои истории в комментариях!
