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 90CODE=b"\xB8\x02\x00\x00\x00\x83\xC0\x03\x90"# Endereço base onde o código será carregadoADDRESS=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 escolhidomu.mem_map(ADDRESS,4*1024)# Escreve o código na memória emuladamu.mem_write(ADDRESS,CODE)# Define o registrador EAX com 0mu.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 EAXeax_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:
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.
Essa função, pegas os valores definidos na função e transforma em uma sequência de bytes separados por espaço.
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
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}