Seguridad con AppArmor: Introducción

AppArmor es un sistema de seguridad MAC (Mandatory Access Control o Control de acceso Obligatorio) para GNU/Linux. El sistema AppArmor utiliza unos perfiles de acceso escritos en un lenguaje amigable para el administrador, con el fin de limitar a los programas el acceso a un determinado grupo de ficheros, capabilities, accesos de red y limites de recursos, conocidos como la política o perfil de acceso de AppArmor para el programa.

DAC vs MAC

El kernel de Linux proporciona Control de acceso discrecional (DAC - Discretionary Access Control) como un mecanismo para limitar la forma en que los usuarios y las aplicaciones pueden acceder a los recursos del sistema. El control de acceso DAC restringe el acceso a objetos en función de la identidad de los sujetos o grupos a los que pertenecen. Esto incluye tanto las comprobaciones de permisos de estilo clásico de UNIX como las Listas de control de acceso (ACL) POSIX. El sistema de compartimentación de recursos capabilities también se engloba dentro del DAC.

Por otro lado, el control de acceso obligatorio (MAC) es un esquema mediante el cual las acciones de las aplicaciones están restringidas por el sistema operativo de acuerdo con una política establecida por el administrador del sistema. Esto difiere de DAC en que los permisos son controlados por el administrador y no a discreción de usuarios o aplicaciones individuales.

Desde la versión 2.6 el kernel de Linux incluye el subsistema Linux Security Module (LSM) que proporciona un conjunto de puntos de enganche en varias zonas críticas para la seguridad dentro del kernel de Linux. Estos puntos de enganche son utilizados por varias extensiones como AppArmor o SELinux para hacer cumplir un control de acceso obligatorio (MAC).

AppArmor por tanto es un sistema de seguridad MAC construido sobre LSM. AppArmor confina las aplicaciones individuales limitando su comportamiento en función de un conjunto de políticas definidas por el administrador que se encuentran en archivos de texto llamados "perfiles". El módulo de seguridad de AppArmor implementa enlaces LSM para hacer cumplir las políticas definidas dentro de los perfiles de AppArmor, este aumenta el DAC tradicional en el sentido de que los programas confinados se evalúan primero con el DAC tradicional y, si DAC permite el comportamiento, se consulta la política de AppArmor.

Este confinamiento es selectivo, de modo que algunos programas del sistema pueden estar confinados, mientras que otros no. Esto permite al administrador la flexibilidad de desactivar un perfil problemático para la resolución de problemas mientras deja confinadas otras partes del sistema.

AppArmor también admite el modo de aprendizaje por perfil (complain) para ayudar a la hora de crear y mantener políticas. El modo de aprendizaje permite crear un perfil ejecutando un programa normalmente y aprendiendo su comportamiento. Una vez que AppArmor haya aprendido suficientemente el comportamiento, el perfil puede cambiarse al modo de aplicación. Tenemos que tener en cuenta que los perfiles generados son normalmente más permisivos que un perfil hecho a mano diseñado para un entorno y una aplicación específicos. De cualquier forma el modo de aprendizaje puede reducir en gran medida el esfuerzo y el conocimiento necesarios para usar AppArmor y su uso nos permite agregar una capa importante de seguridad a nuestro sistema.

Uso de los perfiles AppArmor

Para este ejemplo vamos a crear un programa que lee de un fichero y muestra lo leído por un puerto que especifiquemos. Tanto el puerto como el fichero se especifican como argumentos al lanzar la aplicación. Vamos a ver como podemos crear un perfil que limite el uso de la aplicación a lo estrictamente necesario.

La aplicación es muy sencilla (tampoco es necesario entender el código, solo la funcionalidad):

server_file.c
/* 
 * server_file.c 
 * 
 * (c) Author: David Quiroga 
 * e-mail: david (at) clibre (dot) io 
 * 
 *************************************************************** 
 * Descripción: 
 *  
 * Lee un fichero espera en un puerto y devuelve una cadena 
 * 
 * SPDX-License-Identifier: GPL-3.0  
 * 
*/ 
#define _GNU_SOURCE 
#include <stdio.h> 
#include <stdlib.h> 
#include <stddef.h> 
#include <string.h> 
#include <errno.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <sys/socket.h>  
#include <netdb.h> 
#include <signal.h>  
#include <locale.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
 
