Docly Child

4.4.3. Sistemas Operativos: CPU y Procesos

Tabla de contenidos:

CPU (procesador) y procesos

En este punto se va mostrar formas para obtener información de la CPU y trazados de llamadas a sistema de aplicaciones para completar la información del punto anterior. Además se ampliará la información sobre procesos, memoria y almacenamiento (en el siguiente punto). Las prácticas se van a realizar pensando en los Sistemas Operativos (SO a partir de ahora) más habituales: Windows y Linux. En concreto un Windows 10 y la distribución Linux-Debian de Kali Linux que se ha empleado de forma regular en esta publicación.

4.4.3.1. Información de CPU

4.4.3.1.1. GNU/Linux: proc filesystem

Muchas de las distribuciones del SO Linux tienen lo que se llama el sistema de archivos proc filesystem o procfs. procfs es un sistema de archivos que se halla en el directorio /proc y que genera información del sistema de forma dinámica incluyendo datos de la CPU y los procesos  entre otros. Se puede acceder y listar este sistema de archivos con los siguientes comandos desde la terminal:

				
					cd /proc
ls
				
			
Sistema de ficheros procfs

procfs está implementado en el núcleo del Sistema Operativo y en realidad no son archivos reales ni consumen espacio de almacenamiento. Sin esta utilidad sería muy difícil acceder a la información del sistema. Uno de estos archivos es cpuinfo, que proporciona información del procesador (CPU). Para acceder a la información que proporciona este fichero y otros se va a utilizar el comando cat:

				
					cat /proc/<archivo_procfs>
				
			
procfs cpuinfo

Como puede observarse procfs incluye una amplia gama de archivos para obtener información de los temas tratados en puntos anteriores como es interrupts, devices, meminfo, versión

4.4.3.1.2. Windows: Sysinternals

Windows no dispone de un sistema de archivos como procfs en Linux para obtener información del sistema. Desde el escritorio gráfico y con el surtido de aplicaciones que proporciona al usuario la interfaz de Windows es posible obtener información acerca del sistema (Propiedades del sistema, Ver información del sistema…). No obstante la información proporcionada así suele ser escasa y es preferible optar por otros medios.

Una de las opciones para acceder a la información del sistema en Windows es emplear las herramientas de Sysinternals. Sysinternals es un conjunto de herramientas desarrolladas por Mark Russinovich. Si bien al principio  Sysinternals era una empresa aparte de Windows, finalmente la empresa del SO la absorbió y puede encontrarse la información en la siguiente página: https://learn.microsoft.com/en-us/sysinternals/

Systinternals en realidad está formado por varios archivos ejecutables, proporcionando cada uno de ellos una determinada información. Se puede descargar en: https://learn.microsoft.com/en-us/sysinternals/downloads/

Descargar Sysinternals

Una vez descargado el fichero principal comprimido, se recomienda crear un nuevo directorio C:\Program Files\Sysinternals y descomprimir y ubicar en este directorio todos los ficheros para acceder posteriormente con la consola de línea de comandos CMD:

Nuevo directorio Sysinternals

Existen utilitarios (.EXE) para obtener información de usuarios, de red, procesos, etc. Para obtener información sobre la CPU se puede emplear coreinfo.exe o CoreInfo.exe. Este proporciona información acerca de si el procesador y el SO tienen diferentes rasgos como tipos de instrucciones especiales que soporta, si tiene PAE (extensión de direcciones físicas), equivalencia entre procesadores lógicos y físicos, caché asignada al procesador, etc.

				
					cd C:\Program Files\Sysinternals
CoreInfo.exe

				
			
CoreInfo de Sysinternals

4.4.3.2. Trazado de llamadas al sistema

A continuación se va a mostrar dos herramientas para seguir la traza de las system call de un proceso y mostrar por pantalla como se interceptan y se registran. Estas herramientas aparte de proporcionar información también resultan útiles a los desarrolladores de software para depurar y analizar el funcionamiento interno de un programa.

4.4.3.2.1. GNU/Linux: strace

El software para trazar llamadas al sistema para Linux más sencillo de utilizar es el que nos proporciona la herramienta strace (System Call Trace). Esta se utiliza a través de la consola de línea de comandos y en caso de no estar instalada se puede hacer con el gestor de paquetes apt con el siguiente comando:

				
					sudo apt update && sudo apt strace
				
			

