El modelo de sesión y proceso de Linux como parte del monitoreo y las alertas de seguridad

illustration-radar-security.png

El modelo de proceso de Linux, disponible en Elastic, permite a los usuarios redactar reglas de alerta muy específicas y obtener información más detallada de lo que sucede exactamente en sus equipos de escritorio y servidores Linux.

En este blog, brindaremos información de contexto sobre el modelo de proceso de Linux, un aspecto clave de cómo se representan las cargas de trabajo de Linux.

Linux sigue el modelo de proceso de Unix de la década de 1970 que se incrementó con las sesiones de concepto en la década de 1980, a juzgar por cuando se introdujo la llamada al sistema setsid() con los primeros documentos POSIX

El modelo de proceso de Linux es una buena abstracción para registrar las cargas de trabajo de computadoras (qué programas se ejecutan) y redactar reglas para reaccionar a estos eventos. Ofrece una representación clara de quién hizo qué y cuándo en cuál servidor a los fines de alertas, cumplimiento y búsqueda de amenazas.

Capturar la creación del proceso, la escalada de privilegios y el tiempo de vida ofrece información detallada sobre cómo se implementan las aplicaciones y los servicios, y sobre sus patrones normales de ejecución de programas. Una vez identificados los patrones de ejecución normales, pueden redactarse reglas para enviar alertas cuando se produzcan patrones de ejecución anómalos.

La información detallada del proceso posibilita la redacción de reglas muy específicas para alertas, lo cual reduce los falsos positivos y la fatiga por alertas.  También permite que las sesiones de Linux se categoricen como una de las siguientes opciones:

    • servicios autónomos iniciados en el arranque (por ejemplo, cron)
    • servicios que brindan acceso remoto (por ejemplo, sshd)
    • acceso remoto interactivo (probablemente humano) (por ejemplo, terminal bash iniciada a través de ssh)
    • acceso remoto no interactivo (por ejemplo, instalación de software por parte de Ansible a través de ssh)

Estas categorizaciones permiten una revisión y reglas muy precisas. Por ejemplo, uno podría revisar todas las sesiones interactivas en servidores específicos en un plazo seleccionado.

En este artículo se describe cómo funciona el modelo de proceso de Linux y ayudará en la redacción de reglas de alerta y respuesta para eventos de carga de trabajo. Comprender el modelo de proceso de Linux también es un primer paso fundamental para entender los contenedores y los espacios de nombre y cgroups a partir de los cuales se componen.

Captura de modelo de proceso frente a logs de llamadas al sistema

Capturar los cambios en el modelo de sesión en cuanto a procesos nuevos, sesiones nuevas, procesos de cierre, etc. es más simple y claro que capturar las llamadas al sistema usadas para realizar dichos cambios. Linux tiene aproximadamente 400 llamadas al sistema y no las refactoriza una vez que se lanzan. Este enfoque conserva una interfaz binaria de aplicación (ABI) estable, lo que significa que los programas compilados para ejecutarse en Linux hace años deberían continuar ejecutándose en Linux hoy sin necesidad de volver a compilarlos desde el código fuente.

Se agregan nuevas llamadas al sistema para mejorar las capacidades o la seguridad en lugar de refactorizar las llamadas al sistema existentes (evita romper con la ABI). El resultado es que mapear una lista ordenada por tiempo de llamadas al sistema y sus parámetros a las acciones lógicas que realizan requiere de mucha experiencia. Además, las llamadas al sistema más nuevas, como las de io_uring, posibilitan leer y escribir archivos y sockets sin llamadas al sistema adicionales usando la memoria asignada entre el kernel y el espacio del usuario.

En cambio, el modelo de proceso es estable (no cambió mucho desde la década de 1970), pero aun así abarca de forma integral las acciones que se toman en un sistema cuando una incluye acceso a archivos, redes y otras operaciones lógicas.

Formación del proceso: init es el primero proceso luego del arranque

Una vez iniciado el kernel de Linux, crea un proceso especial denominado "el proceso de init". Un proceso expresa la ejecución de uno o más programas. El proceso init siempre tiene la id. de proceso (PID) 1 y se ejecuta con una id. de usuario 0 (raíz). Las distribuciones de Linux más modernas usan systemd como su programa ejecutable del proceso init.

La tarea de init es iniciar los servicios configurados, como bases de datos, servidores web y servicios de acceso remoto, como sshd. Estos servicios suelen estar encapsulados en sus propias sesiones, lo que simplifica iniciar y detener servicios agrupando todos los procesos de cada servicio en una única id. de sesión (SID).

El acceso remoto, como a través del protocolo SSH a un servicio sshd, creará una sesión de Linux nueva para el usuario que accede. Esta sesión ejecutará inicialmente el programa que solicitó el usuario remoto (con frecuencia, un shell interactivo), y todos los procesos asociados tendrán la misma SID.

