PinkyVM Девиртуализация (Kaspersky Crackme)

Ниже будет представлен метод девиртуализации виртуальной машины от Лаборатории Касперского 2020г.

Программа запрашивает почту и пароль, проверят их и выводит либо Fail!, либо Success!

C:\Users\****\Desktop>Pinky.exe
                                                                        .uuu
      z@#"%c                      .uuzm**"""""*%mu..             z*"` .e@#N
     @!!!R.  #c              .z*"                    ^*c       z    dT!!!!!>
    '!!!!!!N   "i         u*"                            #s  :"   @?!!!!!!!R
    t!!!!!!!#u   "i    .@                                  ^$   :R!!!!!!!!!X
    '!!!!!!!!!#c   "i:#                                      ?> R!!!!!!!!!!X
    '!!!!!!!!!!!N   @                                         4W!!!!!!!!!!!>
    '!!!!!!!!!!!!Ru"                                           ?!!!!!!!!!!X
    'X!!!!!!!!!!!9~                                      .  .  'X!!!!!!!!!6
     R!!!!!!!!!!tF                                     z$#`   h &!!UR!!!!!F
     ?!!!!!$X!!!$                                    .@       X $WTR!!!!!X
      M!!!!!i#U!E  .                                @F        ! FdR!!!!!!f
      'X!!!!!#c'?u@#"*$N.                         :$          F'9!!!!!!!@
       9!!!!!!!?NM      ^*c                      dF          ' @!!!!!!!X>
        R!!!!!!!!&         "e                   d            < E!!!!!!X"
          t!!!!!!!#            ^N            :"      .e$"^  Fn!!!!!XP
           #X!!!!!!ML             *c       z"    .e$$$$$   M'!!!!W*
             "*UX!!X@t  ^%u.         ""**#).zd$$#$$$$$$$  <*@**"
                      'N    4$$$$$@$$$)$$#$$k4$$$$$$$$$E :$
                         ?>  "$$$$$$":$$$W$$$ "$$$$$$$$   %
                        :"           ? ^#*"  S  "$$$$$     ?
                        F            L      d$L            X
                        &           t$i    @$$$           f
                         *          $$$$$$$$$$&        @
                          '*.      W'$$$$$$$$FM h    u#
                             ^*muz* % $$$$$$":    `"
                                     # ^**" d
                                       "***"

What is the password, Pinky?
Email: testtest
Password: testtest
Fail!

Анализ

Первым делом после вывода ascii арта, программа запрашивает почту и пароль. Почта остается неизменной, а вот пароль переводится по 8 символов из hex строки в число и сохраняется в массив v7

После чего расшифровывается 219 байт данных и делается прыжок по ним:

j__printf("What is the password, Pinky?\n");
j__memset(v9, 0, 0x64u);
j__printf("Email: ");
v0 = get_stdin();
j__fgets(v9, 90, v0);
v9[j__strcspn(v9, "\r\n")] = 0;
v9[j__strcspn(v9, "\r\n")] = 0;
j__printf("Password: ");
v1 = get_stdin();
j__fgets(v8, 25, v1);
for ( i = 0; i < 3; ++i )
{
  j__memmove(&v5, &v8[8 * i], 8u);
  v6 = 0;
  v7[i] = j__strtoul(&v5, 0, 16);
}
for ( j = 0; j < 219; ++j )
  byte_44B000[j] ^= 0xDBu;
VirtualProtect(byte_44B000, 0xDBu, 0x40u, 0);
if ( (*(int (char *, int *))byte_44B000)(v9, v7) )

При выполнении первой инструкции расшифрованных данных происходит ошибка и исполнение переходит на зарегистрированный обработчик в SEH-chain. В этом обработчике инциализируется контекст виртуальной машины и ее дальнейшее выполнение, а байткодом служит только что расшифрованные данные. Можно предположить, что таким образом авторы облегчили себе получение контекста программы.

int Exceptions(PEXCEPTION_RECORD ExceptionRecord, int EstablisherFrame, PCONTEXT ContextRecord)
{
  struct VMCTX_t *vctx;

  vctx = (struct VMCTX_t *)VCTX_init(ContextRecord, 9999);
  VCTX_init_opcodes(vctx);
  VCTX_setVIP(vctx, sub_416744);
  VMEnter(vctx);
  ContextRecord->Ebp = vctx->_ebp;
  ContextRecord->Eax = vctx->_eax;
  ContextRecord->Ecx = vctx->_ecx;
  ContextRecord->Edx = vctx->_edx;
  ContextRecord->Ebx = vctx->_ebx;
  ContextRecord->Esi = vctx->_esi;
  ContextRecord->Edi = vctx->_edi;
  ContextRecord->Eip = vctx->_eip;
  ContextRecord->Esp = vctx->saved_esp;
  VCTX_free(vctx);
  return 0;
}