Para realizar la práctica se puede emplear cualquier comando básico que genera un proceso en sí como sería listar directorios (ls), leer un fichero con cat, etc. Cada línea mostrada contiene el nombre de la llamada al sistema seguida de sus argumentos y valores retornados.

				
					strace < ls | cat [archivo] | pwd | … >
				
			
Muestra de llamadas a sistema con strace en Linux

Puede apreciarse en el ejemplo que se están empleando algunas de las llamadas a sistema que se han mencionado anteriormente: execve, close, read…

4.4.3.2.2. Windows: StraceNT

Para realizar una práctica similar y ver el trazado de llamadas a sistema con el SO Windows se va a emplear una herramienta de software que cuenta ya con sus años. Esta es StraceNT. Puede descargarse en el siguiente enlace: https://www.softpedia.com/get/Programming/Debuggers-Decompilers-Dissasemblers/StraceNT.shtml

Una vez descargado se puede emplear a través de la consola de línea de comandos con el fichero stracent.exe o bien con una interfaz gráfica con el fichero straceui.exe. Para la práctica se va a emplear la solución gráfica straceui. Una vez abierta la aplicación se puede emplear diferentes filtros que no se van a explicar aquí o bien se puede seleccionar un ejecutable y ver cómo trabaja (Launch New Process). En este caso se ha capturado un proceso ya existente (Attach to Process):

StraceNT (1)

Los resultados mostraran el tipo de llamada a sistema empleado, argumentos retornados y valores retornados. Como se aprecia en la imagen, la terminología empleada por el SO Windows es diferente a la distribución de Linux:

StraceNT (2)

4.4.3.3. Procesos: características, hilos (threads), planificador

En el punto anterior ya se ha dado una definición más o menos precisa de lo que es un proceso. En todo caso es una entidad dinámica y se refiere al conjunto de instrucciones en ejecución de un programa o la instancia de un programa que está en ejecución. El objetivo principal de este punto es realizar una práctica para crear un proceso con la llamada a sistema fork.  En los siguientes puntos se analizará en detalle cómo se define un proceso en Windows y Linux y como obtener información de los procesos que se están ejecutando en cada SO. Esta información será fundamental para las operaciones de postexplotación.  

A continuación se describen brevemente algunas de las características que representan a los procesos:

  • Procesos padre e hijo: Todos los procesos estándares que están en ejecución tienen un proceso padre y a su vez pueden tener uno o más procesos hijos. El proceso hijo es un nuevo proceso que se crea a partir de un proceso padre. El proceso padre puede crear múltiples procesos hijos y cada uno tiene su propio espacio de memoria y recursos. Los procesos hijos son independientes del proceso padre y se ejecutan en su propio contexto.
  • Hilos o threads: Asociados a un proceso están los hilos (threads en inglés). Un hilo es un flujo de ejecución a lo largo del código del proceso, con su propio contador de programa, registros y pila. Los hilos usualmente comparten el mismo espacio de memoria y recursos del proceso principal, lo que significa que pueden acceder y modificar las mismas variables y datos. Sin entrar en detalles, el uso de hilos mejora la eficiencia de un proceso.
  • Identificación y usuario del proceso: La mayoría de los SO asocian el proceso al usuario (UID) que lo ha iniciado y lo identifican con algún número (PID en Linux).
Procesos en Linux con identificación de usuario (UID) y número (PID)
  • PCB Process Control Block: El PCB o en español Bloque de Control de Procesos es una estructura de datos que contiene información de los procesos. Incluye estado del proceso (en ejecución, bloqueado, preparado…), contador de la siguiente instrucción a ejecutar, registros CPU, información de planificación y gestión de memoria…

Otros conceptos fundamentales que convienen mencionar son el planificador (scheduler) y el activador (dispatcher). Sin entrar en detalles, ambos forman parte del código del núcleo del Sistema Operativo y como el nombre indica el objetivo del primero es seleccionar que proceso debe ejecutarse a continuación de otro mientras que el activador pone en ejecución el proceso seleccionado. Además, en el contexto de los procesos padre-hijo es habitual que los hijos pueden comunicarse con el proceso padre o con otros procesos a través de mecanismos de comunicación interprocesos, como tuberías (pipes), colas de mensajes, etc. (existen varios paradigmas de comunicación entre procesos). 

4.4.3.3.1. Llamada a sistema (fork): proceso padre-hijo

