jueves, 3 de enero de 2013

Encontrando la sys_call_table en kernels modernos (2.6-3.x)

Cuando busco documentación sobre LKMs (Loadable Kernel Modules) muchas veces me encuentro con documentos muy interesantes pero por desgracia bastante desactualizados, generalmente refiriéndose a kernels 2.2 y con suerte 2.4.

Como muchos sabéis la interfaz que ofrece el SO al espacio de usuario son las llamadas al sistema (syscalls) y es muy común encontrar en estos documentos ejemplos que muestran dónde están y sobre todo cómo capturarlas/hookearlas/hijackearlas.

Antiguamente la tabla de llamadas al sistema era un símbolo del kernel que era público, sólo había que importarlo en el módulo que se estuviera desarrollando y usarlo. Algo así:

extern void* sys_call_table[];

No recuerdo exactamente a partir de qué versión del kernel (2.6?), esto dejó de ser así. Los desarrolladores decidieron que tener este símbolo por ahí pululando y de tan fácil acceso daba más beneficio a los malos que a los buenos (estoy de acuerdo), así que lo quitaron. Sobra decir que esto no es una solución final, pero te quitas de por medio a las personas menos hábiles.

Dado que ese símbolo ya no está, ahora hay que buscar la tabla a mano. Durante los últimos años he visto por internet muchas formas de hacerlo:
  1. Buscar la tabla dentro del fichero System.map o /proc/kallsyms
  2. Iterar desde un símbolo del sistema en busca de la dirección de una llamada al sistema.
Vamos a describir un poco estos métodos y por qué considero que, si bien te pueden sacar del apuro, no me terminan de convencer.

Buscar la tabla dentro de System-map o /proc/kallsyms

Generalmente existe un fichero con los símbolos que el kernel exporta en /boot, con el nombre System.map-<version del kernel>. Este fichero, tiene símbolos del kernel y sus direcciones.

Me resulta curioso que, sin ser sys_call_table un símbolo exportado por el kernel, se pueda encontrar en este fichero este símbolo.

$ sudo egrep "sys_call_table" /boot/System.map-`uname -r`
c15b3020 R sys_call_table


Con esto ya tenemos solucionada la vida en principio, sólo tendríamos que meter en nuestro módulo algo como esto para poder empezar a usar la sys_call_table:

void *sys_call_table[] = (void *)0xc15b3020

Un problema que surge con este método es que si, por el motivo que sea, al kernel le da por mover la tabla (nada usual, yo nunca lo he visto), esta dirección ya no será válida y nuestro módulo petará.

Una vuelta de tuerca a esto es buscar el símbolo en el fichero que contiene los símbolos del sistema que está en ejecución, /proc/kallsyms.

$ sudo egrep "sys_call_table" /proc/kallsyms
c15b3020 R sys_call_table


Como estoy en un sistema normal se ve que ambas direcciones son la misma, pero esto no tiene por qué ser así siempre.

Un problema que tiene el buscar el símbolo en estos ficheros es que no esté, o peor aún, que realmente la tabla no se encuentre ahí (porque el sistema nos está mintiendo por ejemplo... la razón de por qué lo hace no importa).

La conclusión sobre esta técnica es que, si bien es muy rápida y simple, no me parece hacer bien las cosas.

Iterar desde un símbolo del sistema en busca de la dirección de una llamada al sistema.

Este otro método fue el primero que aprendí, en mis primeros años en la universidad. Se trata de coger un símbolo que está antes que la tabla de llamadas al sistema e ir recorriendo la memoria en busca de de la dirección de alguna llamada al sistema y entonces asumir que se ha encontrado la tabla. He visto un par de algoritmos haciendo esto, uno no lo encuentro pero recuerdo que uno de los símbolos en los que se basaba era loops_per_jiffy. El otro es éste:

while(i) {
    if (sys_table[__NR_read] == (unsigned long)sys_read) {
        sys_call_table = sys_table;
        flag = 1;
        break;
    }
    i--;
    sys_table++;
}

Previa inicialización de las variables claro.

En el sitio en que encontré esto dice lo siguiente:

