|
G A C E T A D E L I N U X
...haciendo a Linux un poco más divertido! |
|
Seguimiento de procesos usando ptrace,
Parte III
Por Sandeep S Traducción al español por Rafa Pereira
|
Las características básicas de ptrace fueron explicadas en la
Parte I. En la
Parte II
vimos un pequeño programa que accedía a los registros de un proceso y
los modificaba para cambiar la salida de ese proceso, insertando código extra.
En esta ocasión vamos a acceder a la memoria de un proceso. El propósito de este
artículo es introducir un método para infectar programas en tiempo de ejecución.
Son muchas las áreas potenciales de utilización de esta técnica.
Estamos familiarizados con ptrace y conocemos las técnicas para vincular un proceso, para hacerle un seguimiento (tracing) y, finalmente, para liberarlo. También tenemos una idea de la estructura del formato binario de Linux: ELF.
Nuestro plan es acceder/modificar un programa en ejecución. Así que tenemos que
localizar los símbolos del interior del programa. Ahí necesitamos
link_map.
link_map es la estructura interna del enlazador dinámico en la cual éste
mantiene información sobre las librerías cargadas y sobre los símbolos incluídos
en ellas.
El formato de link_map es (de /usr/include/link.h)
struct link_map
{
ElfW(Addr) l_addr; /* Base address shared object is loaded at. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
};
Una breve explicación de los campos:
link_map es una lista enlazada, cada elemento de la cual tiene un apuntador a una librería cargada. Para seguir esta cadena, lo que tenemos que hacer es recorrer todas las librerías y buscar nuestro símbolo en cada una de ellas. Ahora tenemos una pregunta. ¿Dónde podemos encontrar este link_map?
Para cada fichero objeto hay una tabla de desplazamiento global (GOT: Global Offset Table) que contiene muchos detalles del fichero binario. La segunda entrada de la GOT está dedicada al link_map. Así que obtenemos la dirección del link_map de GOT[1] y procedemos a la búsqueda de nuestro símbolo.
Ya hemos reunido la información básica necesaria para acceder a la memoria.
Empecemos. En primer lugar vinculamos el proceso 'pid' para su seguimiento
(tracing). A continuación obtenemos el link_map. Encontrarás funciones
read_data, read_str, etc. Estas son funciones
auxiliares para facilitar el trabajo con ptrace. Las funciones auxiliares son
auto-explicativas.
La función para localizar el link_map es:
struct link_map *locate_linkmap(int pid)
{
Elf32_Ehdr *ehdr = malloc(sizeof(Elf32_Ehdr));
Elf32_Phdr *phdr = malloc(sizeof(Elf32_Phdr));
Elf32_Dyn *dyn = malloc(sizeof(Elf32_Dyn));
Elf32_Word got;
struct link_map *l = malloc(sizeof(struct link_map));
unsigned long phdr_addr, dyn_addr, map_addr;
read_data(pid, 0x08048000, ehdr, sizeof(Elf32_Ehdr));
phdr_addr = 0x08048000 + ehdr->e_phoff;
printf("program header at %p\n", phdr_addr);
read_data(pid, phdr_addr, phdr, sizeof(Elf32_Phdr));
while (phdr->p_type != PT_DYNAMIC) {
read_data(pid, phdr_addr += sizeof(Elf32_Phdr), phdr,
sizeof(Elf32_Phdr));
}
read_data(pid, phdr->p_vaddr, dyn, sizeof(Elf32_Dyn));
dyn_addr = phdr->p_vaddr;
while (dyn->d_tag != DT_PLTGOT) {
read_data(pid, dyn_addr += sizeof(Elf32_Dyn), dyn, sizeof(Elf32_Dyn));
}
got = (Elf32_Word) dyn->d_un.d_ptr;
got += 4; /* second GOT entry, remember? */
read_data(pid, (unsigned long) got, &map_addr, 4);
read_data(pid, map_addr, l, sizeof(struct link_map));
free(phdr);
free(ehdr);
free(dyn);
return l;
}
Empezamos desde la posición 0x08048000 para obtener la cabecera elf del proceso cuyo seguimiento vamos a realizar. Obtenemos la cabecera elf y, a través de sus campos, accedemos a la cabecera de programa. (Los campos de las cabeceras fueron discutidos en la Parte II.) Una vez que hemos obtenido la cabecera de programa, procedemos a localizar la cabecera con la información de enlace dinámico. De la cabecera/estructura con la información de enlace dinámico obtenemos la ubicación de la información. Seguimos buscando hasta obtener la dirección base de la tabla de desplazamiento global (GOT).
Ahora que tenemos la dirección de la GOT tomamos su segunda entrada (ahí tenemos el link_map). De ahí cogemos la dirección del link_map y regresamos.
Tenemos la estructura link_map. Lo que necesitamos obtener es symtab y strtab.
Para ello, vamos al campo l_ld de link_map y recorremos las
secciones
dinámicas hasta encontrar DT_SYMTAB y DT_STRTAB y, finalmente, poder buscar
nuestro símbolo desde DT_SYMTAB. DT_SYMTAB y DT_STRTAB son las direcciones de la
tabla de símbolos y de la tabla de cadenas de caracteres respectivamente.
La función resolv_tables es:
void resolv_tables(int pid, struct link_map *map)
{
Elf32_Dyn *dyn = malloc(sizeof(Elf32_Dyn));
unsigned long addr;
addr = (unsigned long) map->l_ld;
read_data(pid, addr, dyn, sizeof(Elf32_Dyn));
while (dyn->d_tag) {
switch (dyn->d_tag) {
case DT_HASH:
read_data(pid, dyn->d_un.d_ptr + map->l_addr + 4,
&nchains, sizeof(nchains));
break;
case DT_STRTAB:
strtab = dyn->d_un.d_ptr;
break;
case DT_SYMTAB:
symtab = dyn->d_un.d_ptr;
break;
default:
break;
}
addr += sizeof(Elf32_Dyn);
read_data(pid, addr, dyn, sizeof(Elf32_Dyn));
}
free(dyn);
}
Aquí, lo que hacemos es leer las secciones dinámicas una por una y comprobar si
la etiqueta (tag) es DT_STRTAB o DT_SYMTAB. Si lo es, guardamos sus apuntadores
respectivos en strtab y symtab. Una vez que se han
terminado las secciones
dinámicas, podemos parar.
Nuestro siguiente paso es extraer de la tabla de símbolos el símbolo buscado. Para ello, tomamos cada entrada de la tabla de símbolos y comprobamos si se trata del nombre de una función. (Estamos interesados en encontrar el valor de una función de librería). Si lo es, lo comparamos con el nombre de la función que nos han dado. Si coincide, devolvemos el valor del símbolo.
Ya tenemos el valor del símbolo que estábamos buscando. ¿Qué ayuda nos va a proporcionar ese valor? La respuesta depende del lector. Como ya ha quedado establecido, podemos utilizarlo con propósitos nobles o malvados.
Quizá el lector esté pensando que aquí termina todo. Pero hemos olvidado un paso que no deberíamos olvidar: desvincular el proceso del que hemos hecho el seguimiento. Este olvido puede dejar al proceso en un estado de parada indefinida, con las consecuencias ya discutidas en la Parte I. Así que nuestro último paso es desvincular el proceso al que hemos hecho seguimiento.
El programa puede obtenerse en Ptrace.c. Casi todo el código es auto-explicativo.
Lo compilamos con:
#cc Ptrace.c -o symtrace
Ahora queremos probar el programa. Para ello, ejecutamos y dejamos en ejecución
algún proceso en otra consola, volvemos a ésta y tecleamos lo siguiente (aquí el
programa al que hacemos seguimiento es
emacs y el símbolo buscado esstrcpy). Se puede
hacer seguimiento de
cualquier programa 'traceable' en lugar de emacs y buscar cualquier símbolo:
#./symtrace `ps ax | grep 'emacs' | cut -f 2 -d " "` strcpy
Y observa lo que ocurre.
De esta forma llegamos al final de una serie de tres artículos que ha hecho un
recorrido por la programación básica con ptrace. Una vez que se ha
entendido el concepto básico no es difícil dar más pasos por uno mismo. En
www.phrack.org hay disponibles más detalles
sobre ptrace y elf. Una cosa más a comentar es que hemos llegado hasta aquí sin
mencionar siquiera un tema importante. Una de las características importantes
de ptrace es su interacción con las llamadas al sistema. En User Mode Linux
(Linux en Modo
Usuario), esta característica se usa a gran escala. Estoy ocupado con las clases
y con el proyecto de último curso, pero prometo, si dispongo de tiempo, que
continuaremos esta serie y, a continuación, daremos un vistazo a esas
características de ptrace.
Son bienvenidas todas las sugerencias, críticas, contribuciones, etc. Puedes contactar conmigo en busybox@sancharnet.in