En la introducción a las llamadas a sistema (systems calls) se ha explicado que existen lenguajes de programación como C que permiten instrucciones para realizar algunas de ellas directamente. Una de estas es fork(). Esta instrucción crea un nuevo proceso duplicando el existente en curso. Como ya se ha comentado arriba, una de las características de los procesos es que pueden ser hijo y/o padre. En el caso de incluir fork() en el código, el proceso padre recibe el PID (número de identificación) del proceso hijo devuelto por fork(), mientras que el proceso hijo recibe un valor de retorno de 0 indicando que es el proceso hijo (fork() == 0).

A continuación una práctica en lenguaje C para ver esto en detalle. El código sería el siguiente (incluye comentarios que se analizan abajo):

				
					//Comentario 1
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
//Comentario 2
int main()
{
//Comentario 3
    pid_t pid;
    pid = fork();
//Comentario 4
    switch(pid)
    {
        case -1: 
            perror("fork"); 
            break;

        case 0: 
            printf("Proceso %d; padre = %d\n", getpid(), getppid());
            break;

        default:
            printf("Proceso %d; padre = %d\n", getpid(), getppid());
            break;
    }
    return 0;
}

				
			

Para los usuarios que no sepan de lenguaje C o no tengan conocimientos de programación, estos son los comentarios:

  • Comentario 1: Sección de bibliotecas y cabeceras que proporcionan funciones e instrucciones. Se ponen al principio del código antes de la función principal con la cláusula #include. La cabecera <sys/types.h> define tipos de datos comunes y estructuras utilizadas en operaciones de bajo nivel y en la comunicación con el sistema operativo.
  • Comentario 2: La secuencia int main() define el inicio de la función principal.
  • Comentario 3: Sección de definición de variables: primero se indica el tipo y después el nombre. En este caso pid_t es un tipo de dato en C que se utiliza para representar identificadores de procesos. Después la variable toma valor con la función fork() y se duplica el proceso.En el proceso hijo, pid se inicializa con el valor 0, ya que es el valor de retorno de fork() en el proceso hijo.
  • Comentario 4: La instrucción switch es una estructura condicional que ejecuta diferentes bloques de instrucciones dependiendo del valor de una variable, en este caso pid. Si la llamada a fork() devuelve -1 para pid, indica que se produjo un error al crear el nuevo proceso y se imprime un mensaje de error utilizando perror(). En caso contrario, si el valor de retorno de fork() es 0, significa que el código se está ejecutando en el proceso hijo y se imprime (instrucción printf) su PID (getpid) y el PID de su proceso padre (getppid). Por otro lado, si el valor de retorno de fork() es diferente de 0, significa que el código se está ejecutando en el proceso padre y se imprime su PID y el PID de su proceso padre (que será diferente al del proceso hijo).

Una vez comprendido el código, es necesario saber cómo ejecutarlo. En lenguaje C es de nivel medio-alto y para ejecutar las instrucciones del código es necesario realizar el proceso de compilación. Para ello seguir los siguientes pasos en Kali Linux o alguna versión similar de Linux:

1. Copiar el código mostrado en un fichero con algún editor de texto y guardarlo (en la imagen se ha utilizado vi). Debe tener extensión .c para indicar que es un fichero con información de código en lenguaje C. No es necesario incluir los comentarios (indicados por doble barra //).

2. Para compilar utilizar el software de GCC (GNU Compiler Collection). Este software está incluido en casi todas las distribuciones de Linux y es software libre. Se compila con la siguiente instrucción en la consola de línea de comandos. En la primera opción después del parámetro -o se va a indicar el nombre que va a tener el ejecutable y en la segunda opción el fichero que contiene el código en C.

				
					gcc –o <nombre_ejecutable> <fichero>.c
				
			

3. Para ejecutar el fichero:

				
					./<nombre_ejecutable>
				
			
Función fork() en lenguaje C

Como la función fork() realiza una copia del proceso en curso por esto se ven dos líneas de resultados. En los resultados mostrados el proceso padre tiene un ID de proceso (PID) de 1385 y su padre tiene un PID de 1267. El proceso hijo tiene un PID de 1386 y su padre tiene un PID de 1385. Esto significa que el proceso padre creó exitosamente un proceso hijo. Al visualizar los procesos que se están ejecutando también se muestra que el proceso /usr/bin/zsh se corresponde a la consola, siendo este por lo tanto el proceso padre inicial.