Oscar Llorente – S3lab http://s3lab.deusto.es S3lab Security Blog Wed, 06 May 2020 12:51:35 +0000 es hourly 1 https://wordpress.org/?v=5.1.5 La carga de binarios en Linux http://s3lab.deusto.es/carga-binarios-linux/ Sun, 14 Oct 2018 16:36:42 +0000 http://s3lab.deusto.es/?p=10045 Se puede decir que los ejecutables son una representación estática de un programa y que en el momento en el que se ejecutan, el kernel utiliza la información incluida en esos ficheros para crear una representación dinámica, más conocida como

The post La carga de binarios en Linux appeared first on S3lab.

]]>
Se puede decir que los ejecutables son una representación estática de un programa y que en el momento en el que se ejecutan, el kernel utiliza la información incluida en esos ficheros para crear una representación dinámica, más conocida como la imagen del proceso. Antes de poder ejecutar un binario es necesario cargarlo en memoria y el encargado de hacerlo es el loader, que generalmente es parte del sistema operativo.

En los sistemas basados en Linux, que utilizan el estándar ELF  como formato binario, la imagen del proceso se crea cargando e interpretando los segmentos especificados en las cabeceras. Pero antes de hablar sobre el proceso de carga es necesario saber que al crear un ejecutable que utiliza dynamic linking (que son la mayoría de los programas), las bibliotecas compartidas necesarias se ubican y se enlazan en tiempo de ejecución, por lo que el linker añade un campo a las cabeceras del programa del ejecutable de tipo PT_INTERP, indicando al sistema la identidad del linker dinámico (a veces también llamado intérprete) que típicamente es /lib64/ld-linux-x86-64.so.2.  Por otra parte, en el periodo conocido como link-time, el programa (o biblioteca) se construye combinando secciones con atributos similares en segmentos. Normalmente, todas las secciones ejecutables y de datos de sólo-lectura se juntan en un mismo segmento, mientras que el resto de secciones de datos y BSS (variables no inicializadas) se combinan en otro segmento. Estos segmentos típicamente se conocen como segmentos de carga porque es necesario cargarlos en memoria en el momento de la creación del proceso, al contrario que otras secciones como la tabla de símbolos (.symtab) o la información de debugging (.debug_info).

A grandes rasgos, el proceso de carga de un binario ELF se descompone en las siguientes tareas:

  • 1. En primer lugar, se examinan las cabeceras para verificar que el fichero en cuestión tiene un formato ELF compatible.
  • 2. Se recorren las entradas de las cabeceras del programa comprobando si se ha especificado un intérprete (PT_INTERP).
  • 3. Para establecer la memoria virtual, se recorren todos los segmentos de tipo PT_LOAD en el fichero y se mapean dentro del espacio de direcciones del proceso. Después se configuran las páginas inicializadas con ceros correspondientes al segmento BSS. También se configuran otras páginas especiales como las páginas de objetos compartidos dinámicos virtuales (vDSO).
  • 4. Una vez que el código del programa se ha cargado en memoria y como en este caso suponemos que utiliza dynamic linking, el ELF handler también carga el intérprete en memoria. El proceso es prácticamente el mismo que el seguido para cargar el programa original y que hemos explicado en el punto anterior.
  • 5. Por último, la dirección de inicio del programa se configura como el punto de entrada del intérprete en lugar del programa en sí mismo. Esto implica que la ejecución empieza con el intérprete, que se encarga de satisfacer los requisitos de enlace del programa desde el espacio de usuario, es decir, busca y carga las bibliotecas compartidas de las que depende el programa y trata de resolver los símbolos no definidos a las definiciones correctas en dichas bibliotecas. Una vez se ha finalizado este proceso el intérprete puede iniciar la ejecución.

Para una explicación más detallada de este proceso con referencias al código del kernel por favor visitar el arituclo de LWN.net.

