30
Aug
2012

Función de Prólogo y Epílogo (lenguaje assembler)

Las funciones de Prólogo y el Epílogo son una convención (vigente para muchos lenguajes de alto nivel, como por ejemplo C) que los compiladores aplican al generar el código assembler correspondiente a una función. El objetivo es establecer una protección contra los buffer overflows.

Supongamos el siguiente código en C:

int obtener_numero_fijo();
 
int main(void) {
	return 1;
}
 
int obtener_numero_fijo(){
	return 2;
}

Luego de compilarlo (con GCC), analizamos el código objeto (con objdump) y observamos el siguiente código assembler:

00000000 
: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: b8 01 00 00 00 mov $0x1,%eax 8: 5d pop %ebp 9: c3 ret 0000000a : a: 55 push %ebp b: 89 e5 mov %esp,%ebp d: b8 02 00 00 00 mov $0x2,%eax 12: 5d pop %ebp 13: c3 ret

Podemos observar que ambas funciones comienzan poniendo el valor del registro ebp en el stack. Luego mueven el stack pointer al registro ebp. Al finalizar la función, se retira el último valor del stack y se guarda en el registro ebp.


Leer el resto del artículo »

28
Aug
2012

Desensambladores de Barrido Lineal vs Recursivos Descendentes

Los Desensambladores de Barrido Lineal (Linear Sweep Disassemblers) son de diseño más simple. Funcionan en base al algoritmo básico descripto en el artículo Los 4 pasos básicos del desensamblado. El desensamblador determina en primer lugar dónde comienza el código de ejecución de un binario, según las cabezeras del archivo. Una vez encontrado este punto, lee de forma lineal instrucción a instrucción bajo el supuesto de que donde termina una instrucción, comienza la siguiente. El desensamblador “no tiene idea” de los flujos de ejecución del programa.

El problema con esto es que los programas pueden tener embebidos datos entre medio de sus instrucciones. Si el desensamblador no diferencia datos de instrucciones, los datos serán interpretados falsamente como instrucciones.

Los Desensambladores Recursivos Descendentes (Recursive Descent Disassemblers) son otra aproximación al diseño de un algoritmo de desensamblado. A diferencia de los primeros, estos algoritmos se enfocan en entender el flujo de control de un programa: qué es lo que está ocurriendo.

Leer el resto del artículo »

25
Aug
2012

Los 4 pasos básicos del desensamblado

Los lenguajes de programación pueden ser categorizados en 4 niveles:

  • 1a generación: código de máquina. Este código son números (ceros y unos, que generalmente se visualizan en sistema hexa) que representan instrucciones correspondientes a la arquitectura de procesador. Este código es lo único que un CPU puede entender para ejecutar sus tareas. Por ejemplo, los procesadores con arquitectura x86 tienen un set de estas instrucciones definido. Las instrucciones son de largo variable y cada una de ellas consiste de un “código de operación” (número) y, opcionalmente, datos de entrada.
  • 2da generación: traducción del código de máquina a un lenguaje nemotécnico, llamado assembler. Desensamblar un código es el proceso de traducir el código de máquina en código assembler. Para esto se requiere un conocimiento de la arquitectura para la cuál fue compilado el programa: no es lo mismo un código de máquina “90” para el set de instrucciones x86 que para ARM. Una vez identificada la arquitectura, la traducción implica principalmente buscar en una tabla de instrucciones assemblercódigo de máquina. Algunos ejemplos de instrucciones en assembler son NOOP, JMP, ADD, etc.
  • 3ra generación: aquí están los lenguajes de programación de alto nivel como C, C++, Java, etc. Lo que los programadores generalmente escriben, y envían al compilador para que se encargue del pasaje a código de máquina. Decompilar es el proceso de traducir código assembler en lenguaje de 3ra generación (revertir lo que hace un compilador). Este proceso es sucio dado que la decompilación “pierde información” (nombres de variables, nombres de funciones, etc.) y no existe un único camino hacia atrás (un programa puede ser traducido a diferentes códigos assembler y un código assembler a diferentes código de alto nivel).
  • 4ta generación: no interesan en este caso.


Leer el resto del artículo »

6
Jul
2012

Charla de Linus Torvalds en la Universidad Aalto

Si tienen algo de tiempo, quiero recomendarles mirar esta charla de Linus Torvalds en la Universidad Aalto (Finlandia), realizada el 14 de junio de 2012. Básicamente son preguntas sobre los temas más variados que involucran al sistema operativo Linux y a los proyectos y comunidades open-source en general, realizadas por el entrevistador y por la audiencia.

Hay unas cuantas perlitas. No quiero adelantarles aquí nada porque es mejor verlo, pero les digo que no tiene desperdicio y Linus es un genio 🙂


Link directo: http://www.youtube.com/watch?v=MShbP3OpASA

Me dejó pensando como el tipo puede dedicarse con pasión a los aspectos técnicos del proyecto y dejar toda la basura del negocio y la gestión en manos de otros. ¿Cuáles son los motivos por los cuales nosotros deberíamos o no-deberíamos despreocuparnos, como profesionales de perfil técnico, de la basura del negocio y dedicarnos con pasión a lo que nos gusta? ¿El dinero? ¿La necesidad de irse a otro país? ¿Cuánto lo vale? ¿Puede uno, haciendo convivir ambas cosas, sentirse satisfecho a la larga?

22
Jun
2012

Modificar parámetros del kernel (Linux y BSD)

Los kernel de Linux y BSD tienen parámetros que pueden ser configurados para habilitar, deshabilitar o modificar ciertas funcionalidades. Estas funcionalidades pueden estar relacionadas al stack de comunicaciones, a la memoria virtual, a los medios de almacenamiento y al comportamiento del CPU entre otros.

Los parámetros -como es de suponer- tienen valores por defecto. Cuando el kernel inicia, inspecciona el archivo de configuración /etc/sysctl y los archivos contenidos en la carpeta /etc/sysctl.d/ para reemplazar los valores por defecto por los valores allí especificados. A su vez, durante la ejecución (o runtime), el kernel almacena los valores que se encuentra usando para estos parámetros en la estructura de carpetas /proc/sys. Dentro de esta estructura jerárquica hay archivos de texto con el nombre del parámetro y su valor.


Leer el resto del artículo »

12
May
2012

Forking de procesos en Linux

La creación de procesos en Linux implica dos etapas: forking y exec -me tomo la licencia de denominarlas así por las familias de funciones implicadas en cada caso-.

En la etapa de forking se duplica el proceso que realiza la llamada de creación. Para esta duplicación se crea un nuevo process descriptor -estructura donde se guarda la información del proceso- y se agrega a la lista de process descriptors que maneja el kernel. Este nuevo process descriptor es muy similar al del proceso padre pero difiere en el process id y en los procesos hijos -el proceso recién creado no tiene hijos y el proceso padre pasó a tener un hijo más-.

En cuanto al espacio de memoria del nuevo proceso, se utiliza la técnica de copy on write para lograr una mayor optimización. Empleando esta técnica ambos procesos tienen su espacio de memoria pero esta división comienza siendo “virtual”. A medida que se realizan operaciones de escritura en la memoria, se duplican las páginas que corresponden.

La etapa de exec permite la carga en memoria y ejecución de un binario. Veremos en este artículo solo forking.

A continuación la parte más divertida:

#include <stdio .h>
#include <stdlib .h>
#include <unistd .h>
 
int main(int argc, char* argv[]){
    pid_t id_del_proceso_hijo = fork();
    pid_t proceso_actual = getpid();
    printf("Mensaje de %d: Mi hijo es %d\n", proceso_actual, id_del_proceso_hijo);
    int* status_ptr = malloc(sizeof(int));
    *status_ptr = -1;
    pid_t estado_del_hijo = waitpid(id_del_proceso_hijo, status_ptr, 0x0);
    printf("Mensaje de %d: Espere hasta que se reportó el proceso %d. Info del reporte: %d\n", proceso_actual, estado_del_hijo, *status_ptr);
    return 0;
}
</unistd></stdlib></stdio>


Leer el resto del artículo »

24
Mar
2012

Crypto++: cifrar bloque binario con AES + ECB

Crypto++ es una de las bibliotecas más importantes y recomendables para implementar funciones criptográficas en C++. Es de uso libre (open source) y funciona en múltiples plataformas (Windows, Linux, OS X). Si en algún momento deciden usarla, tomenlo con paciencia porque es un poco árida al principio.

En el día de hoy comencé a implementar una demo de CTR (streamcipher) en Qt/C++. Para esta implementación -que no es tan compleja-, necesité aplicar un blockcipher a bloques binarios de largo fijo. Elegí como blockcipher al algoritmo Rijndael. Como no era el objetivo implementar un Rijndael desde cero, decidí utilizar la funcionalidad que brinda Crypto++ (cryptopp).

Para facilitar el asunto, voy a estar trabajando con bloques y claves de 128 bits (16 bytes).

Al cifrar un único bloque de largo fijo e igual a 128 bits, no necesito padding ni técnicas de block-chaining (ej. CBC). Por lo tanto, el modo en el que necesitaba usar AES es ECB sin padding. Recordemos que ECB no encadena bloques realmente y en caso de que el texto plano se repita, se repetirá también el cifrado leakeando información. Por favor nunca utilizar ECB para cifrar mensajes de más de un bloque.

Hechas estas consideraciones, este es el código:


Leer el resto del artículo »

19
Mar
2012

Autenticación de mensajes: HMAC

Otro mecanismo para autenticar mensajes es utilizar una función de hash y una PSK (pre-shared key). Llamemos h a la función de hash.

¿Cómo se calcula el MAC?

h ( K XOR a || h ( K XOR b || m))

K es la clave compartida.
a y b son constantes pre-definidas. Según el NIST en su documento FIPS198a, a (outerpad) es el byte x’5c’ repetido la cantidad de veces que entra un byte como input de la función de hash. b (innerpad) se forma igual que a pero con el byte x’36’.
m es el mensaje a autenticar.

Según el NIST, en su documento FIPS198a:

The size of the key, K, shall be equal to or greater than L/2, where L is the size of the hash function output.

Calcular el HMAC de las siguientes formas está mal:

h ( K || m), h ( m || K), h (K || m || K), etc.

B. Schneier, en su libro Cryptography Engineering, considera que utilizar SHA-1 como función de hash es riesgoso. En su lugar -y para lograr una seguridad de 128 bits- recomienda SHA-256.

Leer el resto del artículo »

17
Mar
2012

Autenticación de mensajes: CBC-MAC II

Retomando el artículo anterior (Autenticación de mensajes: CBC-MAC), voy a comentarles aquí otros dos posible ataques a CBC-MAC.

Este ataque puede aplicarse también algunas funciones de hash. Supongamos que “c” tiene un bloque de largo. El atacante encuentra que MAC(a || c) = T y que MAC(b || c) = T. Como MAC(a || c) = E(MAC(a) XOR c) y MAC(b || c) = E(MAC(b) XOR c), entonces MAC(a) = MAC(b). Por lo tanto, si el atacante captura MAC(a || d), puede saber que MAC(b || d) tendrá el mismo tag.

Para la siguiente demostración, a la función MAC la voy a abreviar como M. Supongamos ahora que “a” y “b” tienen un bloque de largo. El atacante captura las MACs de “a”, “b” y “a || b”. Si captura “a || b”, entonces tiene E(M(a) XOR b). El atacante puede crear ahora la MAC del mensaje “b || M(a) XOR M(b) XOR b”. ¿Por qué? Ese mensaje tendría la siguiente MAC: E(M(b) XOR (M(a) XOR M(b) XOR b)). Si conmutamos y reasociamos, sería igual a E(M(b) XOR M(b) XOR M(a) XOR b). Pero M(b) XOR M(b) es igual a 0. Y 0 XOR ALGO es igual a ALGO. Entonces nos quedaría E(M(a) XOR b). PERO ESO ES M(a || b) QUE YA LO TENEMOS! Entonces usamos ese mismo tag y acabamos de autenticar un mensaje un tanto particular pero construido por nosotros 🙂

Artículo en base a información obtenida de Cryptography Engineering (B. Schneier). Demostración de martin.com.uy/sec.

16
Mar
2012

Autenticación de mensajes: CBC-MAC

Como mencionabamos en el artículo anterior (MAC: autenticación de mensajes), CBC-MAC es una implementación de MAC para autenticación de mensajes.

CBC-MAC

La idea detrás de esta implementación es transformar un algoritmo de cifrado con CBC (encadenamiento de bloques) en una función MAC. El tag MAC se obtiene al aplicar el algoritmo de cifrado con CBC al mensaje y tomar el último bloque -en ocasiones se utiliza la mitad del bloque-. Es importante que la clave utilizada en la obtención del MAC sea distinta a la clave utilizada para cifrar el mensaje. En caso contrario el MAC sería igual al último bloque del mensaje y podrían debilitarse tanto el cifrado como la autenticación.

Un ataque al CBC-MAC

Supongamos que un atacante, con suficiente tiempo, captura todos los mensajes de una comunicación y almacena su MAC en una base de datos con dos columnas: Mensaje – MAC. En determinado momento va a suceder que al almacenar una de esas tuplas mensaje-MAC, el valor de MAC ya se encuentra en la base de datos para un mensaje distinto. Esto es, en criptografía, una colisión. ¿Cuándo sucederá? Por la paradoja del cumpleaños, para un MAC de 128 bits ocurrirá en promedio al capturar 2^64 mensajes.


Leer el resto del artículo »