Al buscar "Unicorn Engine" en Habr, me sorprendi贸 descubrir que esta herramienta nunca ha aparecido en los art铆culos. Intentar茅 llenar este vac铆o. Comencemos con lo b谩sico y veamos un ejemplo del uso del emulador en la vida real. Para no reinventar la rueda, decid铆 simplemente traducir este manual. Antes de comenzar, dir茅 que todos mis comentarios o comentarios se ver谩n as铆 .
驴Qu茅 es un motor de unicornio?
Los propios desarrolladores escriben sobre Motor de unicornio Unicorn Engine como este:
Unicorn es un emulador de procesador ligero, multiplataforma y de arquitectura m煤ltiple.
Este no es un emulador est谩ndar. No emula la operaci贸n de todo el programa o el sistema operativo completo. No admite comandos del sistema (como abrir un archivo, generar un car谩cter en la consola, etc.). Tendr谩 que hacer el marcado de la memoria y cargar los datos usted mismo, y luego simplemente comenzar谩 la ejecuci贸n desde alguna direcci贸n espec铆fica.
Entonces, 驴c贸mo es 煤til?
- Al analizar virus, puede llamar a funciones individuales sin crear un proceso malicioso.
- Para resolver CTF.
- Por fuzzing .
- Un complemento para gdb para predecir el estado futuro, por ejemplo, saltos futuros o valores de registro.
- Emulaci贸n de un c贸digo rico en funciones.
Que necesitas
- Motor Unicornio instalado con enlace Python.
- Desensamblador
Ejemplo
Como ejemplo, realice una tarea con hxp CTF 2017 bajo el nombre de Fibonacci . El binario se puede descargar aqu铆 .
Cuando inicia el programa, comienza a mostrar nuestra bandera en la consola, pero muy lentamente. Cada byte de bandera posterior se considera cada vez m谩s lento.
The flag is: hxp{F
Esto significa que para obtener la bandera en un per铆odo de tiempo razonable, necesitamos optimizar el funcionamiento de esta aplicaci贸n.
Usando IDA Pro ( yo personalmente us茅 radare2 + Cutter ) descompilamos el c贸digo en un pseudoc贸digo tipo C. A pesar de que el c贸digo no se descompil贸 correctamente, a煤n podemos obtener informaci贸n sobre lo que est谩 sucediendo en su interior.
C贸digo descompilado __int64 __fastcall main(__int64 a1, char **a2, char **a3) { void *v3;
unsigned int __fastcall fibonacci(int i, _DWORD *a2) { _DWORD *v2;
Aqu铆 est谩 el c贸digo de ensamblador de las funciones principales y de Fibonacci :
principal .text:0x4004E0 main proc near ; DATA XREF: start+1Do .text:0x4004E0 .text:0x4004E0 var_1C = dword ptr -1Ch .text:0x4004E0 .text:0x4004E0 push rbp .text:0x4004E1 push rbx .text:0x4004E2 xor esi, esi ; buf .text:0x4004E4 mov ebp, offset unk_4007E1 .text:0x4004E9 xor ebx, ebx .text:0x4004EB sub rsp, 18h .text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x4004FB mov edi, offset format ; "The flag is: " .text:0x400500 xor eax, eax .text:0x400502 call _printf .text:0x400507 mov r9d, 49h .text:0x40050D nop dword ptr [rax] .text:0x400510 .text:0x400510 loc_400510: ; CODE XREF: main+8Aj .text:0x400510 xor r8d, r8d .text:0x400513 jmp short loc_40051B .text:0x400513 ; --------------------------------------------------------------------------- .text:0x400515 align 8 .text:0x400518 .text:0x400518 loc_400518: ; CODE XREF: main+67j .text:0x400518 mov r9d, edi .text:0x40051B .text:0x40051B loc_40051B: ; CODE XREF: main+33j .text:0x40051B lea edi, [rbx+r8] .text:0x40051F lea rsi, [rsp+28h+var_1C] .text:0x400524 mov [rsp+28h+var_1C], 0 .text:0x40052C call fibonacci .text:0x400531 mov edi, [rsp+28h+var_1C] .text:0x400535 mov ecx, r8d .text:0x400538 add r8, 1 .text:0x40053C shl edi, cl .text:0x40053E mov eax, edi .text:0x400540 xor edi, r9d .text:0x400543 cmp r8, 8 .text:0x400547 jnz short loc_400518 .text:0x400549 add ebx, 8 .text:0x40054C cmp al, r9b .text:0x40054F mov rsi, cs:stdout ; fp .text:0x400556 jz short loc_400570 .text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc .text:0x400565 movzx r9d, byte ptr [rbp-1] .text:0x40056A jmp short loc_400510 .text:0x40056A ; --------------------------------------------------------------------------- .text:0x40056C align 10h .text:0x400570 .text:0x400570 loc_400570: ; CODE XREF: main+76j .text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc .text:0x40057A add rsp, 18h .text:0x40057E xor eax, eax .text:0x400580 pop rbx .text:0x400581 pop rbp .text:0x400582 retn .text:0x400582 main endp
fibonacci .text:0x400670 fibonacci proc near ; CODE XREF: main+4Cp .text:0x400670 ; fibonacci+19p ... .text:0x400670 test edi, edi .text:0x400672 push r12 .text:0x400674 push rbp .text:0x400675 mov rbp, rsi .text:0x400678 push rbx .text:0x400679 jz short loc_4006F8 .text:0x40067B cmp edi, 1 .text:0x40067E mov ebx, edi .text:0x400680 jz loc_400710 .text:0x400686 lea edi, [rdi-2] .text:0x400689 call fibonacci .text:0x40068E lea edi, [rbx-1] .text:0x400691 mov r12d, eax .text:0x400694 mov rsi, rbp .text:0x400697 call fibonacci .text:0x40069C add eax, r12d .text:0x40069F mov edx, eax .text:0x4006A1 mov ebx, eax .text:0x4006A3 shr edx, 1 .text:0x4006A5 and edx, 55555555h .text:0x4006AB sub ebx, edx .text:0x4006AD mov ecx, ebx .text:0x4006AF mov edx, ebx .text:0x4006B1 shr ecx, 2 .text:0x4006B4 and ecx, 33333333h .text:0x4006BA mov esi, ecx .text:0x4006BC .text:0x4006BC loc_4006BC: ; CODE XREF: fibonacci+C2j .text:0x4006BC and edx, 33333333h .text:0x4006C2 lea ecx, [rsi+rdx] .text:0x4006C5 mov edx, ecx .text:0x4006C7 shr edx, 4 .text:0x4006CA add edx, ecx .text:0x4006CC mov esi, edx .text:0x4006CE and edx, 0F0F0F0Fh .text:0x4006D4 shr esi, 8 .text:0x4006D7 and esi, 0F0F0Fh .text:0x4006DD lea ecx, [rsi+rdx] .text:0x4006E0 mov edx, ecx .text:0x4006E2 shr edx, 10h .text:0x4006E5 add edx, ecx .text:0x4006E7 and edx, 1 .text:0x4006EA xor [rbp+0], edx .text:0x4006ED pop rbx .text:0x4006EE pop rbp .text:0x4006EF pop r12 .text:0x4006F1 retn .text:0x4006F1 ; --------------------------------------------------------------------------- .text:0x4006F2 align 8 .text:0x4006F8 .text:0x4006F8 loc_4006F8: ; CODE XREF: fibonacci+9j .text:0x4006F8 mov edx, 1 .text:0x4006FD xor [rbp+0], edx .text:0x400700 mov eax, 1 .text:0x400705 pop rbx .text:0x400706 pop rbp .text:0x400707 pop r12 .text:0x400709 retn .text:0x400709 ; --------------------------------------------------------------------------- .text:0x40070A align 10h .text:0x400710 .text:0x400710 loc_400710: ; CODE XREF: fibonacci+10j .text:0x400710 xor edi, edi .text:0x400712 call fibonacci .text:0x400717 mov edx, eax .text:0x400719 mov edi, eax .text:0x40071B shr edx, 1 .text:0x40071D and edx, 55555555h .text:0x400723 sub edi, edx .text:0x400725 mov esi, edi .text:0x400727 mov edx, edi .text:0x400729 shr esi, 2 .text:0x40072C and esi, 33333333h .text:0x400732 jmp short loc_4006BC .text:0x400732 fibonacci endp
En esta etapa, tenemos muchas oportunidades para resolver este problema. Por ejemplo, podemos restaurar el c贸digo usando uno de los lenguajes de programaci贸n y aplicar la optimizaci贸n all铆, pero el proceso de recuperaci贸n del c贸digo es una tarea muy dif铆cil, durante la cual podemos cometer errores. Bueno, entonces comparar el c贸digo para encontrar el error generalmente no tiene valor. Pero, si usamos el Unicorn Engine, podemos omitir la etapa de reconstrucci贸n del c贸digo y evitar el problema descrito anteriormente. Por supuesto, podemos evitar estos problemas usando frida o escribiendo scripts para gdb, pero no se trata de eso.
Antes de comenzar la optimizaci贸n, ejecutaremos la emulaci贸n en Unicorn Engine sin cambiar el programa. Y solo despu茅s de un lanzamiento exitoso, pasemos a la optimizaci贸n.
Paso 1: deje que venga la virtualizaci贸n
Creemos el archivo fibonacci.py y gu谩rdelo junto al binario.
Comencemos importando las bibliotecas requeridas:
from unicorn import * from unicorn.x86_const import * import struct
La primera l铆nea carga las constantes Unicornio binarias y b谩sicas principales. La segunda l铆nea carga las constantes para las dos arquitecturas x86 y x86_64.
A continuaci贸n, agregue algunas funciones necesarias:
def read(name): with open(name) as f: return f.read() def u32(data): return struct.unpack("I", data)[0] def p32(num): return struct.pack("I", num)
Aqu铆 anunciamos las funciones que necesitaremos m谩s adelante:
- leer simplemente devuelve el contenido del archivo,
- u32 toma una cadena de 4 bytes en la codificaci贸n LE y la convierte a int,
- p32 hace lo contrario: toma un n煤mero y lo convierte en una cadena de 4 bytes en la codificaci贸n LE.
Nota: Si ha instalado pwntools , entonces no necesita crear estas funciones, solo necesita importarlas:
from pwn import *
Y as铆, finalmente, comencemos a inicializar nuestra clase Unicorn Engine para la arquitectura x86_64:
mu = Uc (UC_ARCH_X86, UC_MODE_64)
Aqu铆 llamamos a las funciones Uc con los siguientes par谩metros:
- El primer par谩metro es la arquitectura principal. Las constantes comienzan con UC_ARCH_ ;
- El segundo par谩metro es la especificaci贸n de la arquitectura. Las constantes comienzan con UC_MODE_ .
Puedes encontrar todas las constantes en la hoja de trucos .
Como escrib铆 anteriormente, para usar el motor Unicorn, necesitamos inicializar la memoria virtual manualmente. Para este ejemplo, necesitamos colocar el c贸digo y la pila en alg煤n lugar de la memoria.
La direcci贸n base (Base addr) del binario comienza en 0x400000. Pongamos nuestra pila en 0x0 y asignemos 1024 * 1024 memoria para ello. Lo m谩s probable es que no necesitemos tanto espacio, pero a煤n as铆 no duele.
Podemos marcar la memoria llamando al m茅todo mem_map .
Agregue estas l铆neas:
BASE = 0x400000 STACK_ADDR = 0x0 STACK_SIZE = 1024*1024 mu.mem_map(BASE, 1024*1024) mu.mem_map(STACK_ADDR, STACK_SIZE)
Ahora necesitamos cargar el binario en su direcci贸n principal de la misma manera que lo hace el gestor de arranque. Despu茅s de eso, necesitamos establecer RSP al final de la pila.
mu.mem_write(BASE, read("./fibonacci")) mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
Ahora podemos comenzar la emulaci贸n y ejecutar el c贸digo, pero necesitamos averiguar con qu茅 direcci贸n comenzar a trabajar y cu谩ndo debe detenerse el emulador.
Tome la direcci贸n del primer comando de main () , podemos comenzar la emulaci贸n desde 0x004004e0. El final se considerar谩 una llamada a putc ("\ n") , que se encuentra en 0x00400575, despu茅s de mostrar la bandera completa.
.text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc
Podemos comenzar a emular:
mu.emu_start(0x004004e0,0x00400575)
Ahora ejecuta el script:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py Traceback (most recent call last): File "solve.py", line 32, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
Vaya, algo sali贸 mal, pero ni siquiera sabemos qu茅. Justo antes de llamar a mu.emu_start, podemos agregar:
def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) mu.hook_add(UC_HOOK_CODE, hook_code)
Este c贸digo agrega un gancho. Declaramos nuestra propia funci贸n hook_code , que el emulador llama antes de cada comando. Toma los siguientes par谩metros:
- nuestra copia de Uc ,
- direcci贸n de instrucciones
- instrucciones de tama帽o
- datos de usuario (podemos pasar este valor con un argumento opcional a hook_add () ).
Ahora, si ejecutamos el script, deber铆amos ver el siguiente resultado:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py >>> Tracing instruction at 0x4004e0, instruction size = 0x1 >>> Tracing instruction at 0x4004e1, instruction size = 0x1 >>> Tracing instruction at 0x4004e2, instruction size = 0x2 >>> Tracing instruction at 0x4004e4, instruction size = 0x5 >>> Tracing instruction at 0x4004e9, instruction size = 0x2 >>> Tracing instruction at 0x4004eb, instruction size = 0x4 >>> Tracing instruction at 0x4004ef, instruction size = 0x7 Traceback (most recent call last): File "solve.py", line 41, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
En la direcci贸n donde ocurri贸 el error, podemos entender que nuestro script no puede procesar este comando:
.text:0x4004EF mov rdi, cs:stdout ; stream
Esta instrucci贸n lee datos de la direcci贸n 0x601038 (puede verlos en IDA Pro). Esta es la secci贸n .bss que no marcamos. Mi soluci贸n ser铆a simplemente omitir todas las instrucciones problem谩ticas si esto no afecta la l贸gica del programa.
A continuaci贸n hay otra instrucci贸n problem谩tica:
.text:0x4004F6 call _setbuf
No podemos llamar a ninguna funci贸n con glibc, ya que no tenemos glibc cargado en la memoria. En cualquier caso, no necesitamos este comando, por lo que tambi茅n podemos omitirlo.
Aqu铆 est谩 la lista completa de comandos para omitir:
.text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x400502 call _printf .text:0x40054F mov rsi, cs:stdout ; fp
Para omitir comandos, necesitamos reescribir RIP con las siguientes instrucciones:
mu.reg_write(UC_X86_REG_RIP, address+size)
Ahora hook_code deber铆a verse as铆:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size)
Tambi茅n debemos hacer algo con instrucciones que muestren la bandera en la consola byte por byte.
.text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc
__IO_putc toma bytes para la salida como primer argumento (este es el registro RDI ).
Podemos leer datos directamente desde el registro, enviar datos a la consola y omitir este conjunto de instrucciones. El hook_code actualizado se presenta a continuaci贸n:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data):
Podemos correr y todo funcionar谩, pero a煤n as铆 lentamente.
Paso 2: 隆Aumenta la velocidad!
Pensemos en aumentar la velocidad del trabajo. 驴Por qu茅 es este programa tan lento?
Si miramos el c贸digo descompilado, veremos que main () llama a fibonacci () varias veces y fibonacci () es una funci贸n recursiva. Echemos un vistazo m谩s de cerca a esta funci贸n; toma y devuelve dos argumentos. El primer valor de retorno se pasa a trav茅s del registro RAX , el segundo se devuelve a trav茅s del enlace que se pas贸 a trav茅s del segundo argumento a la funci贸n. Si observamos m谩s a fondo la relaci贸n entre main () y fibonacci () , veremos que el segundo argumento solo toma dos valores posibles: 0 o 1. Si a煤n no ve esto, ejecute gdb y ponga un punto de interrupci贸n al comienzo de la funci贸n Fibonacci () .
Para optimizar el funcionamiento del algoritmo, podemos utilizar la programaci贸n din谩mica para recordar el valor de retorno de los par谩metros entrantes. Piensa por ti mismo, el segundo argumento solo puede tomar dos valores posibles, por lo que todo lo que tenemos que hacer es recordar $ en l铆nea $ 2 * MAX \ _OF \ _FIRST \ _ARGUMENT $ en l铆nea $ vapor
Para los que no entiendenFibonacci es una funci贸n recursiva que calcula el siguiente valor como la suma de los dos anteriores. A cada paso ella va m谩s profundo. Cada vez que comienza de nuevo, sigue el mismo camino que antes, m谩s un nuevo significado.
Un ejemplo:
Suponga que la profundidad = 6, luego: 1 1 2 3 5 8 .
Y ahora profundidad = 8, entonces: 1 1 2 3 5 8 13 21.
Podr铆amos recordar que los primeros 6 miembros son 1 1 2 3 5 8 , y cuando nos piden contar m谩s de lo que recordamos, tomamos lo que recordamos y contamos solo lo que falta.
Una vez que RIP est谩 al comienzo de fibonacci () , podemos obtener los argumentos de la funci贸n. Sabemos que una funci贸n devuelve un resultado cuando sale de una funci贸n. Como no podemos operar con dos par谩metros a la vez, necesitamos una pila para devolver los par谩metros. Cuando ingresamos fibonacci (), necesitamos poner los argumentos en la pila y recogerlos cuando salimos. Para almacenar los pares contados, podemos usar un diccionario.
驴C贸mo procesar un par de valores?
- Al comienzo de la funci贸n, podemos verificar si este par est谩 en los resultados que ya conocemos:
- si lo hay, entonces podemos devolver este par. Solo necesitamos escribir los valores de retorno en RAX y en la direcci贸n del enlace, que se encuentra en el segundo argumento. Tambi茅n asignamos una direcci贸n RIP para salir de la funci贸n. No podemos usar RET en fibonacci () , ya que estas llamadas est谩n enganchadas, por lo que tomaremos algo de RET de main () ;
- Si estos valores no lo son, simplemente los agregamos a la pila.
- Antes de salir de la funci贸n, podemos guardar el par devuelto. Conocemos los argumentos de entrada, ya que podemos leerlos desde nuestra pila.
Este c贸digo se presenta aqu铆. FIBONACCI_ENTRY = 0x00400670 FIBONACCI_END = [ 0x004006f1, 0x00400709] instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f]
Aqu铆 est谩 el gui贸n completo
Hurra, finalmente pudimos optimizar la aplicaci贸n usando el motor Unicorn. Buen trabajo!
Una nota
Ahora decid铆 darte un poco de tarea.
Aqu铆 puede encontrar tres tareas m谩s, cada una de las cuales tiene una pista y una soluci贸n completa. Puedes echar un vistazo a la hoja de trucos mientras resuelves problemas.
Uno de los problemas m谩s molestos es recordar el nombre de la constante deseada. Esto es f谩cil de manejar si usa complementos de Tab en IPython . Cuando tenga instalado IPython, puede escribir desde unicornio import UC_ARCH_ presione Tab y se le mostrar谩n todas las constantes que comienzan de la misma manera.