|
G A C E T A D E L I N U X
...haciendo a Linux un poco más divertido! |
|
Traceando procesos usando ptrace, Parte 2 Por Sandeep S Traducción al español por Javier Ballesteros
|
Nota: Por favor no se confunda. Definitivamente esto es un artículo sobre ptrace, no sobre ELF, pero se necesita algún conocimiento de ELF para acceder a las imágenes de los procesos, por tanto debe explicarse primero.
ELF significa formato de ejecutable y linkado (Executable and Linking Format). Define el formato de los binarios ejecutables usados en Linux - y también para objetos reubicables y compartidos y ficheros de volcado de memoria. ELF es usado tanto por linkadores como por cargadores, ven ELF desde dos lados, así que ambos deben tener un interfaz común.
La estructura de ELF es de tal forma que tiene muchas secciones y segmentos. Los ficheros reubiclables tienen tablas de cabecera de sección, los ficheros ejecutables tienen tablas de cabecera de programa y los ficheros de objetos compartidos tienen ambos. En las siguientes secciones se explicará lo que son estas cabeceras.
Cada fichero ELF tiene su propia cabecera ELF. Siempre empieza en la posición 0 del fichero. Contiene detalles del fichero binario - debería ser interpretado como a lo que son las estructuras de datos respecto de un fichero, etc
El formato de una cabecera se dá abajo (sacado de /usr/src/include/linux/elf.h)
#define EI_NIDENT 16
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Punto de entrada */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
A continuación se presenta una descripción de los campos
e_ident : Contiene información sobre cómo tratar al binario. Es independiente de plataforma.
e_type : Contiene información sobre el tipo y cómo usar el binario. Los tipos son reutilizable, ejecutable, objeto compartido y fichero core.
e_machine : Como habrás supuesto, este campo especifica la arquitectura - Intel 386, Alpha, Sparc, etc.
e_version : Dá la versión del fichero objeto.
e_phoff : Desplazamiento desde el principio hasta la primera sección de la cabecera de programa.
e_shoff : Desplazamiento desde el principio hasta la primera sección de cabecera.
e_flags : Flags específicos del procesador. No se usa en i386
e_ehsize : Tamaño de la cabecera ELF.
e_phentsize & e_shentsize : Tamaño de la cabecera de programa y la cabecera de sección respectivamente.
e_phnum & e_shnum : Número de cabeceras de programa y de sección. Las tablas de cabeceras de programa serán un array de cabeceras de programa (elementos e_phnum). El caso de las tablas de cabeceras de sección es similar.
e_shstrndx : En la tabla de cabecera de sección, una sección contiene el nombre de las secciones. Esto es el índice dentro de la tabla. (mire mas abajo)
Como se dijo antes, los linkadores tratan el fichero como un conjunto de secciones lógicas, descritas en la tabla de cabecera de sección, y los cargadores tratan el fichero como un conjunto de segmentos descritos por la tabla de cabecera de programa. La siguiente sección da detalles sobre las secciones y las cabeceras de segmento/programa.
El fichero binario es visto como una colección de secciones, los cuales son arrays de bytes de los cuales ninguno est´a duplicado. Aunque parezca que habrá más información para interpretar correctamente los contenidos de la sección, las aplicaciones pueden interpretarlos a su propia manera.
Hay una tabla de cabeceras de sección que es un array de cabeceras de sección. La entrada cero de la tabla, es siempre NULL y no describe ninguna parte del binario. Cada sección de cabecera tiene el siguiente formato: (tomado de /usr/src/include/linux/elf.h)
typedef struct elf32_shdr {
Elf32_Word sh_name; /* Nombre de sección, índice de la cadena tbl (si ELF32) */
Elf32_Word sh_type; /* Tipo de sección (Si Elf32) */
Elf32_Word sh_flags; /* Sección miscelánea de atributos */
Elf32_Addr sh_addr; /* Sección virtual addr en la ejecución */
Elf32_Off sh_offset; /* Sección de desplazamiento del fichero */
Elf32_Word sh_size; /* Tamaño de la sección en bytes */
Elf32_Word sh_link; /* Índice de otra sección (si Elf32) */
Elf32_Word sh_info; /* Sección adicional de información (si ELF32) */
Elf32_Word sh_addralign; /* Sección de alineamiento */
Elf32_Word sh_entsize; /* Tamaño de la entrada si la sección maneja una tabla */
} Elf32_Shdr;
Ahora los campos en detalle.
sh_name : Esto contiene un índice dentro de los contenidos de la sección de la tabla de cadenas e_shstrndx . Este índice es el comienzo de una cadena que termina con un null, que es usado como nombre de la sección. Hay muchos, se dan algunos.
sh_type : Tipo de sección, como los datos de programa, la tabla de símbolos, tabla de cadenas, etc ...
sh_flags : Contiene información de cómo tratar los contenidos de la sección.
sh_addralign : Contiene los requerimientos de alineamiento de los contenidos de la sección, normalmente 0/1 (que significan que no hay alineamiento) o 4.
El resto de campos parecen ser autoexplicativos.
Los segmentos ELF son usados durante la carga, por ejemplo cuando la imagen del proceso está en el core. Cada segmento se describe con una cabecera de programa. Habrá una tabla de cabecera de programa en el fichero (normalmente cerca de la cabecera ELF). La tabla es un array de cabeceras de programa. El formato de las cabeceras de programa es como sigue:
typedef struct
{
Elf32_Word p_type; /* Tipo de segmento */
Elf32_Off p_offset; /* Segmento de desplazamiento de fichero */
Elf32_Addr p_vaddr; /* Segmento de dirección virtual */
Elf32_Addr p_paddr; /* Segmento de dirección física */
Elf32_Word p_filesz; /* Segmento de tamaño en el fichero */
Elf32_Word p_memsz; /* Segmento de tamaño en memoria */
Elf32_Word p_flags; /* Segmento de flags */
Elf32_Word p_align; /* Segmento de alineamiento */
} Elf32_Phdr;
p_type : Dá información sobre cómo tratar los contenidos. Dá el tipo de cabecera de programa, tales como
etc ..
p_vaddr : dirección virtual relativa del segmento que se espera sea cargado.
p_paddr : dirección física del segmento que se espera cargar dentro de la memoria.
p_flags : Contiene flags de protección - permisos de lectura/escritura/ejecución
p_align : Contiene el alineamiento para el segmento de memoria. Si el segmento es de tipo cargable, entonces el alinemiento será el tamaño de página esperado.
El resto de campos parecen ser autoexplicativos.
Tenemos alguna idea sobre la estructura de los ficheros objeto ELF. Ahora debemos de saber cómo y dónde estos ficheros son cargados en ejecución. Normalmente, solo ponemos el nombre del programa en el prompt de la bash. De hecho un montón de cosas interesantes suceden antes de que la tecla return sea pulsada.
Primero la shell llama a la función estándar de la libc, la cual como respuesta llama a la rutina del kernel. Ahora el balón está en el campo del kernel. El kernel abre el fichero y averigua el tipo/formato del ejecutable. Después carga el ELF y las librerías necesarias, inicializa la pila del programa y finalmente pasa el control al código del programa.
El programa se carga en la dirección 0x08048000 (se puede ver esto en /proc/pid/maps)
Hemos visto los detalles de los programas siendo cargados en memoria. Así que dado un proceso y su espacio de memoria conocido, podemos tracearlo (si tenemos permisos) y acceder las estructuras privadas del proceso. Es fácil decirlo, pero no es tan fácil hacerlo. ¿Por qué no intentarlo?
Lo primero de todo escribamos un programa para acceder a los registros de otro programa y modificarlos. Aquí usaremos los siguientes valores de request.
Nota : No olvide llamar esto, de otra forma el proceso permanecerá en modo parado y es muy dif&iacaute;cil de recuperar.
struct user_regs_struct definido así en asm/user.h
struct user_regs_struct {
long ebx, ecx, edx, esi, edi, ebp, eax;
unsigned short ds, __ds, es, __es;
unsigned short fs, __fs, gs, __gs;
long orig_eax, eip;
unsigned short cs, __cs;
long eflags, esp;
unsigned short ss, __ss;
};
Ahora vamos a inyectar un pequeño trozo de nuestra imagen al proceso que está siendo traceado y forzarlo a ejecutar nuestro código cambiando sus punteros de instrucciones.
Lo que hacemos es muy sencillo, primero vinculamos el proceso, y después leemos los contenidos del registro del proceso. Ahora insertamos el código que queremos que se ejecute en algún lugar de la pila y el puntero de instrucciones del proceso es cambiado a esa localización. Finalmente desvinculamos el proceso. Ahora el proceso empieza a ejecutarse y ejecutará el código inyectado.
Tenemos dos ficheros de código, uno es código en ensamblador para ser inyectado y otro el que tracea el proceso. Aportaré un pequeño programa para ser traceado.
Los ficheros fuente
Compile los ficheros
#cc Sample.c -o loop
#cc Tracer.c Code.S -o catch
Vaya a otra consola y ejecute el programa de ejemplo poniendo
#./loop
Vuelva y ejecute el traceador para capturar el proceso de loop y cambiar su salida, ponga
#./catch `ps ax | grep "loop" | cut -f 3 -d ' '`
Ahora vaya donde el programa de ejemplo 'loop' está corriendo y observe qué sucede. Definitivamente el juego con ptrace ha comenzado.
En la primera parte traceamos un proceso y contamos su número de instrucciones. En esa parte hemos estudiado la estructura de un fichero ELF y hemos inyectado una pequeña sección de código dentro de un proceso. En la siguiente parte, accederemos al espacio de memoria de un proceso. Hasta entonces, un saludo de Sandeep S.