La mecánica de crear un proceso

Cada proceso, excepto el proceso init, tiene un solo proceso principal. Cada proceso tiene una PPID, la id. de proceso de su proceso principal (0/sin principal, en el caso de init). La reasignación de proceso principal puede darse si un proceso principal se cierra de un modo que no finalice también los procesos secundarios.

La reasignación de proceso principal escoge init como nuevo proceso principal, e init tiene un código especial para realizar una limpieza una vez que se cierran estos procesos secundarios adoptados. Sin este código de limpieza y adopción, los procesos secundarios huérfanos se convertirían en procesos "zombies" (¡no es broma!). Quedan merodeando hasta que su proceso principal los cosecha para poder examinar su código de cierre; un indicador de si el programa secundario completó sus tareas con éxito.

La llegada de "contenedores", espacios de nombre pid en particular, requirió la capacidad de designar procesos distintos de init como "subcosechadores" (procesos dispuestos a adoptar procesos huérfanos). Por lo general, los subcosechadores son los primeros procesos en un contenedor. Esto se hace porque los procesos en el contenedor no pueden "ver" procesos en los espacios de nombre de pid predecesores (es decir, su valor de PPID no tendría sentido si el proceso principal estuviera en un espacio de nombre de pid predecesor).

Para crear un proceso secundario, el principal se clona mediante la llamada al sistema fork() o clone(). Tras la bifurcación/clonación, la ejecución continúa de inmediato en ambos, el principal y el secundario [ignorando la opción CLONE_VFORK de clone() y vfork()], pero en distintas rutas de código como consecuencia del valor de código de devolución de fork()/clone().

Leíste bien: la llamada al sistema fork()/clone() proporciona un código de devolución en dos procesos distintos. El principal recibe la PID del secundario como código de devolución, y el secundario recibe 0, de modo que el código compartido del principal y el secundario puede ramificarse basándose en ese valor. Hay algunos matices de la clonación con los procesos principales de varios subprocesos y la memoria de copia durante la escritura en cuanto a eficiencia que no necesitamos abordar aquí. El proceso secundario hereda el estado de memoria del proceso principal y sus archivos abiertos, sockets de red y terminal de control, en caso de que haya.

Por lo general, el proceso principal capturará la PID del secundario para monitorear su ciclo de vida (consulta la cosecha descrita antes). El comportamiento del proceso secundario depende del programa que se clonó [proporciona la ruta de ejecución a seguir según el código de devolución de fork()].

Un servidor web como nginx puede clonarse y crear un proceso secundario para que se encargue de las conexiones http. En casos como este, el proceso secundario no ejecuta un programa nuevo, sino que ejecuta una ruta de código diferente en el mismo programa para que se ocupe de las conexiones http, en este caso. Recuerda que el valor de devolución de una clonación o bifurcación le indica al proceso secundario que es el proceso secundario para que pueda elegir esta ruta de código.

Los procesos de shell interactivos (por ejemplo, bash, sh, fish, zsh, etc. con una terminal de control), posiblemente de una sesión ssh, se clonan cuando se introduce un comando. El proceso secundario, que aún ejecuta una ruta de código del principal/shell, realiza una gran cantidad de trabajo para configurar los descriptores de archivos para el redireccionamiento de E/S, configura el grupo de proceso y más antes de que la ruta de código en el proceso secundario haga la llamada al sistema execve() o similar para ejecutar un programa diferente dentro de ese proceso.

Si escribes ls en tu shell, se bifurca el shell, la configuración descrita arriba la realizan el shell/proceso secundario y luego el programa ls (usualmente del archivo /es/usr/bin/ls) se ejecuta para reemplazar los contenidos de ese proceso con el código máquina de ls. En este artículo sobre implementación de control del trabajo de shell se proporciona mucha información sobre el funcionamiento interno de los grupos de procesos y shells.

Es importante destacar que un proceso puede llamar a execve() más de una vez y, por lo tanto, los modelos de datos de captura de carga de trabajo deben poder ocuparse de esto también. Esto significa que un proceso puede convertirse en varios programas diferentes antes de cerrarse; no solo su programa de proceso principal opcionalmente seguido por un programa. Ve el comando exec builtin de shell para ver una forma de hacerlo en un shell (es decir, reemplaza el programa de shell con otro en el mismo proceso).

Otro aspecto de ejecutar un programa en un proceso es que algunos descriptores de archivos abiertos (los marcados como close-on-exec) pueden cerrarse antes de la ejecución del programa nuevo, mientras que otros pueden permanecer disponibles para el programa nuevo. Recuerda que una sola llamada fork()/clone() proporciona un código de devolución en dos procesos: el principal y el secundario. La llamada al sistema execve() es extraña también en cuanto a que una execve() exitosa no tiene código de devolución por completarse correctamente dado que su resultado es una ejecución de programa nuevo, de modo que no tiene dónde realizar la devolución, excepto cuando execve() falla.

