Feeds 3
Artículos recientes
Nube de tags
seguridad
mfa
dns
zerotrust
monitorizacion
kernel
bpf
sysdig
port knocking
iptables
linux
pxe
documentación
rsyslog
zeromq
correo
dovecot
cassandra
solandra
solr
systemtap
nodejs
redis
hadoop
mapreduce
firewall
ossec
psad
tcpdump
tcpflow
Categorías
Archivo
Proyectos
Documentos
Blogs
Systemtap para detectar la actividad de ficheros de un proceso Mon, 18 Jul 2011
Para retomar un poco el blog, y como hace mucho que no escribo nada sobre SystemTap, me he planteado otro sencillo ejercicio "de repaso".
La verdad es que en la actualidad ya hay un buen montón de artículos, documentación y ejemplos sobre lo que se puede hacer con SystemTap, ya sea para la inspección del kernel como para la de aplicaciones tipo mysql, postgresql o python. El rpm de Scientific Linux 6, por ejemplo, incluye más de 80 ejemplos de todo tipo, desde el análisis de procesos hasta el del tráfico de red.
Aún así, como digo, aquí va un ejemplo más.
El problema
Vamos a suponer que tenemos un proceso ejecutando en una máquina, y que queremos vigilar los ficheros que va abriendo.
Para ir un poco más lejos, queremos que se muestren también los ficheros abierto por los hijos de ese proceso principal (sólo un nivel por ahora).
Y ya puestos, queremos poder limitar la monitorización a los ficheros de ciertas rutas. Por ejemplo, podemos querer mostrar sólo los ficheros que se abrán en /etc, o en /lib.
Vamos, que el ejercicio es prácticamente una extensión de uno de los scripts de un post anterior de este mismo blog. Pero que le voy a hacer, sólo se me ocurren ejercicios sobre ficheros.
Resumiendo, y poniendo un ejemplo, tenemos un servidor postfix, con sus habituales procesos hijo:
master─┬─pickup │─qmgr └─tlsmgr
Queremos saber que ficheros abren todos los procesos que cuelgan de master, ya sean los tres que se están ejecutando en este momento, ya sean los nuevos que se vayan creando. Además, queremos poder limitar el número de ficheros vigilados a una serie de rutas determinadas.
La solución
El núcleo de la solución está en dos "probes":
- syscall.open.return
- syscall.close.return
O sea, las llamadas al sistema open y close. Más concretamente, al momento en el que terminan de ejecutarse (return).
Además, vamos a usar el probe "begin" para leer argumentos, preparar variables y demás parafernalia.
Comenzamos:
probe begin { procesoArgumento = $1 printf ("El pid que vamos a tratar es %d\n", procesoArgumento); if (argc > 1) { for (i=2; i <= argc; i++) { ficherosEnPath[argv[i]] = argv[i] numeroPaths += 1 } } }
Poca explicación hace falta. En $1 tenemos el primer argumento, que va a ser el pid a vigilar. A partir del segundo argumento, y de manera opcional, se pueden incluir una serie de rutas a monitorizar. Todas estas son llamadas válidas:
stap -v monitorFicheros.stp $(pidof master) stap -v monitorFicheros.stp $(pidof master) /etc stap -v monitorFicheros.stp $(pidof master) /etc /usr/lib
Una vez definido este bloque básico, pasamos a la llamada al sistema "open":
probe syscall.open.return { proceso = pid() padre = ppid() hilo = tid() ejecutable = execname() insertarEnTabla = 0 if ( (procesoArgumento == proceso) || (procesoArgumento == padre) || (procesoArgumento == hilo) ) { if ( (procesoArgumento == proceso) && (env_var("PWD") != "") ) { pwd = env_var("PWD") } localpwd = (isinstr(env_var("PWD"), "/"))?env_var("PWD"):pwd; filename = user_string($filename) descriptor = $return filename = (substr(filename, 0, 1) == "/")?filename:localpwd . "/" . filename; if ([proceso,padre,hilo,descriptor] in tablaProcesos) { printf ("{codigo: \"error\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor) } else { if (descriptor >= 0) { if (numeroPaths > 0 ) { foreach (ruta in ficherosEnPath) { if (substr(filename, 0, strlen(ruta)) == ruta) { insertarEnTabla = 1 break } } } if ( (insertarEnTabla == 1) || (numeroPaths == 0) ) { tablaProcesos[proceso,padre,hilo,descriptor] = gettimeofday_ms() tablaFicheros[proceso,padre,hilo,descriptor] = filename printf ("{codigo: \"open\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, tablaProcesos[proceso,padre,hilo,descriptor]) } } } } }
A través de las funciones pid(), ppid() y tid() vemos si el proceso que está ejecutando open en este momento es el proceso que nos interesa o un hijo suyo.
Si cumple este requerimiento, pasamos al bloque que revisa si el fichero que se está abriendo está en el path que nos interesa. En este caso, para hacer el ejercicio más completo, he optado por dar unas cuantas vueltas "de más", y he usado la función env_var("PWD") para acceder al entorno del proceso. En la práctica esta no es la mejor forma, y por eso hay más control en el probe, ya que no siempre existe la variable PWD en el entorno de los procesos que llegan a este open.
La llamada al sistema open carga las variables $return (el valor de retorno de la función es el id del descriptor del fichero) y $filename (el nombre del fichero). Ojo! Cada llamada al sistema tiene unos argumentos, y con ello "ofrece" unas variables para nuestros scripts. Por ejemplo, mientras que en open tenemos filename, flags y mode; en close tenemos fd, filp, files, fdt y retval. El propio SystemTap, Google y el código fuente del kernel son ... vuestros amigos.
En cualquier caso, lo importante es que, cuando se dan todas las condiciones, hacemos lo siguiente:
tablaProcesos[proceso,padre,hilo,descriptor] = gettimeofday_ms() tablaFicheros[proceso,padre,hilo,descriptor] = filename printf ("{codigo: \"open\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, tablaProcesos[proceso,padre,hilo,descriptor])
Con el printf escribimos la información del fichero abierto por salida estándar. Las dos tablas (tablaProcesos y tablaFicheros) sirven para guardar el momento en el que se ha abierto el fichero (con una precisión de milisegundos), y el nombre del fichero en sí mismo, con lo que lo tendremos más a mano cuando pasemos a la llamada al sistema close. Para identificar un fichero usamos el índice formado con [el proceso, el padre, el hilo y el descriptor] del fichero que se ha abierto.
Con toda la información en su sitio ya tenemos la primera parte del script. Seguimos:
probe syscall.close.return { proceso = pid() padre = ppid() hilo = tid() descriptor = $fd ejecutable = execname() if ( ( procesoArgumento == proceso ) || ( procesoArgumento == padre ) || (procesoArgumento == hilo ) ) { if ([proceso,padre,hilo,descriptor] in tablaProcesos) { filename = tablaFicheros[proceso,padre,hilo,descriptor] date = gettimeofday_ms() - tablaProcesos[proceso,padre,hilo,descriptor] printf ("{codigo: \"close\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, date) delete tablaProcesos[proceso,padre,hilo,descriptor] delete tablaFicheros[proceso,padre,hilo,descriptor] } } }
Fácil. Si se está haciendo un close de uno de los ficheros monitorizados, se calcula el tiempo que ha estado abierto y se muestra por la salida estándar.
Un ejemplo de ejecución del monitor es el siguiente:
# stap -v monitorFicheros.stp $(pidof master) Pass 1: parsed user script and 76 library script(s) using 96688virt/22448res/2744shr kb, in 120usr/10sys/133real ms. Pass 2: analyzed script: 8 probe(s), 22 function(s), 7 embed(s), 8 global(s) using 251840virt/48200res/3952shr kb, in 420usr/120sys/551real ms. Pass 3: using cached /root/.systemtap/cache/02/stap_02e370ac4942b188c248e7ec11ac8e2c_19586.c Pass 4: using cached /root/.systemtap/cache/02/stap_02e370ac4942b188c248e7ec11ac8e2c_19586.ko Pass 5: starting run. El pid que vamos a tratar es 1082 {codigo: "open", proceso: "smtpd", pid: 5817, ppid: 1082, tid: 5817, fichero: "/etc/hosts", descriptor: 12, date: 1310920975000} {codigo: "close", proceso: "smtpd", pid: 5817, ppid: 1082, tid: 5817, fichero: "/etc/hosts", descriptor: 12, date: 0}
Sorpresa! Los printf se formatéan como datos json, por si alguna vez quisiera hacer algo con node.js y aplicaciones como log.io.
En cuanto a los "date: 0", se debe a que la precisión de milisegundos es demasiado baja para estos ficheros y para esta prueba sin tráfico de correo de ningún tipo.
El código completo (en este post sólo falta la definición de variables globales) está accesible en github, como siempre.
Limitaciones y trabajo futuro
Como ya he comentado, este script está lejos de ser óptimo. Además de mi propia incapacidad, en algunos momentos he optado por dar más vueltas de las necesarias para usar así más funciones y variables.
Mi primer objetivo era permitir la monitorización de procesos que todavía no estuvieran en ejecución. En esta versión, sin embargo, el proceso principal sí tiene que estar ejecutando antes de lanzar el stap. Esto no es algo técnicamente complicado, pero no se hace solo, y no aporta demasiado desde el punto de vista conceptual al post, así que lo he dejado.
¿Y qué pasa si el pidof da más de un pid? Me habéis pillado. Por ahora sólo trato un pid.
Para el que esté interesado, SystemTap permite guardar la salida estándar en un fichero usando el parámetro -o. A partir de aquí es trivial que otro script perl, python, bash, node.js o lo que sea trabaje con él.
Monitorización del kernel con SystemTap II Sat, 11 Apr 2009
A veces (seguro que pocas), nos encontramos ante una máquina que por algún motivo ha dejado de trabajar como se esperaba. Digamos que el rendimiento o el IO del equipo baja, sin motivo visible. Digamos que no hay logs que indiquen problemas, ni errores del sistema. Nada. Este es el …
Leer másMonitorización del kernel con SystemTap I Sat, 28 Feb 2009
Empiezo otra serie de dos posts. En este caso sobre algo que tenía "pendiente" desde hace ya tiempo. De hecho, normalmente escribiría mis propios ejemplos para publicarlos aquí, pero para ir más rápido me voy a limitar a referenciar los scripts que usaré para mostrar las funcionalidades de ..... SystemTap. SystemTap …
Leer más