SpacezVM Девиртуализация Part 2 Бинарная трансляция с Miasm

В этой части мы опишем байткод из предыдущей статьи в наш псевдоязык, затем поднимим его в miasm IR для дальнейшей оптимизации и эмуляции.

Поскольку в miasm отсутствует документация по написанию кастомных архитектур, эта статья должна послужить отличным примером для этого(хоть и некоторые аспекты были до конца не поняты).

Для начала стоит поговорить как инструкции кодируются в байткоде spacezVM.

Описание байткода

  • Максимальная длина инструкции 5 байт
  • первый байт всегда номер опкода
  • Cледующие байты, если они есть - аргументы

Пример (все инструкции далее будут в хексе):

Инструкция 076800
Описание: Загрузить константу в регистр
Псевдокод: mov 0x68, r0

07 - опкод
68 - значение
00 - номер регистра (может быть от 0 до 3)
Инструкция 100002
Описание: Исключающее ИЛИ двух регистров
Псевдокод: xor r0, r2

10 - опкод
00 - номер регистра
02 - номер регистра (куда положить значение)

Можно заметить, что в отличие от x86 ассемблера, здесь операнд назначения всегда последний.

Более сложный пример:

Инструкция 1200000132
Описание: Условный прыжок, если значения регистров равны
Псевдокод: je 0x32, R0, R1

12 - опкод
00 - код условного типа (=, !=, >=, <)
00 - регистр сравнения
01 - регистр сравнения
32 - адрес прыжка, при выполнении условия

Все остальные инструкции, мы описали в предыдущей статье. Далее я покажу как представить эти инструкции в miasm.

Минимальные требования

Для описания любой архитектуры, нам необходимо создать следующие файлы:

  • regs.py - описание регистров процессора
  • ira.py - должен содержать класс промежуточного представления, необходим для работы miasm IR
  • disasm.py - должен содержать класс дизассемблирования, необходим для работы miasm Machine
  • arch.py - описание самой архитектуры (размер регистров, набор инструкций, ABI и.т.д.)
  • sem.py - описание промежуточного представления, как каждая инструкция процессора должна выглядеть в miasm IR
  • jit.py - должен содержать класс эмулятора, необходим для работы miasm Jitter

regs.py

Весь код выглядит так:

from miasm.expression.expression import ExprId
from miasm.core.cpu import gen_regs

# Used by internal miasm exceptions
exception_flags = ExprId("exception_flags", 32)
exception_flags_init = ExprId("exception_flags_init", 32)

# General-purpose registers (R0 - R3) names
gpr_names = ["R%d" % r for r in range(4)]  # register names
gpr_exprs, gpr_inits, gpr_infos = gen_regs(gpr_names, globals())  # sz=32 bits (default)
# PC SP QP
csr_names = ["PC", "SP", "QP"]
csr_exprs, csr_inits, csr_infos = gen_regs(csr_names, globals())

PC = csr_exprs[0]
SP = csr_exprs[1]
QP = csr_exprs[2]

# Set registers initial values
all_regs_ids = gpr_exprs + csr_exprs + [exception_flags]
all_regs_ids_init = gpr_inits + csr_inits + [exception_flags_init]
all_regs_ids_no_alias = all_regs_ids[:]
all_regs_ids_byname = dict([(x.name, x) for x in all_regs_ids])

regs_init = dict()  # mandatory name
for i, r in enumerate(all_regs_ids):
  regs_init[r] = all_regs_ids_init[i]

exception_flags и exception_flags_init нужны даже если у нас нет исключений. Они необходимы для работы miasm Jitter.

У нас будет 4 32-битных регистра с именами R0, R1, R2, R3.

gpr_names = ["R%d" % r for r in range(4)]  # register names
gpr_exprs, gpr_inits, gpr_infos = gen_regs(gpr_names, globals())

gpr_exprs - ExprId с именами этих регистров gpr_exprs_inits - ExprId с именами+_init этих регистров

Далее тоже самое делаем для специальных регистров: SP - указывает на конец очереди QP - указывает на начало очереди PC - счетчик инструкций

all_regs_ids, all_regs_ids_init список всех регистров и список всех init регистров соотвественно.

all_regs_ids_no_alias - список всех регистров, не имеющих alias. Например этот список будет содержать EAX EBX ECX EDX, но не AH AL BH BL и т.д. Поскольку у нас все регистры 32-битные и разбивать их на более мелкие мы не будем, то просто указываем all_regs_ids.

arch.py

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

class instruction_spacez(instruction): # Этот класс нужен для создания миасм инструкций
class spacez_additional_info(object):  # Нужен, но до конца не понят
class mn_spacez(cls_mn):               # Этот класс представляет собой miasm Machine и необходим для работы дизассемблера
class spacez_arg(m_arg):               # Аргумент инструкции (регистр или число)
class spacez_reg(reg_noarg, spacez_arg): # Описание регистра
class spacez_imm(imm_noarg, spacez_arg): # Описание константы

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

Тут мы создаем мнемоники инструкций, вот как это выглядит:

reg   = bs(l=8,  cls=(spacez_reg, ))
imm8  = bs(l=8,  cls=(spacez_imm, spacez_arg))

# mnemonics
addop("CLS", [bs("00000000")])
addop("STRD", [bs("00000001"), reg])
addop("STRB", [bs("00000010"), reg])
addop("STRW", [bs("00000011"), reg])
addop("LDRD", [bs("00000100"), reg])
addop("LDRB", [bs("00000101"), reg])
addop("LDRW", [bs("00000110"), reg])
addop("MOV", [bs("00000111"), imm8, reg])
addop("MOV", [bs("00001000"), reg, reg])
addop("ADD", [bs("00001001"), reg, reg])
addop("SUB", [bs("00001010"), reg, reg])
addop("MUL", [bs("00001011"), reg, reg])
addop("DIV", [bs("00001100"), reg, reg])
addop("MOD", [bs("00001101"), reg, reg])
addop("AND", [bs("00001110"), reg, reg])
addop("OR",  [bs("00001111"), reg, reg])
addop("XOR", [bs("00010000"), reg, reg])
addop("JMP", [bs("00010001"), imm8])
addop("JE",  [bs("00010010"), bs("00000000"), reg, reg, imm8])
addop("JNE", [bs("00010010"), bs("00000001"), reg, reg, imm8])
addop("JGE", [bs("00010010"), bs("00000010"), reg, reg, imm8])
addop("JL",  [bs("00010010"), bs("00000011"), reg, reg, imm8])
addop("VMEXIT", [bs("00010101")])
addop("PRN", [bs("00010100")])
addop("READ_INPUT", [bs("00010011"), imm8])

disasm.py

Тут объяснять в принципе нечего, мы просто наследуемся от базового класса:

from miasm.core.asmblock import disasmEngine
from miasm.arch.spacez.arch import mn_spacez


class dis_spacez(disasmEngine):
  """Spacez miasm disassembly engine
      Notes:
          - its is mandatory to call the miasm Machine
  """
  def __init__(self, bs=None, **kwargs):
    super(dis_spacez, self).__init__(mn_spacez, None, bs, **kwargs)

sem.py

В этом файле лежит описание всех инструкций на языке miasm IR. Все что нам нужно - описать каждую инструкцию.

Для начала создадим класс, который будет создавать IR:

class ir_spacez(IntermediateRepresentation):
  """Spacez miasm IR
     It transforms an instructon into an IR.
  """
  addrsize = 32

  def __init__(self, loc_db=None):
    IntermediateRepresentation.__init__(self, mn_spacez, None, loc_db)
    self.pc = mn_spacez.getpc()
    self.sp = mn_spacez.getsp()
    self.qp = mn_spacez.getqp()
    self.IRDst = ExprId("IRDst", 32)

  def get_ir(self, instr):
    """Get the IR from a miasm instruction."""
    args = instr.args
    instr_ir, extra_ir = mnemo_func[instr.name](self, instr, *args)
    return instr_ir, extra_ir

mnemo_func - словарь имен инструкций и функций, которые обрабатывают ту или иную инструкцию:

mnemo_func = {
    "MOV": mov,
    "ADD": add,
    "SUB": sub,
    "MUL": mul,
    "DIV": div,
    "MOD": mod,
    "AND": v_and,
    "OR": v_or,
    "XOR": xor,
    "JMP": jmp,
    "JE": je,
    "JNE": jne,
    "JGE": jge,
    "JL": jl,
    "STR": v_str,
    "STRB": v_strb,
    "STRW": v_strw,
    "CLS": cls,
    "VMEXIT": vmexit,
    "PRN" : prn,
    "READ_INPUT": read_input,
    "LDR": ldr,
    "LDRB": ldrb,
    "LDRW": ldrw,
}

Вот пример некоторых функций:

def xor(_, instr, reg1, reg2):
  result = reg1 ^ reg2
  e = [ExprAssign(reg2, result)]
  return e, []

def jmp(ir, instr, imm):
  if imm.is_loc():
    imm = loc_key_bitness(imm, 32)
  e = []
  e += [ExprAssign(PC, imm)]
  e += [ExprAssign(ir.IRDst, imm)]
  return e, []

def read_input(ir, instr, imm):
  e = []
  imm = ExprInt(int(imm), ir.sp.size)
  e += ir.call_effects(ExprId("scanf", 32), imm)
  return e, []

ira.py

Как я уже упоминалось выше, ira.py нужен для работы движка miasm IR. К счастью для нас, все что нам нужно сделать - наследоваться от базового класса. Вот как это будет выглядеть:

from miasm.ir.analysis import ira
from miasm.arch.spacez.sem import ir_spacez
from miasm.expression.expression import *

class ir_a_spacez_base(ir_spacez, ira):

  def __init__(self, loc_db):
    ir_spacez.__init__(self, loc_db)
    self.ret_reg = self.arch.regs.R0

  def call_effects(self, addr, *args):
    call_assignblk = [
            ExprAssign(self.ret_reg, ExprOp('call_func', addr, *args)),
    ]
    return call_assignblk

class ir_a_spacez(ir_a_spacez_base):

  def __init__(self, loc_db):
      ir_a_spacez_base.__init__(self, loc_db)

  def get_out_regs(self, _):
      return set([self.ret_reg, self.sp])

В базовом классе я перезаписал метод call_effects, который порождает expressions при вызове функций. У нас байткод вызывает всего несколько функций: scanf и puts и все они находятся в обработчиках опкодов, поэтому в нашем случае этот метод служит лишь для визуального представления в ir.

Дизассемблирование байткода

И вот что у нас получилось в итоге:

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

Вот как будет выглядеть miasm IR:

Теперь у нас есть возможность символьно выполнять байткод, оптимизировать и если сильно постараться - перевести в llvm и перекомпилировать в x86 :).


Конец.