This function attempts to gain access to sys_call_table by starting at the address of system_utsname. The system_utsname structure contains a list of system information and is known to exist before the system call table. Therefore, the function starts at the location of system_utsname and iterates 1,024 (MAX_TRY) times. It advances a byte every time and compares the current location with that of sys_read(), whose address is assumed to be available to the LKM. Once a match is found, the loop breaks and we have access to sys_call_table.

Muy bien, tres suposiciones, no está mal. La primera dice que se sabe que ese símbolo está antes que la tabla de llamadas al sistema. Espero que no le owneen el sistema nunca, que no pruebe en sistemas owneados, que los desarrolladores no les de por cambiar la posición de ese símbolo, que realmente ese símbolo esté siempre antes que la tabla independientemente de la compilación del kernel y también espero que al kernel no le pueda dar por cambiar la posición de estos símbolos. Bitch please...

La segunda suposición es que sys_read va a estar disponible para el LKM. Generalmente es así, no he visto nunca un sistema sin este símbolo, pero quién sabe lo que me puedo encontrar por ahí. No me gusta suponer.

La tercera suposición es que si encontramos un número que vale lo mismo que sys_read (aquí estamos tratando a sys_read como un número, un puntero) entonces es sys_read y hemos encontrado la tabla. No sé en que se basa para asegurar esto, podría aparecer el mismo número por otro motivo y la técnica no funcionaría... y además tiene toda la pinta de que depurarlo iba a ser complicado. En definitiva, este método va a funcionar muchas veces, pero es más guarro que el código de find. Y personalmente no me gusta la cantidad de situaciones en las que puede fallar.

Discutido sobre los métodos más comunes encontrados por internet, expongo aquí otro método que, si bien lo he pensado y escrito yo desde cero, me consta que no es algo genuino y que se usa por ahí. El principal problema con este método es que no es tan sencillo como los otros, sin embargo en mi opinión es mucho más estable y bonito.

Se trata de buscar la dirección de la tabla de llamadas al sistema allí en donde se encuentra, ni más ni menos. Nada de suponer cosas. Explicaré cómo funciona el método para arquitecturas x86, pero desde ya hago saber que si se quiere usar en otra arquitectura hay que saber cómo funcionan las interrupciones en la misma.

El ordenador es un cacharro que funciona con interrupciones. El hardware manda interrupciones al procesador, y también los programas para ejecutar llamadas al sistema. En GNU/Linux en concreto, las llamadas al sistema que hacen los programas de espacio de usuario se realizan mediante la instrucción int $0x80. Esta instrucción ejecuta una interrupción, el procesador se interrumpe y la manejará (en algún momento, supuesto que no esté enmascarada y bla bla bla). Para ello el procesador accede a la tabla de manejadores de interrupción, coge el manejador de la interrupción 0x80 (lo cierto es que puede haber varios manejadores y es problema de ellos reconocer a quién le toca manejarla) y ejecuta el manejador.

En el caso de Linux, la función manejadora de esta interrupción es system_call, de arch/x86/kernel/entry_32.S:

ENTRY(system_call)
    RING0_INT_FRAME
    pushl_cfi %eax
    SAVE_ALL
    GET_THREAD_INFO(%ebp)

    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    jnz syscall_trace_entry
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys
syscall_call:
    call *sys_call_table(,%eax,4)

    movl %eax,PT_EAX(%esp)

syscall_exit:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)   

    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx
    testl $_TIF_ALLWORK_MASK, %ecx 

    jne syscall_exit_work

restore_all:
    TRACE_IRQS_IRET
restore_all_notrace:
    movl PT_EFLAGS(%esp), %eax   
    movb PT_OLDSS(%esp), %ah
    movb PT_CS(%esp), %al
    andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
    cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
    CFI_REMEMBER_STATE
    je ldt_ss   

restore_nocheck:
    RESTORE_REGS 4  

irq_return:
    INTERRUPT_RETURN


Puede resultar un poco lioso todo este código, pero con la cabeza fría se entiende. De todas formas he marcado en negrita lo que realmente nos interesa, la instrucción que hace uso de la tabla de llamadas al sistema. Esa instrucción call tiene hardcodeada la dirección de la sys_call_table. Si como dijimos con los métodos anteriores, al sistema le da por moverla, tiene que modificar esta instrucción por cojones. No es muy aconsejable eso de estar cambiando valores en instrucciones y en el kernel, pero es posible. Y de aquí es de donde vamos a sacar la dirección que buscamos y no de sitios que "supuestamente" la tienen.