The post La carga de binarios en Linux appeared first on S3lab.

]]>
Instrumentación dinámica de binarios http://s3lab.deusto.es/instrumentacion-dinamica-binarios/ Fri, 06 Jul 2018 12:53:11 +0000 http://s3lab.deusto.es/?p=9952 En el post anterior hablamos sobre las posibilidades para instrumentar programas y llevar a cabo todo tipo de tareas como profiling o detección de vulnerabilidades. También introdujimos Intel Pin, una herramienta de instrumentación dinámica de binarios (principalmente para IA32 y

The post Instrumentación dinámica de binarios appeared first on S3lab.

]]>
En el post anterior hablamos sobre las posibilidades para instrumentar programas y llevar a cabo todo tipo de tareas como profiling o detección de vulnerabilidades. También introdujimos Intel Pin, una herramienta de instrumentación dinámica de binarios (principalmente para IA32 y x86_64), sobre la que vamos a hablar en esta entrega. Al igual que un debugger, Pin puede lanzar una aplicación o se puede fijar a un proceso en ejecución, intrumentarlo como sea necesario, recoger la información de interés y separarse en cualquier momento para que pueda seguir con su ejecución normal. Para tener control sobre el proceso instrumentado utiliza llamadas a ptrace() (en Linux) que ya vimos en un post anterior. La arquitectura general de Intel Pin es la que se muestra en la figura pero para más detalle recomiendo leer el artículo original.

La instrumentación se realiza a través de un compilador JIT. Pin compila el código de la aplicación desde una ISA directamente a la misma sin pasar por una representación intermedia. Las unidades de traducción son trazas, por lo que se compila una traza cada vez, que consisten en una secuencia lineal de instrucciones que termina en: (1) una transferencia de control incondicional, (2) un número predefinido de transferencias de control condicionales, o (3) un número predefinido de instrucciones en una misma traza. El único código que se ejecuta es el generado por Pin, utilizando el original solamente como referencia, por lo que cada vez que el compilador JIT obtiene código, la Pintool (que es como se llaman las herramientas que utilizan Pin) tiene la oportunidad de instrumentarlo antes de ser traducido para su ejecución, guardando el código generado y el de instrumentación en una caché de código.

Todo el software necesario se puede descargar desde la web oficial. Una vez descomprimido, un buen punto de partida es echar un vistazo a los ejemplos disponibles en source/tools/ManualExamples entre los que se encuentra por ejemplo un contador de instrucciones: inscount0.cpp. Si nos fijamos en la función main, además de otras llamadas a funciones, la que más nos interesa en este caso es INS_AddInstrumentFunction, que recibe como parámetros un callback de instrumentación a llamar por cada instrucción y sus respectivos parámetros.

En la implementación de la función Instruction, a través de INS_InsertCall se añade una llamada a la función docount() que simplemente incrementa en 1 una variable global que almacena el número total de instrucciones.

El segundo parámetro de la llamada (IPOINT_BEFORE) resulta interesante puesto que nos permite especificar cuando insertar la llamada a docount(), siendo las alternativas IPOINT_AFTER y IPOINT_TAKEN_BRANCH, aunque en este caso IPOINT_BEFORE es la mejor opción porque solo estamos contando instrucciones. Este es el ejemplo más simple dentro de todos los que nos proporciona Intel, por lo que antes de empezar a desarrollar nuestra Pintool es recomendable examinar el resto de ejemplos.

The post Instrumentación dinámica de binarios appeared first on S3lab.

]]>
Opciones de instrumentación de programas http://s3lab.deusto.es/instrumentacion-de-programas/ Mon, 14 May 2018 13:00:34 +0000 http://s3lab.deusto.es/?p=9869 Muchos de los métodos de software testing y análisis dinámico de programas (no necesariamente relacionados con seguridad) requieren insertar algunas instrucciones adicionales en el texto del programa para obtener información añadida, es decir, instrumentar el programa. Por ejemplo, una de