Структура виртуальной машины

Виртуальная машина представляет собой регистровый тип вм, где виртуальным регистрам соотвествуют реальные.

PVMCTX VCTX_init(PCONTEXT ContextRecord, unsigned int value)
{
  PVMCTX vctx;

  if ( !ContextRecord || !value || value > 65535 )
    return 0;
  vctx = (PVMCTX)j__malloc(0x454u);
  if ( !vctx )
    return 0;
  j__memset(vctx, 0, 0x454u);
  vctx->v_eip = 0;
  vctx->_eip = ContextRecord->Eip;
  LOWORD(vctx->vmexit) = 1;
  vctx->v_ebp_reg_or_mem = 0;
  vctx->_ebp = ContextRecord->Ebp;
  vctx->v_eax_reg_or_mem = 0;
  vctx->_eax = ContextRecord->Eax;
  vctx->v_ecx_reg_or_mem = 0;
  vctx->_ecx = ContextRecord->Ecx;
  vctx->v_edx_reg_or_mem = 0;
  vctx->_edx = ContextRecord->Edx;
  vctx->v_ebx_reg_or_mem = 0;
  vctx->_ebx = ContextRecord->Ebx;
  vctx->v_esi_reg_or_mem = 0;
  vctx->_esi = ContextRecord->Esi;
  vctx->v_edi_reg_or_mem = 0;
  vctx->_edi = ContextRecord->Edi;
  vctx->v_esp_reg_or_mem = 0;
  vctx->_esp = ContextRecord->Esp;
  vctx->_zf = 0;
  vctx->saved_esp = ContextRecord->Esp;
  VCTX_init_opcodes(vctx);
  return vctx;
}

В данном случае reg_or_mem обозначает является ли виртуальный регистр значением или ссылкой. ВМ имеет 256 обработчиков, 21 из них делают какие-то полезные действия, все остальные - nop. Обработчики хранятся в виртуальном контексте программы в виде массива и вызываются по номеру опкода.

Пример обработчика xor:

void __stdcall sub_419FD0(PVMCTX ctx)
{
  int v1;
  int v2;
  int v3; 
  unsigned int v4;

  out_reg = (unsigned __int8)GetNextByte(ctx);
  if ( out_reg >= 8 )
    crash(ctx, 1);
  v3 = (unsigned __int8)GetNextByte(ctx);
  if ( out_reg >= 8 )
    crash(ctx, 1);
  v2 = (unsigned __int8)GetNextByte(ctx);
  if ( out_reg >= 8 )
    crash(ctx, 1);
  if ( *(&ctx->v_ebp_reg_or_mem + 2 * out_reg) == 1 && *(&ctx->_ebp + 2 * out_reg) )
    j__free(*((void **)&ctx->_ebp + 2 * out_reg));
  v1 = GetRegisterValue((int)ctx, v3);
  *(&ctx->_ebp + 2 * out_reg) = GetRegisterValue((int)ctx, v2) ^ v1;
  *(&ctx->v_ebp_reg_or_mem + 2 * out_reg) = 0;
  if ( *(&ctx->_ebp + 2 * out_reg) )
    ctx->_zf = 0;
  else
    ctx->_zf = 1;
  ++ctx->_eip;
}

Концепт во всех обработчиках одинаковый:

  1. Берется байт с текущего адреса инструкции eip.
  2. Если это номер регистра, то сверяются границы.
  3. Если обработчик это математическая операция, то выставляется ZF zero flag.

Кстати в этом обработчике можно заметить ошибку: out_reg проверяется 3 раза, хотя должны проверяться другие регистры v3 и v2.

reg   = bs(l=8,   cls=(pinky_reg, ))
reg_deref = bs(l=8, cls=(pinky_reg_deref,))
imm8  = bs(l=8,   cls=(pinky_imm8,  pinky_arg))
imm16 = bs(l=16,  cls=(pinky_imm16, pinky_arg))
imm32 = bs(l=32,  cls=(pinky_imm32, pinky_arg))

addop("MOV",    [bs("00000001"), reg, imm16])     # 1
addop("JMP",    [bs("00010000"), imm16])          # 16
addop("JE" ,    [bs("00010001"), imm16])          # 17
addop("JNE",    [bs("00010010"), imm16])          # 18
addop("XOR",    [bs("00100000"), reg, reg, reg])  # 32
addop("ADD",    [bs("00100001"), reg, reg, reg])  # 33
addop("SUB",    [bs("00100010"), reg, reg, reg])  # 34
addop("MUL",    [bs("00100011"), reg, reg, reg])  # 35
addop("DIV",    [bs("00100100"), reg, reg, reg])  # 36
addop("INC",    [bs("00100101"), reg])            # 37
addop("DEC",    [bs("00100110"), reg])            # 38
addop("AND",    [bs("00100111"), reg, reg, reg])  # 39
addop("OR",     [bs("00101000"), reg, reg, reg])  # 40
addop("CMP",    [bs("01000000"), reg, reg])       # 64
addop("CMP",    [bs("01000001"), reg, imm16])     # 65
addop("CMP",    [bs("01000010"), reg, imm32])     # 66
addop("MOV1",   [bs("01010001"), reg, reg])       # 81  # REG TO REG OR MEM TO REG
addop("MOV2",   [bs("01100000"), reg, reg_deref]) # 96  # REG TO REG
addop("MOV3",   [bs("01100001"), reg, reg_deref]) # 97  # MEMORY
addop("RET",    [bs("01110010")])                 # 114
addop("NOP",    [bs("11000100")])                 # 196

Пару вещей, все таки, стоит сказать. Поскольку вм может работать как с 16-битными числами, так и с 32-битными, необходимо было конвертировать порядок байт. Это делается в одном из классов, описывающих аргумент, функцией decodeval:

class pinky_imm16(imm_noarg, pinky_arg):
  """Generic pinky immediate
  """
  intsize = 16
  intmask = (1 << intsize) - 1
  parser = base_expr

  def decodeval(self, v):
    return swap_sint(self.l, v) & self.intmask

  def encodeval(self, v):
    return swap_sint(self.l, v) & self.intmask

Так же у вм имеется возможность записывать в память. Чтобы это было заметно в дизассемблере, необходимо переводить выражение в ExprMem:

class pinky_reg_deref(pinky_arg):
  parser = deref_reg
  reg_info = gpr_infos  # the list of pinky registers defined in regs.py

  def decode(self, v):
    v = v & self.lmask
    if v >= len(self.reg_info.expr):
      return False
    self.expr = self.reg_info.expr[v]
    self.expr = ExprMem(self.expr, self.expr.size)
    return True

Что получилось

Как выглядит девиртуализированная функция:

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

Miasm IR после SSA преобразования:

Пишем keygen

Судя по алгоритму, введенная почта записывается сначала на стэк по 4 байта с помощью ксора (0 xor val = val) 3 раза. Затем к этим 4 байтам прибавляются 4 байта введенного ключа.

В конечном счете алгоритм будет такой:

int virtualized_function(int* mail, int* password) {
  int ret = 1;
  if (mail[0] + password[0] != 0x8b987160)
    ret = 0;
  if (mail[1] + password[1] != 0x8b987160)
    ret = 0;
  if (mail[2] + password[2] != 0x8b987160)
    ret = 0;
  }
  return ret;
}

Для написания keygen’а будем использовать z3 решатель:

from z3 import *
from struct import pack

magic = [0x8b987160, 0x9b854771, 0xa0bea5d7]

s = Solver()

inp = []

for i in range(3):
  m = BitVec('mail_%d' % i, 32)
  p = BitVec('passwd_%d' % i, 32)
  for j in range(0, 32, 8):
    v = (m & (0xff << j)) >> j
    s.add(v > 51, v < 124)

  for j in range(0, 32, 4):
    v = (m & (0xf << j)) >> j
    s.add(v >= 0, v < 16)
  
  s.add(m + p == magic[i])
  inp.append((m, p))

if s.check() == sat:
  model = s.model()

  mail = b''
  password = ''
  for i in range(3):
    v1 = model[inp[i][0]].as_long()
    v2 = model[inp[i][1]].as_long()
    v1 = pack("<I", v1)
    mail += v1
    password += hex(v2)[2:]

  print(mail)
  print(password)

Запускаем:

$ python3 solve.py
b'84484448@448'
53643d286351133d688a7197

Пробуем:

What is the password, Pinky?
Email: 84484448@448
Password: 53643d286351133d688a7197
Success!

Конец :)