Lo principal para conseguir esto es saber dónde y cómo buscar la función system_call(). En x86, además de los registros que todos conocemos (eax, ebx, ebp, eip, etc...), el procesador tiene otra serie de registros que son principalmente usados por el sistema y que no se puede acceder siempre, sino sólo por código que se ejecute en ring0, los programas de usuario se ejecutan en ring3. No voy a profundizar mucho más porque no viene al caso, pero si quieres obtener más información mi recomendación es mirar el manual oficial de intel, es bastante hardcore, pero ayuda mucho.

Para obtener la dirección de la función que maneja una interrupción (puede haber hasta 256), los procesadores de la familia x86 tienen un par de registros especiales (IDTR y GDTR) a los que sólo se puede acceder estando en ring0 (sólo el kernel puede, para que nos entendamos) y que contienen la dirección en memoria de dos tablas (IDT y GDT respectivamente), el propósito completo de las mismas no vamos a explicarlo aquí, pero diremos por encima que la primera (Interrupt Descriptor Table) contiene información sobre el manejador de una interrupción, sin embargo por la arquitectura de manejo de memoria de estos procesadores (segmentada y paginada), esta tabla por si sola no puede darnos la dirección de la función manejadora, sino que señala a una entrada en la GDT (Global Descriptor Table) que nos da información para saber en que segmento se encuentra el manejador que buscamos.

Se puede leer de los registros IDTR y GDTR con las instrucciones sidt y sgdt, también se puede escribir en ellos con las instrucciones lidt y lgdt pero estas no nos interesan ahora mismo. En la siguiente imagen muestro el formato y funcionamiento del IDTR, pero GDTR funciona igual.


El campo base es la dirección dónde se encuentra la tabla correspondiente a este registro y limit indica el número de entradas en total. La forma de acceder a cada una de las entradas de estas tablas es mediante el vector pasado a la instrucción int (o el que traiga si se trata de una excepción u otra interrupción no generada por el procesador sino por el hardware), en el caso de las llamadas al sistema 0x80.

A la hora de implementar esto en nuestro programa podemos optar por crearnos la estructura nosotros mismos o usar las que ya tiene definidas el kernel en arch/x86/include/asm/desc_defs.h,

struct desc_ptr {
        unsigned short size;
        unsigned long address;
} __attribute__((packed)) ;

Lo mismo podemos hacer a la hora de usar las instrucciones sidt y sgdt para inicializar estas estructuras. Podemos usar assembly inline o podemos usar las funciones que define el kernel que básicamente acaban usando el assembly inline. Están en arch/x86/include/asm/desc.h.
static inline void native_store_gdt(struct desc_ptr *dtr)
{
        asm volatile("sgdt %0":"=m" (*dtr));
}

static inline void native_store_idt(struct desc_ptr *dtr)
{
        asm volatile("sidt %0":"=m" (*dtr));
}


Edito: En un primer momento usé las funciones native_store_[ig]dt(), he comprobado que en máquina virtual no funcionan, hay que usar las funciones store_[ig]dt().

Una vez tengamos ambos registros accedemos a la entrada a la que apunta IDTR dentro de la IDT (en nuestro caso la entrada 0x80). Cada una de las entradas en la IDT puede tener uno de estos 3 formatos:
  • Task gate
  • Interrupt gate
  • Trap gate
No profundizaremos en ellos porque se sale del objetivo de este post, nos quedaremos con saber que en la entrada que a nosotros nos interesa hay un interrupt gate.



No voy a entrar a describir todos los campos, tan sólo comentaré los que nos interesan para esta tarea. El campo segment selector contiene el desplazamiento dentro de la GDT donde comienza la entrada correspondiente a esta entrada de la IDT (hace falta leerlo y pensarlo un par de veces para entenderlo). Los dos campos de offset son el desplazamiento donde se encuentra la función manejadora dentro del segmento de memoria correspondiente a esta entrada, para averiguar la base de dicho segment necesitamos la GDT y por eso las entradas en la IDT contienen información de acceso a la GDT (segment selector)