The post Opciones de instrumentación de programas appeared first on S3lab.

]]>
Muchos de los métodos de software testing y análisis dinámico de programas (no necesariamente relacionados con seguridad) requieren insertar algunas instrucciones adicionales en el texto del programa para obtener información añadida, es decir, instrumentar el programa. Por ejemplo, una de las opciones a la hora de medir el rendimiento de una aplicación es añadir sentencias para leer el reloj al principio y al final de cada función de forma que se pueda calcular el tiempo que tarda en ejecutarse cada una y después optimizar las que tarden más de lo debido. Por poner otro ejemplo, dentro del proceso de software testing es un hábito muy común tener métricas de cobertura de código, dicho de otra forma, medir cuanto código está siendo realmente testeado. Para ello, si quisieramos comprobar si nuestros tests ejecutan todos los puntos de ramificación posibles (branch coverage), se podrían añadir al código sentencias que contasen el número de veces que se ha tomado cada rama (cuando la condición es true y cuando es false).

Por lo tanto, instrumentación se refiere a la técnica que consiste en añadir código extra a un programa, normalmente con el objetivo de recoger información sobre su comportamiento durante la ejecución y enviarla a rutinas de análisis que se encargan de manipular dicha información para llevar a cabo tareas que van desde profiling, detección de errores o debugging, hasta análisis de malware.

La instrumentación se puede llevar a cabo desde distintos niveles: directamente en el código fuente, en una representación intermedia como bytecode o LLVM IR, o a nivel de binario. Como curiosidad, en los últimos años se han desarrollado varias herramientas de detección de errores como MemorySanitizer que implementan una fase de instrumentación sobre la representación intermedia en tiempo de compilación.

Por otra parte, también es importante mencionar que la instrumentación puede ser estática o dinámica. En una situación en la que solo se dispone de un binario, se puede instrumentar de forma estática mediante binary rewriting como lo hacen herramientas como PEBIL, se puede hacer de forma dinámica sobrescribiendo instrucciones en memoria por trampolines que saltan al código de instrumentación, o siguiendo el concepto de Dynamic Binary Translation (de una ISA a la misma) como lo hace Intel Pin, por ejemplo. Cada uno de los métodos tiene sus ventajas e inconvenientes dependiendo de la tarea que se quiera llevar a cabo (analizar malware tiene requisitos de transparencia que algunas soluciones no pueden cumplir) y de lo recursos disponibles (no vamos a instrumentar el código fuente si no está disponible).

The post Opciones de instrumentación de programas appeared first on S3lab.

]]>
Empezando a depurar codigo (II) – Breakpoints http://s3lab.deusto.es/depurando-codigo-2/ Fri, 16 Mar 2018 10:55:10 +0000 http://s3lab.deusto.es/?p=9681 En el post anterior introdujimos cómo los depuradores pueden ganar control y acceder a información de bajo nivel en el proceso objetivo mediante la llamada al sistema ptrace(). En esta ocasión vamos a tratar una de las funciones más útiles

The post Empezando a depurar codigo (II) – Breakpoints appeared first on S3lab.

]]>
En el post anterior introdujimos cómo los depuradores pueden ganar control y acceder a información de bajo nivel en el proceso objetivo mediante la llamada al sistema ptrace(). En esta ocasión vamos a tratar una de las funciones más útiles que proporcionan los depuradores, la de establecer breakpoints. En general, un breakpoint es una ubicación designada por el usuario en el programa donde el usuario desea recuperar el control y examinar el estado del programa si la ejecución llega alguna vez a esa ubicación. Los breakpoints pueden ser tanto hardware como software. GDB puede establecer breakpoints hardware de tres maneras distintas:

  • 1. A través de registros dedicados en los que se almacena la dirección del punto de interrupción y después se comprueba si el contador de programa coincide con el valor de alguno de los registros.
  • 2. Cuando se está utilizando un emulador que incluye circuitos que monitorizan las líneas de direcciones de salida del procesador y detienen la ejecución si coinciden con la dirección de un punto de interrupción.
  • 3. Cuando el objetivo tiene la habilidad propia de establecer breakpoints, como por ejemplo, un monitor ROM que es capaz de establecer sus propios breakpoints software, que aunque no sean breakpoints hardware literales, para GDB si lo son.

