Introducción a Frida
¿Qué es Frida?
Frida es un framework de instrumentación de binarios multiplataforma: en el momento de escribir este artículo, Frida puede ser usado en Windows, Mac, Linux, iOS y Android.
Frida funciona inyectando el motor de JavaScript V8 en aplicaciones nativas. De esta manera, Frida puede ejecutar código JavaScript en el contexto de la aplicación en la que está inyectado, pudiendo así acceder a la memoria, hookear funciones, llamarlas, etc.
Frida no es sólo una librería. Frida trae consigo un conjunto de herramientas que te pueden ayudar a la hora de realizar una análisis de un aplicación.
En este post os mostraré algunas de las capacidades de Frida mediante la realización de una serie de ejercicios prácticos en los que analizaremos un binario compilado para OSx (Mach-O 64-bit executable x86_64).
Analizando el binario
Analizando el binario con radare2 (o haciendo trampa y mirando el código) se puede ver que el programa pide una contraseña y luego comprueba si esta es buena
o no.
Si la contraseña es correcta devuelve el mensaje «Yey!», y si no es buena «Nice try, but nope».
$ r2 poc -- Experts agree, security holes suck, and we fixed some of them! [0x100000e60]> pd 60 @ main ;-- main: ;-- entry0: ;-- _main: ;-- func.100000e60: 0x100000e60 55 push rbp 0x100000e61 4889e5 mov rbp, rsp 0x100000e64 4883ec50 sub rsp, 0x50 0x100000e68 c745fc000000. mov dword [rbp - 4], 0 0x100000e6f 897df8 mov dword [rbp - 8], edi 0x100000e72 488975f0 mov qword [rbp - 0x10], rsi 0x100000e76 c745ec000000. mov dword [rbp - 0x14], 0 .-> 0x100000e7d 488d3d080100. lea rdi, [rip + 0x108] ; 0x100000f8c ; section.3.__cstring ; "Password: " @ 0x100000f8c | 0x100000e84 b000 mov al, 0 | 0x100000e86 e88f000000 call sym.imp.printf | 0x100000e8b 488d3d050100. lea rdi, [rip + 0x105] ; 0x100000f97 ; str._10s ; "%10s" @ 0x100000f97 | 0x100000e92 488d75cc lea rsi, [rbp - 0x34] | 0x100000e96 8945c8 mov dword [rbp - 0x38], eax | 0x100000e99 b000 mov al, 0 | 0x100000e9b e880000000 call sym.imp.scanf | 0x100000ea0 488d7dcc lea rdi, [rbp - 0x34] | 0x100000ea4 c645dd64 mov byte [rbp - 0x23], 0x64 ; [0x64:1]=0 ; 'd' | 0x100000ea8 c645d949 mov byte [rbp - 0x27], 0x49 ; [0x49:1]=0 ; 'I' | 0x100000eac c645de65 mov byte [rbp - 0x22], 0x65 ; [0x65:1]=0 ; 'e' | 0x100000eb0 c645d653 mov byte [rbp - 0x2a], 0x53 ; [0x53:1]=0 ; 'S' | 0x100000eb4 c645df00 mov byte [rbp - 0x21], 0 | 0x100000eb8 c645d765 mov byte [rbp - 0x29], 0x65 ; [0x65:1]=0 ; 'e' | 0x100000ebc c645da6e mov byte [rbp - 0x26], 0x6e ; [0x6e:1]=0 ; 'n' | 0x100000ec0 c645dc69 mov byte [rbp - 0x24], 0x69 ; [0x69:1]=0 ; 'i' | 0x100000ec4 c645d863 mov byte [rbp - 0x28], 0x63 ; [0x63:1]=0 ; 'c' | 0x100000ec8 c645db73 mov byte [rbp - 0x25], 0x73 ; [0x73:1]=69 ; 's' ; "EXT" @ 0x73 | 0x100000ecc 8945c4 mov dword [rbp - 0x3c], eax | 0x100000ecf e8fcfeffff call sym._encrypt_arg | 0x100000ed4 488d75d6 lea rsi, [rbp - 0x2a] | 0x100000ed8 488945e0 mov qword [rbp - 0x20], rax | 0x100000edc 488b7de0 mov rdi, qword [rbp - 0x20] | 0x100000ee0 e847000000 call sym.imp.strcmp | 0x100000ee5 83f800 cmp eax, 0 ,==< 0x100000ee8 0f8416000000 je 0x100000f04 || 0x100000eee 488d3da70000. lea rdi, [rip + 0xa7] ; 0x100000f9c ; str.Nice_try__but_nope_n ; "Nice try, but nope." @ 0x100000f9c || 0x100000ef5 b000 mov al, 0 || 0x100000ef7 e81e000000 call sym.imp.printf || 0x100000efc 8945c0 mov dword [rbp - 0x40], eax ,===< 0x100000eff e911000000 jmp 0x100000f15 |`--> 0x100000f04 488d3da50000. lea rdi, [rip + 0xa5] ; 0x100000fb0 ; str.Yey__n ; "Yey!." @ 0x100000fb0 | | 0x100000f0b b000 mov al, 0 | | 0x100000f0d e808000000 call sym.imp.printf | | 0x100000f12 8945bc mov dword [rbp - 0x44], eax `-`=< 0x100000f15 e963ffffff jmp 0x100000e7d
El mensaje «Yey!» se puede ver en la linea 46 y «Nice try, buy nope.»en la linea 42.
Analizando cómo llegar a «Yey!», vemos la siguiente parte del código, dónde se llama a strcmp y si las dos cadenas son iguales, salta a la parte del código que hará que se escriba «Yey!»:
| 0x100000ee0 e847000000 call sym.imp.strcmp | 0x100000ee5 83f800 cmp eax, 0 ,==< 0x100000ee8 0f8416000000 je 0x100000f04
Analicemos el resto de llamadas que ocurren en el main del programa:
Primero hay una llamada a printf y luego una a scanf. Sin saber nada de ingeniería inversa, sólo ejecutando el binario podemos imaginar que se trata de la petición de contraseña que aparece al ejecutar el programa:
Después de eso se llama a la función encrypt_arg y por ultimo el resultado de esa función se pasa a strcmp, que lo compara con la cadena que se forma con la ejecución de las siguientes instrucciones:
| 0x100000ea4 c645dd64 mov byte [rbp - 0x23], 0x64 ; [0x64:1]=0 ; 'd' | 0x100000ea8 c645d949 mov byte [rbp - 0x27], 0x49 ; [0x49:1]=0 ; 'I' | 0x100000eac c645de65 mov byte [rbp - 0x22], 0x65 ; [0x65:1]=0 ; 'e' | 0x100000eb0 c645d653 mov byte [rbp - 0x2a], 0x53 ; [0x53:1]=0 ; 'S' | 0x100000eb4 c645df00 mov byte [rbp - 0x21], 0 | 0x100000eb8 c645d765 mov byte [rbp - 0x29], 0x65 ; [0x65:1]=0 ; 'e' | 0x100000ebc c645da6e mov byte [rbp - 0x26], 0x6e ; [0x6e:1]=0 ; 'n' | 0x100000ec0 c645dc69 mov byte [rbp - 0x24], 0x69 ; [0x69:1]=0 ; 'i' | 0x100000ec4 c645d863 mov byte [rbp - 0x28], 0x63 ; [0x63:1]=0 ; 'c' | 0x100000ec8 c645db73 mov byte [rbp - 0x25], 0x73 ; [0x73:1]=69 ; 's' ; "EXT" @ 0x73
Dependiendo del resultado de esa comparación, podremos saltaremos a «Yey!», o no.
Para resolver este ejercicio, necesitamos primero saber cual es la cadena contra la compara strcmp y luego saber que es lo que hace la función encrypt_arg con la password que introducimos antes de pasársela a strcmp. En ese momento podremos generar una cadena que tras ser procesada por encrypt_arg sea igual que el segundo argumento de strcmp.
Solucionando nuestros problemas usando Frida
Normalmente este tipo de cosas se pueden solucionar leyendo ensamblador y reformando la cadena, este es el caso de nuestro binario. Pero otras veces, esta cadena se forma dinámicamente en tiempo de ejecución hay que ir paso a paso con el debugger mirando la cadena que se forma.
O, si lo que queremos es sólo llegar a «Yey!» podríamos parchear la instrucción je 0x100000f04
para convertirla en un salto incondicional. Pero esto se trata de utilizar Frida, así que veamos que podemos hacer con Frida.
Frida nos permite interceptar la función y mandarnos un mensaje con el contenido de los argumentos de la función.
Para ello basta con escribir un modulo en python de este estilo:
import frida import sys session = frida.attach(int(sys.argv[2])) script = session.create_script(""" Interceptor.attach(ptr("%s"), { onEnter: function (args) { send("arg[0]: " + Memory.readCString(args[0])); send("arg[1]: " + Memory.readCString(args[1])); } }); """ % int(sys.argv[1], 16)) def on_message(message, data): print message['payload'] script.on('message', on_message) script.load() sys.stdin.read()
Este script va a interceptar la llamada en la dirección que le pasemos como primer parámetro, del proceso cuyo PID pasemos como segundo parámetro.
La dirección de la llamada función se puede sacar de muchas maneras, pero en el video de demostración que podéis ver a continuación, he usado radare2 para depurar el proceso y encontrar la dirección.
Ya sabemos entonces que la cadena que introducimos se procesa de alguna manera y se compara con SecInside. Ahora podríamos analizar la función que «cifra» la cadena (que en este caso es muy fácil, o usar Frida para hacer que strcmp siempre se cumpla.
Para eso tenemos dos opciones: hookear la llamada y modificar los argumentos, o modificar el valor de retorno. Para ambos casos la API de Frida nos lo pone fácil con sus «onEnter» y «onLeave».
En el primer caso querremos que al entrar el primer argumento sea igual que el segundo. Este se consigue muy fácil modificando un poco el script anterior:
import frida import sys session = frida.attach(int(sys.argv[2])) script = session.create_script(""" Interceptor.attach(ptr("%s"), { onEnter: function (args) { args[0] = args[1] send("arg[0]: " + Memory.readCString(args[0])); send("arg[1]: " + Memory.readCString(args[1])); } }); """ % int(sys.argv[1], 16)) def on_message(message, data): print message['payload'] script.on('message', on_message) script.load() sys.stdin.read()
En este caso la parte de los mensajes no es necesaria, pero la he dejado para que se vea claramente lo que está pasando. La clave está en la linea 8, dónde le decimos que al entrar en la función args[0] = args[1]
.
En el segundo caso, lo que haremos será aprovechar onLeave para modificar el valor de salida de strcmp que lo modificaremos para que siempre diga que las cadenas son iguales. Es decir, que siempre devuelva 0:
import frida import sys session = frida.attach(int(sys.argv[2])) script = session.create_script(""" Interceptor.attach(ptr("%s"), { onEnter: function (args) { send("arg[0]: " + Memory.readCString(args[0])); send("arg[1]: " + Memory.readCString(args[1])); }, onLeave: function (retval) { retval.replace(0); } }); """ % int(sys.argv[1], 16)) def on_message(message, data): print message['payload'] script.on('message', on_message) script.load() sys.stdin.read()
Como podéis ver, he utilizado también onEnter, pero la única razón por la que aparece es para registrar la actividad y que se vea un poco mejor lo que está pasando.
A continuación un video con estos dos scripts en acción:
Esto son sólo unos ejemplos simples de las cosas que se pueden hacer con Frida, pero vale que para lo que no lo conocíais, tengáis una idea general de las cosas que permite automatizar con unas pocas lines de JavaScript. Si te ha llamado la atención, pruébalo y cuéntanos que tal te fue en los comentarios. ¡Hasta la próxima!