#define SIZE 100 
static FILE *error_log = NULL; 
 
/* Macro para la gestión de errores */ 
#define ifError(assertion, ...) ({ \ 
    if(assertion) { \ 
        int errsv = errno; \ 
        fprintf (error_log ? error_log : stderr, "[%s:%d]:" \ 
            " \n [+] ", __FILE__, __LINE__); \ 
        fprintf (error_log ? error_log : stderr, __VA_ARGS__ ); \ 
        if (errsv != 0) \ 
            fprintf (error_log ? error_log : stderr, \ 
                        " [+] errno: [%s]\n", strerror(errsv)); \ 
    exit(EXIT_FAILURE); \ 
    } \ 
})  
 
static volatile sig_atomic_t SignalSalir = 1; /* Señal de salir */  
int conSocket = 0; 
char *clibremesg = NULL; 
 
void handle_signal()  
{  
    SignalSalir = 0; 
    shutdown(conSocket, SHUT_RDWR); 
}  
 
int main(int argc, char *argv[]) { 
 
    uint16_t conPort = 0; 
    int returnStatus = 0; 
    struct sockaddr_in conServer; 
    int fd; 
    int numRead = -1; 
 
    setlocale(LC_ALL, ""); 
 
    ifError (3 != argc, "Uso: %s <puerto> <fichero>\n", argv[0]); 
 
    clibremesg = malloc(SIZE); 
    ifError (clibremesg == NULL, "Error al asignar memoria\n"); 
      
    /* abrimos fichero */ 
    fd = open(argv[2], O_RDONLY);  
    ifError (fd == -1, "Error al abrir el fichero: %s\n", argv[2]) ; 
    /* leemos datos */ 
    numRead = read(fd, clibremesg, SIZE); 
    ifError (numRead == -1, "Error al leer los datos del fichero %s\n", argv[2]); 
    close (fd); 
 
    /* Añadimos manejador para señales, finalizar */  
    ifError (signal(SIGINT, handle_signal) == SIG_ERR, "Error en signal\n");  
 
    conSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 
    ifError (conSocket == -1, "Error en socket\n"); 
 
    printf("Socket creado!\n"); 
 
    /* Puerto de escucha */ 
    conPort = (uint16_t) atoi(argv[1]); 
    ifError (conPort < 1, "Error en el puerto seleccionado\n"); 
 
    /* configuramos la estructura address usando */ 
    /* INADDR_ANY para hacer bind en todas las direcciones locales */ 
    bzero(&conServer, sizeof(conServer));  
    conServer.sin_family = AF_INET; 
    conServer.sin_addr.s_addr = htonl(INADDR_ANY); 
    conServer.sin_port = htons(conPort); 
 
    /* hacemos bind en la dirección y puerto */ 
    returnStatus = bind(conSocket,(struct sockaddr *)&conServer,sizeof(conServer)); 
    ifError (returnStatus != 0, "Error en Bind\n"); 
 
    printf ("Escuchando conexiones en puerto %d\n", conPort); 
    /* escuchamos conexiones en el socket */ 
    returnStatus = listen(conSocket, 5); 
    ifError (returnStatus == -1, "Error en listen\n"); 
 
    struct sockaddr_in clientName = { 0 }; 
     int conChildSocket = 0; 
     socklen_t clientNameLength = sizeof(clientName); 
 
    while (SignalSalir) { 
        /* Esperando conexiones */ 
        conChildSocket = accept(conSocket,(struct sockaddr *)&clientName, 
                                    &clientNameLength); 
        if(SignalSalir == 0)  
         break; 
         
        ifError (conChildSocket == -1, "Error en accept\n"); 
 
        /* tenemos una conexión */ 
        /* escribimos el mensaje al cliente */ 
        write(conChildSocket, clibremesg, strlen(clibremesg)); 
    } 
 
    printf("...cerrando aplicación y saliendo...\n");  
    close(conChildSocket); 
    close(conSocket);  
    free (clibremesg); 
    exit (EXIT_SUCCESS); 
} 

