
Una biblioteca ligera de instrumentación dinámica
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
## ¿Qué es TinyInst?
TinyInst es una biblioteca ligera de instrumentación dinámica que se puede utilizar para instrumentar únicamente el/los módulo(s) seleccionado(s) del proceso, dejando que el resto del proceso se ejecute de forma nativa. Está pensado para ser fácil de entender, fácil de modificar y fácil de usar con fines de hacking. No está diseñado para ser compatible con todos los objetivos (más sobre esto más adelante).
### ¿Cómo se compara con [DynamoRIO](https://dynamorio.org/) y [PIN](https://software.intel.com/en-us/articles/pintool)?
TinyInst no está pensado como un reemplazo de frameworks de instrumentación complejos como DynamoRIO y PIN, sino más bien como una alternativa para escenarios donde una solución más ligera sea suficiente. TinyInst asume que el objetivo está bien comportado (en el sentido que se explica a continuación), algo que no ocurre con frameworks más complejos. Por lo tanto, probablemente no podrás ejecutar TinyInst con éxito contra malware como [se hizo anteriormente con DynamoRIO](https://www.slideshare.net/MaximShudrak/fuzzing-malware-for-fun-profit-applying-coverageguided-fuzzing-to-find-bugs-in-modern-malware). Por otro lado, si un objetivo no funciona con otros frameworks debido al módulo que no necesita ser instrumentado, y el módulo instrumentado está bien comportado, podría funcionar con TinyInst. Debido a que con TinyInst la mayor parte del proceso se ejecuta de forma nativa, tendrá un tiempo de arranque del proceso más corto y podría superar a otras soluciones en los casos en que el proceso objetivo dedica mucho tiempo a los módulos donde no se necesita instrumentación.
### ¿Cómo se compara con [Mesos](https://github.com/gamozolabs/mesos) y [TrapFuzz](https://github.com/googleprojectzero/p0tools/tree/master/TrapFuzz)?
TinyInst es una solución completa de reescritura de binarios, por lo que se puede cambiar el comportamiento arbitrario en el módulo objetivo. Esto le permite, por ejemplo, extraer cobertura de aristas en lugar de solo bloques básicos. Además, TinyInst no depende de otro software, como IDA Pro, para identificar bloques básicos.
### ¿Qué sistema operativo es compatible con TinyInst?
TinyInst funciona en Windows (x86 y x64), macOS (x64 y ARM64), Linux (x64 y ARM64) y Android (ARM64). Consulta el archivo README en el directorio correspondiente de cada sistema operativo para obtener notas y limitaciones adicionales.
### ¿Qué objetivos son compatibles con TinyInst?
TinyInst asume que todos los módulos instrumentados están bien comportados en el sentido de que
- No hay código automodificable
- La dirección de retorno en la pila nunca es accedida directamente por el programa
O/Y (dependiendo de la configuración)
- Nunca se almacenan datos antes de la parte superior de la pila (en direcciones más bajas que las apuntadas por ESP/RSP). Esta condición se puede relajar a "ningún dato antes de (ESP/RSP - arbitrary_offset)" utilizando la opción `-stack_offset`.
TinyInst también requiere que DEP/NX esté habilitado para el proceso objetivo. Si ese no es el caso, puedes utilizar la opción `-force_dep` para forzarlo. Sin embargo, en el improbable caso de que el objetivo realmente necesite DEP desactivado para funcionar correctamente, forzarlo podría hacer que se comporte de manera incorrecta.
### ¿Cuál es la sobrecarga de rendimiento?
Según mediciones tempranas en decodificación de imágenes, en un objetivo de 64 bits bien comportado con la configuración predeterminada de TinyInst, la sobrecarga de rendimiento fue de alrededor del 15 % sin un cliente y de aproximadamente el 20 % con el cliente de ejemplo que recopila cobertura. Ten en cuenta que esto no incluye el tiempo de espera (timeout) introducido por la instrumentación inicial de los módulos. Consulta los consejos de rendimiento a continuación para obtener más detalles.
## Compilación de TinyInst
1. Abre una terminal y configura tu entorno de compilación (p. ej., en Windows, ejecuta vcvars64.bat / vcvars32.bat)
2. Navega hasta el directorio que contiene el código fuente
3. Ejecuta los siguientes comandos (cambia el generador según la versión del IDE y la plataforma para la que quieras compilar):
#### Windows```
mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Release
mkdir build cd build cmake -G Xcode .. cmake --build . --config Release
#### Linux```
mkdir build
cd build
cmake ..
cmake --build . --config Release
mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE=</path/to/android/ndk>build/cmake/android.toolchain.cmake -DANDROID_NDK=</path/to/android/ndk> -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM= .. cmake --build . --config Release
Nota #1: la compilación de 64 bits también funcionará con objetivos de 32 bits en sistemas operativos Windows y Linux
Nota #2: ¿Tienes problemas para crear una compilación de 32 bits en Windows de 64 bits porque el entorno no está configurado correctamente y faltan bibliotecas? Abre el archivo .sln generado en Visual Studio y compílalo desde allí en lugar de ejecutar cmake --build. Ten en cuenta también que la compilación de 64 bits funcionará con objetivos de 32 bits, por lo que puede que no sea necesario crear una compilación de 32 bits.
## Uso de TinyInst
TinyInst está pensado principalmente para usarse como una biblioteca dentro de otros programas.
Un cliente de TinyInst se implementa como una subclase de la clase TinyInst. El cliente puede sobrescribir los métodos de la API que necesite. Los métodos de la API se definen a continuación.
Después de crear el cliente, debe inicializarse con las opciones de línea de comandos llamando a
`void init(int argc, char **argv);`
Las opciones de línea de comandos se definen a continuación y un cliente también puede definir las suyas propias. Después de eso, para ejecutar y controlar un programa instrumentado, se pueden usar las siguientes funciones.
`DebuggerStatus Run(int argc, char **argv, uint32_t timeout);`
`DebuggerStatus Attach(unsigned int pid, uint32_t timeout);`
Estas funciones ejecutan un programa (usando la línea de comandos especificada) o se adjuntan a un programa ya en ejecución. Si no se especifica un método objetivo, el objetivo continuará ejecutándose hasta que el programa salga, el programa falle o expire el tiempo de espera (expresado en milisegundos). Si se define un método objetivo, TinyInst retornará cada vez que se entre al método objetivo y cada vez que el método objetivo retorne, lo que permite a quien lo invoca realizar tareas adicionales.
Cuando `Run` y `Attach` retornan mientras el proceso objetivo sigue vivo, se pueden usar las siguientes funciones para terminar el proceso o continuar la ejecución.
`DebuggerStatus Kill();`
`DebuggerStatus Continue(uint32_t timeout);`
TinyInst incluye un binario de cobertura de ejemplo, que se puede invocar usando
`<options> -- <target command line>`
Ejemplo en Windows:
`litecov.exe -instrument_module notepad.exe -coverage_file coverage.txt -- notepad.exe`
## API de instrumentación
### Devoluciones de llamada de eventos del depurador
Estas devoluciones de llamada son solo informativas y el cliente no debe emitir ningún código instrumentado durante ellas. Los clientes deben llamar al mismo manejador definido en la superclase antes de gestionar estos eventos por sí mismos.
`OnProcessCreated`
Llamada cuando se crea o se adjunta el proceso objetivo.
`OnProcessExit`
Llamada cuando el proceso objetivo sale.
`OnProcessEntrypoint`
Llamada cuando se alcanza el punto de entrada del proceso (binario principal)
`OnTargetMethodReached`
Si el método objetivo está definido, se llama cuando se alcanza el método objetivo por primera vez.
`OnModuleLoaded`
Llamada cuando se carga un módulo. Se llama para cada módulo, no solo para los instrumentados.
`OnModuleUnloaded`
Llamada cuando se descarga un módulo. Se llama para cada módulo, no solo para los instrumentados.
`OnException`
Llamada cuando se encuentra una excepción. El cliente debe retornar true (si la excepción fue manejada) o el resultado del mismo método en la clase padre.
### Devoluciones de llamada de instrumentación
Durante estas devoluciones de llamada, el cliente puede añadir código al objetivo llamando a `WriteCode()`. Ten en cuenta que el cliente es responsable de guardar y restaurar cualquier contexto (como registros y banderas modificados en el código insertado).
`InstrumentBasicBlock`
Puede usarse para insertar código que se ejecutará en un bloque básico concreto.
`InstrumentEdge`
Puede usarse para insertar código que se ejecutará en una arista concreta. Nota: Por razones de rendimiento, esta devolución de llamada solo se emite en aristas no deterministas (es decir, saltos condicionales) y saltos/llamadas indirectas (p. ej. `call rax`). Para aristas donde el siguiente bloque básico siempre se conoce dado el bloque básico anterior (p. ej. `jmp offset`, `call offset`), no se emitirá ninguna devolución de llamada.
`InstrumentInstruction`
Puede usarse para modificar la instrucción o insertar código antes de ella. Dependiendo del código de retorno, la instrucción original se emitirá o no después de la devolución de llamada.
### Otras devoluciones de llamada
`OnModuleEntered`
Llamada cuando un flujo de control se transfiere a un módulo instrumentado desde otro módulo
`OnModuleInstrumented`
Llamada cuando un módulo es instrumentado. Esto sucede generalmente cuando se alcanza el punto de entrada del proceso (si el método objetivo no está definido) o cuando se alcanza el método objetivo (si está definido). El cliente puede inicializar aquí sus datos relacionados con la instrumentación
`OnModuleUninstrumented`
Llamada cuando los datos de instrumentación ya no son válidos y deben limpiarse. Ten en cuenta que esto no es lo mismo que la descarga de un módulo, ya que, de forma predeterminada, la instrumentación persiste entre descargas/recargas de módulos. Esta devolución de llamada puede usarse para limpiar cualquier dato relacionado con la instrumentación en el cliente.
### API de enganche (Hook)
Además de la API de propósito general documentada anteriormente, TinyInst también implementa una API de enganche que es más adecuada para inspeccionar y modificar el comportamiento de funciones individuales. Esta API está documentada en una [página separada](https://github.com/googleprojectzero/TinyInst/blob/master/hook.md).
## Opciones de línea de comandos
### Relacionadas con la instrumentación
`-instrument_module [module name]` especifica qué módulo instrumentar, se pueden especificar varias opciones `-instrument_module` para instrumentar múltiples módulos.
`-instrument_transitive [module name]` similar a `-instrument_module` excepto que solo el código entrado desde otros módulos instrumentados se ejecutará instrumentado. Se usa principalmente como optimización para llamadas como module1->module2->module1 donde no es importante instrumentar todo el módulo module2, pero las entradas module2->module1 están causando ralentizaciones.
`-indirect_instrumentation [none|local|global|auto]` qué instrumentación usar para saltos/llamadas indirectas
`-patch_return_addresses` - reemplaza la dirección de retorno con el valor original, hace que los retornos se instrumenten usando el método `-indirect_instrumentation` que se especifique
`-generate_unwind` - Genera datos de desenrollado de pila para el código instrumentado (para un manejo más rápido de excepciones de C++). Ten en cuenta que podría no funcionar correctamente en algunas versiones antiguas de Windows.
`-persist_instrumentation_data` (predeterminado = true) No vuelve a instrumentar el módulo en descargas/recargas de módulos. Solo funciona si el módulo se carga en la misma dirección en la que se cargó antes.
`-instrument_cross_module_calls` (predeterminado=true) Si se especifican múltiples módulos `-instrument_module` y uno llama a otro, salta al código instrumentado del otro módulo sin provocar una excepción (lo que causaría ralentizaciones).
`-stack_offset` (predeterminado=0) Al guardar el contexto en la pila, deja sin cambios esta cantidad de bytes en la parte superior de la pila (antes del puntero de pila).
`-patch_module_entries [off|data|code|all]` Intenta resolver las ralentizaciones debidas a entradas excesivas de módulos buscando punteros a puntos de entrada detectados previamente y reemplazándolos con sus contrapartes instrumentadas. El valor del indicador controla dónde buscar estos punteros. Advertencia: Activar esto podría introducir inestabilidades en el objetivo.
### Relacionadas con la depuración
`-trace_debug_events` - imprime eventos del depurador (módulos cargados, excepciones, etc.)
`-trace_basic_blocks` - imprime los bloques básicos a medida que se ejecutan
`-trace_module_entries` - imprime todas las entradas al código instrumentado
`-trace_syscalls` - [solo Linux/Android] Permite al cliente recibir eventos de inicio/fin de llamadas al sistema (syscall) mediante las devoluciones de llamada `OnSyscall()` / `OnSyscallEnd()`.
`-full_address_map` - Mantiene un mapa a nivel de instrucción de las direcciones del código instrumentado a las direcciones del código original. Consume mucha memoria, pero es útil para depurar.
### Método objetivo y persistencia
TinyInst permite al usuario definir un método objetivo. Si se define un método objetivo, no se instrumentará ningún código (todo se ejecutará de forma nativa) hasta que se alcance el método objetivo por primera vez. Además, TinyInst detendrá la ejecución en la entrada y salida del método objetivo.
`-target_module` - módulo que contiene el método objetivo
`-target_method` - nombre del método objetivo. Esto solo funciona si el método objetivo está exportado o si tienes símbolos para el módulo objetivo.
`-target_offset` - se usa cuando el método objetivo no puede especificarse por nombre. Dirección relativa del método objetivo desde la base del módulo
`-loop` - si se especifica este indicador, TinyInst ejecutará el método objetivo en un bucle infinito (o hasta que se llame a Kill() o el proceso termine por otra razón). Los argumentos de la función se guardarán y restaurarán entre iteraciones. Esto se usa principalmente para forzar la persistencia en el fuzzing.
`-nargs` - número de argumentos del método objetivo a guardar entre iteraciones. Para usarse junto con `-loop`
`-callcon [ms64|stdcall|fastcall|thiscall]` - convención de llamada que usa el método objetivo. Para usarse junto con `-loop`
### Otros
`-target_env key=value` - [actualmente solo macOS y Linux/Android] especifica una variable de entorno adicional para pasar al proceso objetivo. Se pueden especificar múltiples opciones `-target_env` para pasar múltiples variables de entorno.
`-force_dep` - [solo Windows] Fuerza la habilitación de DEP para el proceso objetivo.
## Módulo de cobertura
TinyInst incluye un módulo de cobertura (de ejemplo), `LiteCov`. El módulo de cobertura puede recopilar cobertura de bloques básicos o de aristas (controlada mediante el indicador `-covtype`). Además de esto, el módulo puede extraer cobertura de "comparación" (contando el número de bytes que coinciden en instrucciones cmp/sub) especificando el indicador `-cmp_coverage`.
Una característica especial del módulo de cobertura es que el búfer de cobertura en el proceso objetivo se asigna inicialmente como de solo lectura, lo que provoca una excepción la primera vez que se encuentra cobertura nueva. Combinado con una opción para ignorar un subconjunto determinado de cobertura, esto permite consultar rápidamente si ejecutar el objetivo con una entrada dada resultó en cobertura nueva o no.
## ¿Cómo funciona TinyInst?
TinyInst está construido sobre un depurador personalizado. El depurador observa el proceso objetivo para detectar eventos como la carga de módulos, la activación de puntos de interrupción, el lanzamiento de excepciones, etc. El depurador también implementa puntos de interrupción y persistencia si se especifica el método objetivo.
Cuando se carga un módulo que se va a instrumentar, se "instrumenta" inicialmente de la siguiente manera
- Todas las regiones ejecutables del módulo se marcan como no ejecutables, manteniendo los demás permisos (lectura/escritura) tal como estaban originalmente. Esto provoca una excepción cada vez que el flujo de control llega a un módulo instrumentado, la cual es capturada y manejada por el depurador.
- Se asigna una región de memoria ejecutable dentro de los 2GB del rango de direcciones del módulo original. Aquí es donde se colocará el código instrumentado/reescrito del módulo. 2GB es importante porque permite que todas las instrucciones que usan direccionamiento en la forma [rip+offset] se reemplacen con [rip+fixed_offset].
Cada vez que se entra a un módulo instrumentado (ya sea por primera vez o en cualquier otro momento), se instrumenta el bloque básico que se alcanzó, junto con todos los bloques básicos que pueden descubrirse de manera confiable siguiendo recursivamente ramas condicionales, así como llamadas y saltos directos (p. ej. jmp offset, call offset).
Esto es suficiente para ejecutar el código instrumentado porque
- todos los saltos/llamadas directos aterrizarán en el código instrumentado en la ubicación correcta
- todos los saltos/llamadas indirectos (p. ej. call rax) aterrizarán en su ubicación de código original, lo que provoca una excepción, que el depurador resuelve reemplazando el puntero de instrucción con la ubicación correspondiente en el código instrumentado.
Sin embargo, aunque esto funciona, ten en cuenta que provocará una excepción en cada llamada/salto indirecto cuyo objetivo esté en un módulo instrumentado. Dado que el manejo de excepciones es lento, instrumentar objetivos con mucha indirección (p. ej. métodos virtuales en C++, punteros a función) será lento sin instrumentación adicional.
### Instrumentación de llamadas y saltos indirectos
TinyInst puede instrumentar llamadas y saltos indirectos para evitar excepciones en objetivos indirectos (ya vistos). Una llamada/salto instrumentado, en lugar de saltar al objetivo original, saltará a la cabeza de la lista enlazada de stubs. Cada stub contiene un par de (original_target, translated_target). Comprueba si el objetivo del salto/llamada coincide con original_target y, si es así, el flujo de control se dirige a translated_target. De lo contrario, salta al siguiente stub. Si se llega al final de la lista, eso significa que el objetivo del salto/llamada no se ha visto antes. Esto provocará un punto de interrupción que es capturado por el depurador, el cual se resolverá creando otro stub e insertándolo en la lista.
Este mecanismo puede implementarse de 2 maneras
- lista por sitio de llamada (local)
- tabla hash global utilizada por todos los saltos/llamadas indirectos
La tabla hash global da como resultado un mejor rendimiento. La local (lista por sitio de llamada) permite obtener aristas correctas (con la dirección de origen correcta) en llamadas/saltos indirectos.
Ten en cuenta que en Windows moderno, debido a CFG, todos los saltos/llamadas indirectos ocurren desde la misma ubicación, por lo tanto, con binarios compilados con CFG, es imposible (sin algún tipo de manejo especial) obtener aristas precisas de todos modos. Esto, junto con el beneficio de rendimiento, es la razón por la que la lista hash global es el método predeterminado para manejar llamadas/saltos indirectos en TinyInst.
### Parcheo de direcciones de retorno
De forma predeterminada, cuando se produce una llamada en código instrumentado, la dirección de retorno que se escribe será la siguiente instrucción en el *código instrumentado*. Esto funciona correctamente en la mayoría de los casos; sin embargo, causará problemas si el proceso objetivo accede alguna vez a las direcciones de retorno para fines distintos del retorno. Un ejemplo notable de esto es el desenrollado de pila durante el manejo de excepciones en sistemas operativos de 64 bits. Por lo tanto, los objetivos que necesitan capturar excepciones no funcionarán correctamente con TinyInst de forma predeterminada.
Esto puede resolverse en la mayoría de los casos añadiendo el indicador `-generate_unwind`, que hace que TinyInst genere y registre metadatos de desenrollado de pila / manejo de excepciones para el proceso objetivo. Ten en cuenta que `-generate_unwind` podría no funcionar correctamente en algunas versiones antiguas de Windows debido a que requiere la versión 2 de UNWIND_INFO.
TinyInst también tiene una opción (expuesta a través del indicador `-patch_return_addresses`) para reescribir las direcciones de retorno a sus valores correspondientes en el código no instrumentado cada vez que se produce una llamada. Ten en cuenta, sin embargo, que esta opción introduce una sobrecarga bastante grande, ya que provoca un cambio de contexto en cada retorno (arista hacia atrás) desde un módulo no instrumentado hacia un módulo instrumentado.
## Consejos de rendimiento
La mayor sobrecarga en TinyInst proviene de que se lanza una excepción cada vez que se entra a un módulo instrumentado desde un módulo no instrumentado. Puedes ver estas excepciones activándose usando el indicador `-trace_module_entries`. La instrumentación de saltos/llamadas indirectos debe usarse siempre que sea posible y la instrumentación de retornos no debe usarse siempre que sea posible. TinyInst funciona mejor en módulos (o grupos de módulos) que son razonablemente autocontenidos. Por ejemplo, si tienes dos módulos, A y B, donde A llama a B con frecuencia pero solo B está instrumentado, esto causará mucha ralentización. Se podría lograr un mejor rendimiento instrumentando tanto A como B.
## Consejos de depuración
Usa `-trace_basic_blocks` para ver los bloques básicos a medida que se ejecutan. Verás tanto las direcciones en el código instrumentado como las direcciones correspondientes en el código no instrumentado.
Usa la devolución de llamada OnException() para examinar el estado del programa cuando se produce el fallo.
## Aviso legal
Este no es un producto oficial de Google.