Creación de sesiones nuevas

Linux actualmente crea nuevas sesiones con una única llamada al sistema, setsid(), que realiza el proceso que se convierte en el líder de la sesión nueva. Esta llamada al sistema suele ser parte de la ruta de código del proceso secundario clonado que se ejecuta antes de ejecutar otro programa en ese proceso (es decir, está planificada, e incluida, en el código del proceso principal). Todos los procesos dentro de una sesión comparten la misma SID, que coincide con la PID del proceso que se llama setsid(), también se los conoce como líderes de sesión. En otras palabras, un líder de sesión es cualquier proceso con una PID que coincida con su SID. El cierre del proceso líder de sesión desencadenará la finalización de sus grupos de procesos secundarios inmediatos.

Creación de grupos de procesos nuevos

Linux usa grupos de procesos para identificar un grupo de procesos que trabajan juntos dentro de una sesión. Todos tendrán la misma SID e id. de grupo de procesos (PGID). La PGID es la PID del líder de grupo de procesos. El líder de grupo de procesos no tiene un estado especial; puede cerrarse sin afectar a los otros miembros del grupo de procesos y ellos mantendrán la misma PGID, aunque el proceso con esa PID ya no exista.

Ten en cuenta que incluso con pid-wrap (reutilización de una pid usada recientemente en sistemas ajetreados), el kernel de Linux se asegura de que la pid de un líder de grupo de procesos cerrado no se reutilice hasta que todos los miembros de ese grupo de procesos se hayan cerrado (es decir, no hay forma de que la PGID pudiera referirse accidentalmente a un proceso nuevo).

Los grupos de procesos son valiosos para los comandos de pipeline de shell como:

cat foo.txt | grep bar | wc -l

Esto crea tres procesos para tres programas diferentes (cat, grep y wc) y los conecta con barras verticales. Los shells crearán un grupo de procesos nuevo incluso para comandos de programa únicos como ls. El propósito de los grupos de procesos es permitir enfocarse en señales en un conjunto de procesos e identificar un conjunto de procesos (el grupo de procesos en primer plano) con acceso de lectura y escritura completo a la terminal de control de su sesión, si la hay.

En otras palabras, control-C en tu shell enviará una señal de interrupción a todos los procesos en el grupo de procesos en primer plano (el valor de PGID negativo como objetivo de la pid de la señal discrimina entre el grupo y el proceso del líder del grupo de procesos en sí). La asociación de la terminal de control garantiza que los procesos que leen la entrada desde la terminal no compitan entre sí y causen problemas (la salida de la terminal puede permitirse desde grupos de procesos que no estén en primer plano).

Usuarios y grupos

Como se mencionó antes, el proceso init tiene la id. de usuario 0 (raíz). Cada proceso tiene un grupo y usuario asociado, y estos pueden usarse para restringir el acceso a los archivos y llamadas al sistema. Los usuarios y los grupos tienen id. numéricas y pueden tener un nombre asociado como root o ms. El usuario raíz es el superusuario, que puede hacer cualquier cosa y solo debería usarse cuando es absolutamente necesario por cuestiones de seguridad.

Al kernel de Linux solo le interesan las id. Los nombres son opcionales y los proporcionan para conveniencia de las personas los archivos /etc/passwd y /etc/group. Name Service Switch (NSS) permite que estos archivos se extiendan con usuarios y grupos de LDAP y otros directorios (usa getent passwd si deseas ver la combinación de /es/etc/passwd y usuarios proporcionados por NSS).

Cada proceso puede tener varios usuarios y grupos asociados (grupos reales, efectivos, guardados y complementarios). Consulta man 7 credentials (man 7 credenciales) para más información.

El mayor uso de contenedores cuyos sistemas de archivos raíz se definen en imágenes de contenedores ha aumentado la probabilidad de que /es/etc/passwd y /es/etc/group estén ausentes o les falten algunos nombres de id. de grupos y usuarios que pueden estar en uso. Como al kernel de Linux no le preocupan estos nombres, sino solo las id., no es un problema.

Resumen

El modelo de proceso de Linux proporciona una forma sucinta y precisa de representar cargas de trabajo de servidor, lo que a su vez permite una revisión y reglas de alerta muy específicas. Una representación por sesión fácil de comprender del modelo de proceso en tu navegador proporcionaría información muy útil sobre las cargas de trabajo del servidor.

Puedes comenzar con una prueba gratuita de 14 días de Elastic Cloud. O descarga la versión autogestionada del Elastic Stack de forma gratuita.

Aprender más

Las páginas man de Linux son una fuente excelente de información. Las páginas man siguientes tienen detalles del modelo de proceso de Linux descrito antes: