Unicorn Engine

Unicorn engine é uma framework que permite a emulação de CPU para executar instruções de diferentes arquiteturas, sendo utilizado em desafios CTFs, engenharia reversa, análise de malware e fuzzing.

Instalação e documentação

Para instalar o Unicorn como uma biblioteca python, basta apenas usar o seguinte comando no terminal

pip install unicorn

A documentação do Unicorn pode ser encontrada em https://www.unicorn-engine.org/docs/

Exemplos

Exemplo 1

A seguir, um exemplo de um uso simples do Unicorn, apenas para demonstrar principais funcionalidades:

from unicorn import *
from unicorn.x86_const import *

# Cada instrução em assembly é representada em opcodes
# Código em Assembly x86: mov eax, 2; add eax, 3; nop
# Em hexadecimal: B8 02 00 00 00 83 C0 03 90
CODE = b"\xB8\x02\x00\x00\x00\x83\xC0\x03\x90"

# Endereço base onde o código será carregado
ADDRESS = 0x1000

# Inicializa o emulador para x86 (32 bits)
mu = Uc(UC_ARCH_X86, UC_MODE_32)

# Mapeia 4KB de memória a partir do endereço escolhido
mu.mem_map(ADDRESS, 4 * 1024)

# Escreve o código na memória emulada
mu.mem_write(ADDRESS, CODE)

# Define o registrador EAX com 0
mu.reg_write(UC_X86_REG_EAX, 0)

# Inicia a emulação do código (do início até o fim)
mu.emu_start(ADDRESS, ADDRESS + len(CODE))

# Lê o valor final do registrador EAX
eax_value = mu.reg_read(UC_X86_REG_EAX)
print(f"EAX = {eax_value}")

Exemplo 2

A seguir, há um exemplo de uma resolução de um desafio de autoria própria. Primeiramente, precisamos analisar o código obtido através do ghidra com o binário disponibilizado:

undefined8 main(void)

{
  int iVar1;
  int local_188 [24];
  int aiStack_128 [24];
  char local_c8 [32];
  undefined8 local_a8;
  undefined8 local_a0;
  undefined8 local_98;
  undefined8 local_90;
  undefined8 local_88;
  undefined8 local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined8 local_60;
  undefined8 local_58;
  undefined7 local_50;
  undefined4 uStack_49;
  code *local_38;
  code *local_30;
  int local_24;
  int local_20;
  int local_1c;
  
  local_a8 = 0x7589f889e5894855;
  local_a0 = 0xfc45be0ffc4588f8;
  local_98 = 0xc1f8558bf845af0f;
  local_90 = 0xff8458bc20102fa;
  local_88 = 0x55be0f020c8dc0af;
  local_80 = 0xc2af0ffc45be0ffc;
  local_78 = 0xaf0ff8458b01148d;
  local_70 = 0xf020c8d16c06bc0;
  local_68 = 0xffc45be0ffc55be;
  local_60 = 0xaf0ffc45be0fd0af;
  local_58 = 0x1000002fec069c2;
  local_50 = 0xc1000009fb2dc8;
  uStack_49 = 0xc35d02e0;
  local_38 = (code *)mmap((void *)0x0,99,7,0x22,-1,0);
  *(undefined8 *)local_38 = local_a8;
  *(undefined8 *)(local_38 + 8) = local_a0;
  *(undefined8 *)(local_38 + 0x10) = local_98;
  *(undefined8 *)(local_38 + 0x18) = local_90;
  *(undefined8 *)(local_38 + 0x20) = local_88;
  *(undefined8 *)(local_38 + 0x28) = local_80;
  *(undefined8 *)(local_38 + 0x30) = local_78;
  *(undefined8 *)(local_38 + 0x38) = local_70;
  *(undefined8 *)(local_38 + 0x40) = local_68;
  *(undefined8 *)(local_38 + 0x48) = local_60;
  *(undefined8 *)(local_38 + 0x50) = local_58;
  *(ulong *)(local_38 + 0x58) = CONCAT17((undefined1)uStack_49,local_50);
  *(undefined4 *)(local_38 + 0x5f) = uStack_49;
  local_188[0] = 0x442a9914;
  local_188[1] = 0x19add980;
  local_188[2] = 0x78434448;
  local_188[3] = 0x4d0c1b20;
  local_188[4] = 0x53d9cdd4;
  local_188[5] = 0x3ea4745c;
  local_188[6] = 0xe60fdf68;
  local_188[7] = 0x19adefa4;
  local_188[8] = 0x415daa08;
  local_188[9] = 0x9c955aa0;
  local_188[10] = 0x85236b6c;
  local_188[0xb] = 0x157ca100;
  local_188[0xc] = 0x4d0c563c;
  local_188[0xd] = 0x19ae1fac;
  local_188[0xe] = 0x9c958b64;
  local_188[0xf] = 0x396d1f2c;
  local_188[0x10] = 0x183a3c30;
  local_188[0x11] = 0x5360f270;
  local_188[0x12] = 0x19ae5b74;
  local_188[0x13] = 0x6909da8;
  local_188[0x14] = 0x686da524;
  local_188[0x15] = 0x64b3e4a4;
  local_30 = local_38;
  __isoc99_scanf(&DAT_00102004,local_c8);
  for (local_1c = 0; local_1c < 0x16; local_1c = local_1c + 1) {
    iVar1 = (*local_38)((int)local_c8[local_1c],local_1c);
    aiStack_128[local_1c] = iVar1;
  }
  local_20 = 0;
  local_24 = 0;
  do {
    if (0x15 < local_24) {
LAB_0010143c:
      if (local_20 == 0) {
        puts("That\'s correct\n");
      }
      else {
        puts("That\'s not it\n");
      }
      return 0;
    }
    if (aiStack_128[local_24] != local_188[local_24]) {
      local_20 = 1;
      goto LAB_0010143c;
    }
    local_24 = local_24 + 1;
  } while( true );
}

Basicamente, esse programa utiliza a função nmap para escrever bytes em uma região de memória, utilizando as variáveis local_a8 até local_50, e então chama essa função para cada caractere que o usuário digitou e com local_1c como segundo parâmetro. Após isso, o valor retornado de cada valor é comparado com cada valor do vetor local_188 e, se todos os valores forem iguais, imprime que a flag está correta.

Uma solução que poderíamos utilizar seria reconstruir o código em assembly da região de memória que foi alocado e então fazer a engenharia reversa da função. Porém, podemos utilizar para executar essa função em assembly e então com brute-force obter a flag.

lista = [      "7589f889e5894855",
  "fc45be0ffc4588f8",
  "c1f8558bf845af0f",
  "ff8458bc20102fa",
  "55be0f020c8dc0af",
  "c2af0ffc45be0ffc",
  "af0ff8458b01148d",
  "f020c8d16c06bc0",
  "ffc45be0ffc55be",
  "af0ffc45be0fd0af",
  "1000002fec069c2",
  "5d02e0c1000009fb2dc8", ]

CODE = ""

for string in lista:
    i = 0
    aux = ""
    for j in string[::-1]:

        aux += j

        if i % 2 == 1:
            CODE += j 
            CODE += aux[0]
            CODE += " "
            aux = ""

        i += 1
    
    if i % 2 == 1:
        CODE += "0"
        CODE += aux[0]
        CODE += " "
        aux = ""

print(CODE)

# Saída
# 55 48 89 e5 89 f8 89 75 f8 88 45 fc 0f be 45 fc 0f af 45 f8 8b 55 f8 c1 fa 02 01 c2 8b 45 f8 0f af c0 8d 0c 02 0f be 55 fc 0f be 45 fc 0f af c2 8d 14 01 8b 45 f8 0f af c0 6b c0 16 8d 0c 02 0f be 55 fc 0f be 45 fc 0f af d0 0f be 45 fc 0f af c2 69 c0 fe 02 00 00 01 c8 2d fb 09 00 00 c1 e0 02 5d

Essa função, pegas os valores definidos na função e transforma em uma sequência de bytes separados por espaço.

Agora, podemos realizar a emulação com o Unicorn.

from unicorn import *
from unicorn.x86_const import *
from capstone import *
import ctypes

# Os bytes obtidos da função anterior
CODE_RAW = "55 48 89 e5 89 f8 89 75 f8 88 45 fc 0f be 45 fc 0f af 45 f8 8b 55 f8 c1 fa 02 01 c2 8b 45 f8 0f af c0 8d 0c 02 0f be 55 fc 0f be 45 fc 0f af c2 8d 14 01 8b 45 f8 0f af c0 6b c0 16 8d 0c 02 0f be 55 fc 0f be 45 fc 0f af d0 0f be 45 fc 0f af c2 69 c0 fe 02 00 00 01 c8 2d fb 09 00 00 c1 e0 02 5d"
CODE = bytes.fromhex(CODE_RAW)

# Vetor local_188 do desafio
# Obs: como int tem 32 bits e existia valores que ultrapassavam 32 bits no código em c,
# eles viram negativos nesse vetor
key = [1143642388, 430823808, 2017674312, 1292639008, 1406782932, 1050965084, -435167384, 430829476, 1096657416, -1667933536, -2061276308, 360489216, 1292654140, 430841772, -1667921052, 963452716, 406469680, 1398862448, 430857076, 110140840, 1752016164, 1689511076 ]

# Uso da biblioteca capstone para imprimir o código em assembly
md = Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(CODE, 0x1000):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")

# Definição dos endereços
ADDRESS = 0x10000000
STACK_ADDR = 0x200000
STACK_SIZE = 0x2000

# Inicialização do emulador
mu = Uc(UC_ARCH_X86, UC_MODE_64)

# Alocando o código que o emulador vai executar
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
mu.mem_write(ADDRESS, CODE)

# Alocando memória para a pilha
mu.mem_map(STACK_ADDR, STACK_SIZE)

# Pegando o topo da pilha e colocando esse valor nos regisrtradores RSP e RBP
STACK_TOP = STACK_ADDR + STACK_SIZE - 0x100
mu.reg_write(UC_X86_REG_RSP, STACK_TOP)
mu.reg_write(UC_X86_REG_RBP, STACK_TOP)

flag = ""

i = 0

# Brute-force da função
while i < 0x16:

    # Inicializando com o primeiro caractere ascii que é imprimível "!"
    guess = 0x20

    EAX = 0

    while EAX != key[i]:

        guess += 1

        # Parâmetros que são passados para a função
        mu.reg_write(UC_X86_REG_EDI, guess)  # Caractere da string digitada pelo usuário
        mu.reg_write(UC_X86_REG_ESI, i)  # O índice do caractere

        # Emulação do código
        mu.emu_start(ADDRESS, ADDRESS + len(CODE))

        # Depois da emulação, pegamos o valor que está em EAX, levando em conta o sinal
        EAX = mu.reg_read(UC_X86_REG_EAX)
        EAX = ctypes.c_int32(EAX).value

    # Adicionamos o caractere à flag e passamos para o próximo índice
    i += 1
    flag += chr(guess)

print(flag)
# Saída: H4WK{Fl4G_Z1K4_D3M4!S}

Referências

Site oficial: https://www.unicorn-engine.org/

Unicorn notes: https://github.com/alexander-hanel/unicorn-engine-notes

Atualizado