Construimos el ejecutable, creamos un fichero con un texto para pasarle como parámetro y lanzamos la aplicación:

❯ gcc -Wall server_file.c -o server_file
❯ echo "CLIBRE: Comunidad Libre - Software Libre" > fichero.txt
❯ sudo cp server_file /usr/local/bin
❯ server_file 1973 fichero.txt
Socket creado!
Escuchando conexiones en puerto 1973

Desde otro terminal podemos acceder al socket, por ejemplo con nc:

❯ nc 127.0.0.1 1973
CLIBRE: Comunidad Libre - Software Libre
^C

Pero ¿que ocurre si server_file intenta acceder a otro fichero, por ejemplo /etc/passwd?:

❯ server_file 1973 /etc/passwd
Socket creado!
Escuchando conexiones en puerto 1973

Leemos desde otro terminal:

❯ nc 127.0.0.1 1973
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/^C

La aplicación puede leer cualquier fichero que los permisos de GNU/Linux le permiten y por tanto tiene acceso a muchos más fichero de los que necesita para su ejecución. Con esto podemos hacernos una idea de qué acceso tendría un atacante que pudiera comprometer nuestra aplicación.

Vamos a crea un perfil AppArmor para limitar el acceso.

Primero vamos a ejecutar aa-unconfined que nos muestra los programas que tienen algún puerto abierto en escucha (y por tanto más susceptibles a ser comprometidos) y no están confinados, es decir, no tienen un perfil AppArmor. Tenemos server_file ejecutado en otro terminal y lanzamos:

❯ sudo aa-unconfined
[sudo] contraseña para david:
686 /usr/sbin/cups-browsed confined by '/usr/sbin/cups-browsed (enforce)'
942 /usr/sbin/cupsd confined by '/usr/sbin/cupsd (enforce)'
8370 /usr/local/bin/server_file not confined

Como vemos al final tenemos a nuestra aplicación server_file que aparece como 'not confined'.

Vamos a utilizar el modo de aprendizage de AppArmor para crear el perfil. Finalizamos server_file y lanzamos aa-genprof:

❯ sudo aa-genprof /usr/local/bin/server_file
Writing updated profile for /usr/local/bin/server_file.
Estableciendo /usr/local/bin/server_file al modo reclamar.

Antes de comenzar, es posible que desee comprobar si
ya existe el perfil para la aplicación que
quiere confinar. Vea la siguiente página wiki para
más información:
https://gitlab.com/apparmor/apparmor/wikis/Profiles

Perfilado: /usr/local/bin/server_file

Please start the application to be profiled in
another window and exercise its functionality now.

Once completed, select the "Scan" option below in
order to scan the system logs for AppArmor events.

For each AppArmor event, you will be given the
opportunity to choose whether the access should be
allowed or denied.

[(S)can system log for AppArmor events] / (F)inalizar

Como nos indican, tenemos que larzar server_file para que el programa pueda aprender el uso que hace del sistema. En otro terminal lanzamos server_file:

❯ server_file 1973 fichero.txt
Socket creado!
Escuchando conexiones en puerto 1973

y en el terminal donde tenemos aa-genprof esperando pulsamos S y contestamos a las preguntas:

Reading log entries from /var/log/syslog.
Updating AppArmor profiles in /etc/apparmor.d.
Cambios del modo-reclamar:

Perfil:     /usr/local/bin/server_file
Ruta:       /home/david/fichero.txt
Modo nuevo: owner r
Severidad:  4

 [1 - owner /home/*/fichero.txt r,]
  2 - owner /home/david/fichero.txt r,
(A)llow / [(D)eny] / (I)gnorar / (G)lob / Glob with (E)xtension / (N)uevo / Audi(t) / (O)wner permissions off / Abo(r)t / (F)inalizar

Perfil:     /usr/local/bin/server_file
Ruta:       /home/david/fichero.txt
Modo nuevo: owner r
Severidad:  4

  1 - owner /home/*/fichero.txt r,
 [2 - owner /home/david/fichero.txt r,]
(A)llow / [(D)eny] / (I)gnorar / (G)lob / Glob with (E)xtension / (N)uevo / Audi(t) / (O)wner permissions off / Abo(r)t / (F)inalizar
Añadiendo owner /home/david/fichero.txt r, al perfil.

Perfil:         /usr/local/bin/server_file
Familia de red: inet
Tipo de zócalo: stream

 [1 - #include <abstractions/apache2-common>]
  2 - #include <abstractions/nameservice>
  3 - network inet stream,
(A)llow / [(D)eny] / (I)gnorar / Audi(t) / Abo(r)t / (F)inalizar

Perfil:         /usr/local/bin/server_file
Familia de red: inet
Tipo de zócalo: stream

  1 - #include <abstractions/apache2-common>
  2 - #include <abstractions/nameservice>
 [3 - network inet stream,]
(A)llow / [(D)eny] / (I)gnorar / Audi(t) / Abo(r)t / (F)inalizar
Añadiendo network inet stream, al perfil.

= Changed Local Profiles =

Los siguientes perfiles locales se cambiaron. ¿Quiere guardarlos?

 [1 - /usr/local/bin/server_file]
(S)ave Changes / Save Selec(t)ed Profile / [(V)iew Changes] / View Changes b/w (C)lean profiles / Abo(r)t
Writing updated profile for /usr/local/bin/server_file.

Perfilado: /usr/local/bin/server_file

Please start the application to be profiled in
another window and exercise its functionality now.

Once completed, select the "Scan" option below in
order to scan the system logs for AppArmor events.

For each AppArmor event, you will be given the
opportunity to choose whether the access should be
allowed or denied.

[(S)can system log for AppArmor events] / (F)inalizar
Setting /usr/local/bin/server_file to enforce mode.

Reloaded AppArmor profiles in enforce mode.

¡Considere la posibilidad de aportar su nuevo perfil!
Mire la siguiente página wiki para tener más información:
https://gitlab.com/apparmor/apparmor/wikis/Profiles

Generacióin del perfil finalizada para /usr/local/bin/server_file

Al final pulsamos F y nos genera el perfil. Podemos ver que nuestro perfil se encuentra activo y en 'enforce mode':

❯ sudo aa-status
apparmor module is loaded.
38 profiles are loaded.
36 profiles are in enforce mode.
.
.
/usr/local/bin/server_file
.
.

Se puede permitir o denegar el acceso a la red. Tambien es posible por ejemplo, limitar solo a tráfico TCP sobre IPv4. Estan planeadas mejoras futuras que agreguen un control más detallado como limitar el tráfico a direcciones IP y puertos específicos

Ahora si intentamos lanzar server_file con otro fichero que no sea fichero.txt:

❯ server_file 1973 /etc/passwd
[server_file.c:74]:
  [+] Error al abrir el fichero: /etc/passwd
  [+] errno: [Permiso denegado]

Tendremos un error de acceso. Aunque los permisos de fichero (DAC) nos permiten el acceso, los permisos del perfil AppArmor (MAC) nos limitan a usar solo fichero.txt. Podemos ver el perfil creado:

❯ sudo cat /etc/apparmor.d/usr.local.bin.server_file
# Last Modified: Wed Oct 14 22:44:05 2020
#include <tunables/global>

/usr/local/bin/server_file {
  #include <abstractions/base>

  network inet stream,

  /usr/local/bin/server_file mr,
  owner /home/david/fichero.txt r,

}

 

Para poder crear perfiles más complejos y poder ajustarlos de forma detallada tenemos que conocer el lenguaje de perfiles de AppArmor.  En la QuickProfileLanguaje de la documentación de AppArmor tenemos un buen resumen para empezar.

Si has llegado hasta aquí, ¡Enhorabuena!. Este es un tema denso, pero es un apartado muy importante de seguridad, a si que es interesante dedicarle algún tiempo para familiarizarse con el sistema. En futuras entradas ampliaremos un poco el uso de AppArmor.

Espero que sea de utilidad, cualquier feedback o sugerencia la pueden dejar en los comentarios!

 

(2 votos)
Etiquetado como :

Más en esta categoría:

« Virus en GNU/Linux: Binarios ELF

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.