Como los breakpoints hardware no están siempre disponibles, se necesita un método para establecer breakpoints software, a grandes rasgos:

  • 1. GDB guarda internamente la instrucción original en la dirección del punto de interrupción y la reemplaza por una interrupción (por ejemplo, INT3 en x86) o una instrucción inválida (por ejemplo, una división ilegal) que cause una excepción.
  • 2. GDB recibe la señal de que ha ocurrido una excepción (SIGCHLD) y comprueba si la dirección del contador de programa está en la lista de breakpoints, si es así, se trata de un punto de interrupción, si no, es un fallo producido por el programa.
  • 3. El depurador se encuentra detenido en el punto de interrupción y el usuario realiza las operaciones que considere necesarias.
  • 4. Para reanudar la ejecución, GDB recupera la instrucción original, ejecuta la instrucción en modo single-step, vuelve a introducir la interrupción y, por último, continúa con la ejecución normal.

GDB proporciona otra funcionalidad además de los breakpoints. Echar un ojo a la documentación.

The post Empezando a depurar codigo (II) – Breakpoints appeared first on S3lab.

]]>
Empezando a depurar codigo (I) http://s3lab.deusto.es/depurando-codigo-1/ Wed, 31 Jan 2018 16:23:27 +0000 http://s3lab.deusto.es/?p=9667 Los errores son un fenómeno colateral (e indeseado) que se produce al escribir código. Algunos, como los errores sintácticos, son fáciles de detectar y corregir ya que normalmente el compilador, el intérprete o el propio IDE nos avisará de que

The post Empezando a depurar codigo (I) appeared first on S3lab.

]]>
Los errores son un fenómeno colateral (e indeseado) que se produce al escribir código. Algunos, como los errores sintácticos, son fáciles de detectar y corregir ya que normalmente el compilador, el intérprete o el propio IDE nos avisará de que se nos ha olvidado cerrar un paréntesis, por ejemplo. Sin embargo, otros errores pueden producir intensos dolores de cabeza, como un fallo de segmentación en algún lugar dentro de miles de líneas de código que se desencadena bajo una serie de condiciones específicas.

Existen varias formas de lidiar con los errores, probablemente el método más utilizado es añadir llamadas a la función print() (en todas sus variantes) en distintas partes del código para poder seguir la ejecución del programa y comprobar que valores toman las variables en determinados momentos. Pero también existen herramientas conocidas como depuradores que, a pesar de que a personas como Linus Torvalds no le entusiasmen demasiado, ofrecen funcionalidad útil para combatir errores complejos, y no solo eso, sino que también son una herramienta potente en el análisis de programas con distintos objetivos. En sistemas Unix, el depurador por excelencia es GDB, que permite hacer cuatro cosas principalmente: iniciar un programa especificando parámetros que puedan afectar su comportamiento, detener un programa en determinadas condiciones y momentos, inspeccionar que está ocurriendo dentro del programa, y modificar distintos valores del programa para observar como afecta a su comportamiento. Pero, ¿como puede un depurador como GDB tener control sobre otro proceso?

La funcionalidad de observar y controlar la ejecución de otro proceso se proporciona mediante la llamada al sistema ptrace(), pero para ello primero es necesario adjuntar el proceso «rastreador» (el depurador GDB en este caso) al proceso «rastreado«. A grandes rasgos, la primera forma de conseguirlo es ejecutando el programa desde GDB, de forma que se haga un fork() del proceso y se establezca una relación directa padre-hijo. Por otra parte, también existe la posibilidad de asumir el rol de proceso padre y adjuntarse a un proceso en ejecución mediante la operación PTRACE_ATTACH. Las principales operaciones (entre otras) que ptrace permite realizar sobre el proceso rastreado son:

  • Leer contenidos de la memoria (PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER) y escribir en memoria (PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER).
  • Obtener/leer contenidos en los registros (PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETREGSET) y escribir en los registros (PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETREGSET).
  • Detener la ejecución (PTRACE_INTERRUPT) y forzar la ejecución de instrucciones una por una (PTRACE_SINGLESTEP).

The post Empezando a depurar codigo (I) appeared first on S3lab.

]]>
La idea detrás de la ejecución simbólica http://s3lab.deusto.es/idea-ejecucion-simbolica/ Tue, 07 Nov 2017 09:31:06 +0000 http://s3lab.deusto.es/?p=9545 Siguiendo en la línea de técnicas de análisis de programas, en esta ocasión vamos a hablar sobre una técnica que se encuentra en algún punto intermedio entre las técnicas estáticas y las dinámicas, la ejecución simbólica. Es un tipo de

The post La idea detrás de la ejecución simbólica appeared first on S3lab.

]]>
Siguiendo en la línea de técnicas de análisis de programas, en esta ocasión vamos a hablar sobre una técnica que se encuentra en algún punto intermedio entre las técnicas estáticas y las dinámicas, la ejecución simbólica. Es un tipo de análisis que cubre distintas limitaciones del análisis estático y dinámico y proporciona una solución para hacer frente a la limitada visión semántica del fuzzing, por lo que es utilizado por una enorme cantidad de aplicaciones de seguridad, como por ejemplo en detección de vulnerabilidades, análisis de malware o generación de casos de prueba.

La idea principal detrás de la ejecución simbólica reside en ejecutar el programa objetivo de forma simbólica, es decir, los valores de entrada y las variables se representan como valores simbólicos en lugar de valores concretos. Estos valores se usan para generar condiciones de ruta, que son fórmulas lógicas que representan el estado del programa y las transformaciones entre estados del programa.

Para implementar la ejecución simbólica es necesario un dominio simbólico. Este dominio consiste en un estado simbólico que asigna variables a expresiones simbólicas y una condición de ruta (PC) para cada camino. Inicialmente, el estado simbólico es un mapa vacío y la ejecución simbólica solo mantiene una condición de ruta configurada como verdadero. La ejecución de sentencias condicionales actualizan la fórmula de condición de ruta, mientras que las asignaciones actualizan el estado simbólico. Cuando se alcanza un punto de bifurcación (producido por una sentencia condicional como un if), conceptualmente el intérprete bifurca guardando la condición de ruta sobre la rama que se ha seguido y la inversa como la condición de ruta para la rama que no se ha seguido.

Uno de los principales desafíos a los que se enfrenta la ejecución simbólica es la selección de ruta, puesto que cuando se encuentra una bifurcación debe elegir que camino seguir primero. Esta elección es importante porque los bucles con condiciones simbólicas pueden provocar árboles de profundidad infinita y puede que no terminen nunca. Por lo tanto, el manejo de bucles es un componente integral en la estrategia de selección de ruta. Existen varios enfoques, como por ejemplo:

  • Rutas aleatorias: Seleccionar estados aleatoriamente recorriendo el árbol de estados desde el principio.
  • Búsqueda en profundidad: Aplicar el algoritmo de búsqueda DFS  sobre el árbol de estados.
  • Búsqueda en amplitud: Aplicar el algoritmo de búsqueda BFS sobre el árbol de estados.
  • Heurísticas: Útiles para objetivos específicos, como por ejemplo calcular un peso para los estados en base a como de lejos está la instrucción más cercana descubierta y seleccionar estados en función de ese peso para maximizar la cobertura de código.

Las técnicas de ejecución simbólica tienen una escalabilidad limitada debido al problema conocido como path explosion: como se crean nuevas rutas en cada rama, el número de rutas en un programa aumenta exponencialmente con el número de sentencias condicionales en cada camino, lo que deriva en un número exponencial de fórmulas con un tamaño exponencial al número de ramas y un tiempo de ejecución exponencial al número de rutas en el programa. Para poder sobrevivir a este fenómeno se deben implementar distintas compensaciones.

Por otra parte, a diferencia del fuzzing, la ejecución simbólica tiene una visión semántica extremadamente alta, es decir, puede razonar sobre cómo desencadenar estados específicos del programa utilizando las condiciones de ruta acumuladas para producir una entrada adecuada a la aplicación cuando una de las rutas ejecutadas ha desencadenado una condición en la que el análisis está interesado.

The post La idea detrás de la ejecución simbólica appeared first on S3lab.

]]>