
Suite de fuzzing in-process basada en Frida con proxy AFL++, modos activo/pasivo independientes y comunicación por memoria compartida para descubrimiento de vulnerabilidades guiado por cobertura de alto rendimiento entre plataformas.
fpicker es un conjunto de herramientas de fuzzing basado en Frida que ofrece una variedad de modos de fuzzing para fuzzing en proceso, como un modo AFL++ o un modo de rastreo pasivo. Debería funcionar en todas las plataformas compatibles con Frida.
Alguna información de contexto y las ideas detrás de fpicker se pueden encontrar en un blogpost que escribí.
Fpicker se basa en esfuerzos anteriores como ToothPicker, que fue desarrollado durante mi tesis de maestría. La mayor parte de fpicker se desarrolló durante horas de trabajo en mi empleador (ERNW).
Requerido para ejecutar fpicker:
frida-core-devkit para la plataforma respectiva que se encuentra en Lanzamientos de Frida en GitHub.
libfrida-core-ios.a, libfrida-core-macos.a o libfrida-core-linux.a.frida-core.h). Guárdalos como frida-core-linux.h o frida-core-ios.h según la plataforma.update_frida_version.sh antes de compilar.Requerido solo cuando se ejecuta en modo AFL++:
CFLAGS="-DUSEMMAP=1".CFLAGS="-DUSEMMAP=1 -DTARGET_OS_IOS".Fpicker se puede compilar para macOS, iOS o Linux. El Makefile actualmente solo admite la compilación para iOS en macOS, pero debería ser totalmente posible compilar fpicker usando un toolchain de iOS en Linux.
Dependiendo del objetivo deseado, ejecute:
make fpicker-macos
make fpicker-ios
make fpicker-linux
para compilar fpicker.
Una vez que fpicker está compilado, el harness de fuzzing debe compilarse a continuación:
Consulte la carpeta de ejemplos para diferentes casos de muestra de fuzzing. El enfoque general es el siguiente:
examples/test/test.js) (consulte aquí para obtener más información sobre los harnesses)frida-compile test.js -o harness.jsAhora fpicker puede comenzar a fuzzear. El comando exacto depende en gran medida de la configuración y el entorno. A continuación se presentan algunos casos de ejemplo. Estos corresponden en su mayoría a los ejemplos en la carpeta de ejemplos.
afl-fuzz -i examples/test-network/in -o ./examples/test-network/out -- \\
./fpicker --fuzzer-mode afl -e attach -p test-network -f ./examples/test-network/harness.js
./fpicker --fuzzer-mode standalone -e attach -p server-process -f harness.js --input-mode cmd \\
--command "./client-send @@" -i indir -o outdir
./fpicker --fuzzer-mode active --communication-mode shm -e attach -p server-process -f harness.js \\
-i indir -o outdir --standalone-mutator cmd --mutator-command "radamsa"
./fpicker --fuzzer-mode passive --communication-mode send -e attach -p server-process -o outdir -f harness.js
./fpicker --fuzzer-mode active -e attach -p test -D remote -o examples/test/out/ -i examples/test/in/ \\
-f fuzzer-agent.js --standalone-mutator cmd --mutator-command "radamsa"
Cada objetivo requiere su propio harness de fuzzing. La parte más importante de este harness es definir la función de entrada de Stalker de Frida, que determina efectivamente en qué punto se inserta la instrumentación. En el modo in-process esto es simple. La función suele ser la que se llama en cada iteración de fuzzing. Sin embargo, también podría ser una diferente.
Una implementación mínima de harness (en modo command) podría ser esta:
// Import the fuzzer base class
const Fuzzer = require("harness/fuzzer.js");
// The custom fuzzer needs to subclass the Fuzzer class to work properly
class TestFuzzer extends Fuzzer.Fuzzer {
constructor() {
// The constructor needs to specify the address of the targeted function and a NativeFunction
// object that can later be called by the fuzzer.
const FUZZ_FUNCTION_ADDR = Module.getExportByName(null, "FUZZ_FUNCTION");
const FUZZ_FUNCTION = new NativeFunction(
FUZZ_FUNCTION_ADDR,
"void", ["pointer", "int64"], {
});
super("test", FUZZ_FUNCTION_ADDR, FUZZ_FUNCTION);
}
}
const f = new TestFuzzer();
exports.fuzzer = f;
Este harness configura la instrumentación para seguir la función FUZZ_FUNCTION. La instrumentación comenzará cuando se ingrese a esta función y se detendrá cuando la función retorne. Esta función debe elegirse con cuidado, ya que es costosa y cuantas más partes (potencialmente sin importancia) del proceso se instrumenten, más lento se vuelve el fuzzer. Por supuesto, esto es una consideración entre velocidad y cobertura deseada. Además, el fuzzer actualmente solo admite funciones que se ingresan solo una vez durante una iteración de fuzzing, es decir, la función no debe ser llamada más de una vez durante un caso de fuzz, de lo contrario, la información de cobertura podría volverse poco confiable.
Cuando se usa el modo in-process, se requiere otra función en el script del fuzzer: el método fuzz. Se llamará en cada iteración. Se llamará con dos parámetros: un puntero a un búfer y la longitud del búfer. Nuestra función objetivo de ejemplo toma dos parámetros: un puntero a un búfer y su longitud. Por lo tanto, podemos pasar los parámetros que recibimos en el método fuzz.
fuzz(payload, len) {
this.target_function(payload, parseInt(len));
}
En modo passive, se debe especificar un callback que procese los datos requeridos. El fuzzer espera recibir un búfer de payload y su longitud. Dependiendo de la función objetivo que se esté fuzzeando, estos datos deben extraerse. En el siguiente ejemplo, nuevamente tenemos una función que tiene dos parámetros: un puntero a un búfer y su longitud. El parámetro args contiene todos los parámetros potenciales que recibe la función objetivo, por lo que el parámetro de longitud (que es el segundo en nuestro caso) se puede acceder con args[1]. Luego leemos el búfer como Uint8Array y lo enviamos de vuelta al fuzzer usando el método sendPassiveCorpus.
passiveCallback(args) {
const len = args[1];
const data = new Uint8Array(Memory.readByteArray(args[0], parseInt(len)));
// this encodes the data and sends it back to the fuzzer
this.sendPassiveCorpus(data, len);
}
En caso de que el objetivo necesite algún tipo de preparación antes de que el fuzzer pueda comenzar, fpicker proporciona un método prepare que se llama durante la inicialización del fuzzer. La preparación podría ser el establecimiento de estado, por ejemplo, instanciando un objeto. Tal función de preparación podría verse así:
prepare() {
// the object can be attached to the fuzzer instance so that it can be used within the
// fuzz() method later on.
this.required_object = call_native_function_that_creates_object();
}
pficker ofrece un gran conjunto de modos y configuraciones que se explican a continuación. La mayoría de estos modos se pueden combinar de diferentes maneras. Al final de esta sección hay una tabla que muestra qué opciones se pueden combinar y cuál es su estado de implementación.
Fpicker tiene tres modos de fuzzing diferentes: Modo AFL++, Modo Activo Independiente y Modo Pasivo Independiente:
Modo AFL++: En el modo AFL++, fpicker actúa como un proxy entre AFL++ y el proceso objetivo. Usando las capacidades de instrumentación de Frida, el mapa de bits de cobertura de AFL se completa mientras el objetivo se fuzzea con datos de entrada generados por AFL++.
Modo Activo Independiente: En el modo activo independiente, el fuzzer utiliza los resúmenes de llamadas de Stalker de Frida para recopilar cobertura en forma de bloques básicos que se ejecutan durante una iteración. Esto no es nuevo y se ha implementado de varias formas antes. Sin embargo, en combinación con otras configuraciones del fuzzer, esto puede tener varios beneficios. También es una buena alternativa si AFL++ no es aplicable o deseable en un entorno o caso determinado.
Modo Pasivo Independiente: El modo pasivo es menos un fuzzer y más un trazador. Esencialmente, hace lo mismo que el modo activo independiente. Sin embargo, no envía sus propias entradas. Simplemente se adjunta a una función determinada y recopila cobertura. Una vez que se observa nueva cobertura, se almacenan tanto la cobertura como la entrada.
Si bien fpicker está diseñado principalmente como un fuzzer en proceso, también admite fuzzing a través de un comando externo. Para esto, fpicker ofrece dos modos de entrada.
Modo de entrada en proceso: En el modo de entrada en proceso, el harness llama directamente a una función especificada en el proceso objetivo. El fuzzer envía el payload al harness y el harness prepara el payload de manera que pueda llamar a la función objetivo.
Modo de entrada CMD: En el modo de entrada por comando, el payload se redirige a un comando externo. Esto es útil cuando es demasiado complejo preparar los parámetros u otro estado al llamar directamente a la función objetivo. La recolección de cobertura aún debe adjuntarse a una función determinada. Quizás hay un cliente al que se le puede suministrar un payload que luego activa la función objetivo.
El modo de comunicación determina cómo el harness inyectado se comunica con el fuzzer. Esto depende en gran medida de la aplicación objetivo. Frida ofrece una API para enviar y recibir mensajes desde el script del agente inyectado. Este tipo de comunicación es bastante costoso. Uno de los factores es que el mensaje transportado debe codificarse en JSON. Por lo tanto, enviar datos binarios es sencillo. Por lo tanto, fpicker ofrece un segundo modo de comunicación sobre memoria compartida. Sin embargo, esto solo funciona si es posible establecer memoria compartida entre el fuzzer y la aplicación objetivo, lo que significa que este modo no se puede usar cuando el objetivo está conectado al host del fuzzer a través de USB. En el modo de entrada CMD, el modo de comunicación solo se refiere a cómo se comunica la información de cobertura de vuelta al fuzzer, no cómo se envía el payload, ya que esto se delega a un comando externo.
Modo de comunicación Send: En el modo de comunicación send, el payload se envía mediante el mecanismo de llamada RPC de Frida. Esto permite que el fuzzer ejecute una función de JavaScript dentro del script del harness inyectado. Esta función dentro del harness puede entonces realizar todas las preparaciones necesarias para llamar a la función objetivo. Una vez que la función objetivo retorna, la recolección de cobertura se detiene y el harness puede señalar al fuzzer que la iteración ha finalizado. Esto se hace enviando la información de cobertura de vuelta al fuzzer usando la API send de Frida.
Modo de comunicación SHM: En el modo de comunicación SHM, el fuzzer y el script del harness se comunican a través de memoria compartida y semáforos. Se utiliza un búfer en memoria compartida para enviar el payload y recibir la información de cobertura. En lugar de enviar y recibir, los dos componentes usan espera y publicación en el semáforo. Dependiendo del sistema y el objetivo, esto introduce bastantes ganancias de rendimiento. Especialmente porque el payload binario se escribe en memoria una vez y no tiene que ser codificado y decodificado o copiado a otras ubicaciones de memoria. Desafortunadamente, este modo a veces conduce a una baja estabilidad cuando se ejecuta con AFL++. No estoy seguro de por qué todavía.
El modo de ejecución puede ser spawn o attach. Esto es bastante autoexplicativo. fpicker puede adjuntarse a un proceso en ejecución o crear un proceso. Una diferencia importante entre los dos modos es que, si el objetivo adjunto falla, fpicker no intentará reiniciarlo.
En modo independiente, fpicker ofrece tres estrategias diferentes de mutación de entrada. Dicho de manera elegante, la mutación de entrada ciertamente tiene mucho margen de mejora.
Mutador independiente NULL: Este mutador no muta el payload y simplemente devuelve una copia del mismo payload. Principalmente para fines de prueba. Por lo demás, no es realmente útil.
Mutador independiente Rand: Un mutador aleatorio muy malo. Todo lo que hace es reemplazar valores aleatoriamente en ubicaciones aleatorias del payload original. No cambia la longitud del payload.
Mutador independiente Personalizado: Este mutador puede llamar a un comando externo para mutar payloads. Escribe el payload en stdin y recibe el payload mutado desde stdout. Debido a su implementación superficial, tiene un impacto significativo en el rendimiento.
Usando la opción de dispositivo -D usb, Frida seleccionará el primer dispositivo USB local, por ejemplo, un iPhone o un teléfono Android.
Con la opción -D remote es posible fuzzear un proceso que se ejecuta en un dispositivo de red. Para esto, el dispositivo remoto debe estar ejecutando frida-server. Como configuración de ejemplo, use SSH con reenvío de puertos para vincular el puerto de escucha predeterminado 27042 de frida-server en el dispositivo remoto a un socket en el cliente local.
ssh -N [email protected] -L 127.0.0.1:27042:127.0.0.1:27042
En un iPhone, también se puede usar iproxy para reenviar el puerto desde una conexión USB.
Esto puede ser especialmente útil si se ejecuta Frida en un puerto no estándar en un dispositivo sin jailbreak con el gadget de Frida. Al trabajar con el gadget de Frida, el único proceso disponible tendrá el nombre Gadget, independientemente del nombre de la aplicación objetivo.
iproxy 27042 27042
Luego use frida-ps para validar la configuración listando los procesos en el dispositivo remoto:
frida-ps -R