Con eso sólo tenemos el desplazamiento dentro del segmento, nos hace falta la dirección base del mismo. Esto lo obtenemos de la entrada de la GDT a la que apunta el campo base address que acabamos de ver en el registro de la IDT, cuyo formato es el siguiente.
Queda poco profesional usar imágenes con distintos formatos, lo sé, pero es lo que hay, no he encontrado ninguna imagen de Intel con el formato de las entradas de la GDT.

De estas entradas necesitamos los campos base para formar la dirección base del segmento donde se encuentra el manejador. Base address 15:00 contiene los 16 bits menos significativos de la dirección, base 31:24 los 8 bits más significativos.

De nuevo al igual que antes, podemos implementarnos nosotros mismos los accesos a estos datos (se aprende bastante) o podemos hacer uso de las funciones del kernel. En este caso de gate_offset(), get_desc_base() para obtener el desplazamiento y la base necesarios para acceder al fin la función manejadora. La dirección de la misma es base + desplazamiento (recordemos, base en la GDT y desplazamiento en la IDT). Una vez hecho esto lo que tenemos es la dirección donde se encuentra system_call (anteriormente expuesta).

Ahora podemos empezar a buscar la dirección de la sys_call_table, que está hardcodeada dentro de la instrucción call *sys_call_table(,%eax,4). No voy a liarlo más describiendo el formato de la instrucción call, tan sólo comentar que está encodeada con los bytes \xff\x14\x85\x??\x??\x??\x??, donde los bytes con \x?? son la dirección de la sys_call_table en little-endian. Desde el comienzo del código de system_call hasta el punto donde se realiza este call no existen más instrucciones que tengan este patrón así que ahora sólo tenemos que iterar por el código de la función en busca del patrón \xff\x14\x85 y coger los 4 bytes siguiente y tendremos la autentica y genuina sys_call_table :).

Después de todo este peñazo veamos el código que nos devolvería la sys_call_table (aquí faltan las cabeceras necesarias que he comentado anteriormente).

#define LINUX_SYSCALL_VECTOR (0x80)
#define CALL_PATTERN (0x0085ff14)
#define CALL_PATT_MASK (0x00ffffff)

unsigned long **getSyscallTable(void) {
    struct desc_ptr idtr, gdtr;
    struct desc_struct *idt_entry, *gdt_entry;
    u32 gate_offset, gate_base;
    u8 *syscall_desc, *call_offset;

    native_store_idt(&idtr);    native_store_gdt(&gdtr);


    store_idt(&idtr);
    store_gdt(&gdtr);

    idt_entry = (struct desc_struct *)idtr.address + LINUX_SYSCALL_VECTOR;
    gdt_entry = (struct desc_struct *)gdtr.address + gate_segment(*idt_entry);
    gate_offset = (u32)gate_offset(*idt_entry);
    gate_base = (u32)get_desc_base(gdt_entry);
    syscall_desc = (u8 *)(gate_base + gate_offset);

    call_offset = search_call_opcode(syscall_desc);
    return *(unsigned long ***)(call_offset + 3);
}


void *search_call_opcode(void *code) {
    while ((*(u32 *)(code++) & CALL_PATT_MASK ) != CALL_PATTERN);
    return --code;
}


La función getSyscallTable() que aquí muestro realiza todo este trabajo: Lee los registros IDTR y GDTR, a partir de ellos calcula dónde se encuentra system_call y posteriormente busca dentro del código el patrón \xff\x14\x85 y recoge de los siguientes 4 bytes la dirección de la sys_call_table.

Bastante más complicado que buscar el símbolo dentro de System.map o /proc/kallsyms o que iterar a partir de estructuras del kernel. Pero haciéndolo de esta forma tenemos más garantías de que vamos a recoger la verdadera dirección de la sys_call_table que está usando el kernel, incluso aunque estemos en un sistema que está raro.

¿Es este método infalible? Por supuesto... que no, se me ocurren al menos un par de situaciones dónde se podría engañar a este método, pero son demasiado rocambolescas como para describirlas aquí y tenerlas en cuenta en el programa. Situaciones donde la desconfianza en el sistema es absoluta, pero contra la desconfianza no se puede hacer nada. También el método depende de que este código no cambie e introduzca entre el comienzo de system_call y el call que buscamos el patrón en el que nos basamos, pero en caso de que ocurriera (cosa poco probable), modificar este código para manejar la situación es bastante sencillo.

Saludos.