Evite que el proceso abra un nuevo descriptor de archivo en Linux pero permita recibir descriptores de archivo a través de sockets

9

Actualmente estoy trabajando en un proyecto en el que tengo un proceso principal que configura un par de sockets, se bifurca y luego usa este par de sockets para comunicarse. El hijo, si quiere abrir un archivo (o cualquier otro recurso basado en descriptores de archivo) siempre debe ir al padre, solicitar el recurso y recibirlo a fdtravés del socketpair. Además, quiero evitar que el niño abra cualquier descriptor de archivo por sí mismo.

Me topé con lo setrlimitque impide que el niño abra nuevos descriptores de archivos, pero también parece invalidar cualquier descriptor de archivos enviado a través de la conexión inicial del socket. ¿Hay algún método en Linux que permita que un solo proceso abra cualquier archivo, envíe su descriptor de archivo a otros procesos y les permita usarlos sin permitir que estos otros procesos abran cualquier descriptor de archivo por sí mismos?

Para mi caso de uso, puede ser cualquier configuración de kernel, llamada al sistema, etc., siempre y cuando se pueda aplicar después de fork y siempre que se aplique a todos los descriptores de archivos (no solo archivos, sino también sockets, socketpairs, etc.).

jklmnn
fuente
1
Quizás te interese seccomp.
user253751

Respuestas:

6

Lo que tienes aquí es exactamente el caso de uso de seccomp .

Usando seccomp, puede filtrar las llamadas al sistema de diferentes maneras. Lo que se quiere hacer en esta situación es, justo después fork(), la instalación de un seccompfiltro que no permite el uso de open(2), openat(2), socket(2)(y más). Para lograr esto, puede hacer lo siguiente:

  1. Primero, cree un contexto seccomp seccomp_init(3)con el comportamiento predeterminado de SCMP_ACT_ALLOW.
  2. Luego agregue una regla al contexto usando seccomp_rule_add(3)para cada llamada al sistema que desee negar. Puede usar SCMP_ACT_KILLpara matar el proceso si se intenta la llamada al sistema, SCMP_ACT_ERRNO(val)para hacer que la llamada al sistema no devuelva el errnovalor especificado o cualquier otro actionvalor definido en la página del manual.
  3. Cargue el contexto usando seccomp_load(3)para hacerlo efectivo.

Antes de continuar, TENGA EN CUENTA que un enfoque de lista negra como este es en general más débil que un enfoque de lista blanca. Permite cualquier llamada al sistema que no esté explícitamente prohibida, y podría provocar un bypass del filtro . Si cree que el proceso secundario que desea ejecutar podría estar tratando maliciosamente de evitar el filtro, o si ya sabe qué syscalls necesitarán los menores, un enfoque de lista blanca es mejor, y debe hacer lo contrario de lo anterior: crear filtro con la acción predeterminada de SCMP_ACT_KILLy permitir las llamadas al sistema necesarias con SCMP_ACT_ALLOW. En términos de código, la diferencia es mínima (la lista blanca probablemente sea más larga, pero los pasos son los mismos).

Aquí hay un ejemplo de lo anterior (lo estoy haciendo exit(-1)en caso de error solo por simplicidad):

#include <stdlib.h>
#include <seccomp.h>

static void secure(void) {
    int err;
    scmp_filter_ctx ctx;

    int blacklist[] = {
        SCMP_SYS(open),
        SCMP_SYS(openat),
        SCMP_SYS(creat),
        SCMP_SYS(socket),
        SCMP_SYS(open_by_handle_at),
        // ... possibly more ...
    };

    // Create a new seccomp context, allowing every syscall by default.
    ctx = seccomp_init(SCMP_ACT_ALLOW);
    if (ctx == NULL)
        exit(-1);

    /* Now add a filter for each syscall that you want to disallow.
       In this case, we'll use SCMP_ACT_KILL to kill the process if it
       attempts to execute the specified syscall. */

    for (unsigned i = 0; i < sizeof(blacklist) / sizeof(blacklist[0]); i++) {
        err = seccomp_rule_add(ctx, SCMP_ACT_KILL, blacklist[i], 0);
        if (err)
            exit(-1);
    }

    // Load the context making it effective.
    err = seccomp_load(ctx);
    if (err)
        exit(-1);
}

Ahora, en su programa, puede llamar a la función anterior para aplicar el filtro seccomp justo después de fork(), de esta manera:

child_pid = fork();
if (child_pid == -1)
    exit(-1);

if (child_pid == 0) {
    secure();

    // Child code here...

    exit(0);
} else {
    // Parent code here...
}

Algunas notas importantes sobre seccomp:

  • Un filtro seccomp, una vez aplicado, no puede ser eliminado o alterado por el proceso.
  • Si el filtro lo permite fork(2)o clone(2)no, los procesos secundarios estarán restringidos por el mismo filtro.
  • Si execve(2)está permitido, el filtro existente se conservará en una llamada a execve(2).
  • Si prctl(2)se permite la llamada al sistema, el proceso puede aplicar más filtros.
Marco Bonelli
fuente
2
Lista negra para una caja de arena? En general, es una mala idea, en su lugar, desea la lista blanca.
Deduplicador
@Deduplicator Lo sé, pero un enfoque de lista blanca no se aplica muy bien a la situación de OP ya que solo quieren no permitir la apertura de nuevos descriptores de archivo. Agregaré una nota al final.
Marco Bonelli
Gracias por la respuesta, eso es lo que necesito. Una lista blanca es realmente mejor para la aplicación que originalmente pretendía. Simplemente no tenía en mente que hay más cosas que uno debería restringir que solo abrir descriptores de archivos.
jklmnn
@jklmnn sí, exactamente. De hecho, simplemente olvidé que socket(2)también puede crear un fd, por lo que también debería bloquearse. Si conoce el proceso secundario, es mejor un enfoque de lista blanca.
Marco Bonelli
@MarcoBonelli Una lista blanca es definitivamente mejor. A primera vista, creat(), dup(), y dup2()son todas las llamadas del sistema Linux que los descriptores de fichero de retorno. Hay muchas maneras de evitar una lista negra ...
Andrew Henle