Impedire al processo di aprire un nuovo descrittore di file su Linux ma consentire la ricezione di descrittori di file tramite socket


9

Attualmente sto lavorando a un progetto in cui ho un processo padre che imposta un socketpair, forks e quindi utilizza questo socketpair per comunicare. Il bambino, se vuole aprire un file (o qualsiasi altra risorsa basata sul descrittore di file) dovrebbe sempre andare al genitore, richiedere la risorsa e ricevere l' fdinvio tramite socketpair. Inoltre, voglio impedire al bambino di aprire qualsiasi descrittore di file da solo.

Mi sono imbattuto in ciò setrlimitche impedisce al bambino di aprire nuovi descrittori di file, ma sembra anche invalidare tutti i descrittori di file inviati tramite la connessione socket iniziale. Esiste un metodo su Linux che consente a un singolo processo di aprire qualsiasi file, inviare il suo descrittore di file ad altri processi e consentire loro di usarli senza consentire a questi altri processi di aprire qualsiasi descrittore di file da soli?

Per il mio caso d'uso che può essere qualsiasi configurazione del kernel, chiamata di sistema, ecc. Purché possa essere applicato dopo il fork e fintanto che si applica a tutti i descrittori di file (non solo file ma anche socket, socket, ecc.).


1
Potresti essere interessato a seccomp.
user253751

Risposte:


6

Quello che hai qui è esattamente il caso d'uso di seccomp .

Usando seccomp, puoi filtrare syscalls in diversi modi. Che cosa si vuole fare in questa situazione è, subito dopo fork(), l'installazione di un seccompfiltro che non consente l'uso di open(2), openat(2), socket(2)(e più). A tale scopo, è possibile effettuare le seguenti operazioni:

  1. Innanzitutto, crea un contesto seccomp usando seccomp_init(3)il comportamento predefinito di SCMP_ACT_ALLOW.
  2. Quindi aggiungi una regola al contesto usando seccomp_rule_add(3)per ogni syscall che vuoi negare. È possibile utilizzare SCMP_ACT_KILLper terminare il processo se si tenta la syscall, SCMP_ACT_ERRNO(val)per far sì che la syscall non riesca a restituire il errnovalore specificato o qualsiasi altro actionvalore definito nella pagina del manuale.
  3. Carica il contesto usando seccomp_load(3)per renderlo efficace.

Prima di continuare, NOTA che un approccio di lista nera come questo è in generale più debole di un approccio di lista bianca. Consente qualsiasi syscall che non è esplicitamente vietato e potrebbe comportare un bypass del filtro . Se ritieni che il processo figlio che desideri eseguire potrebbe tentare di evitare in modo dannoso il filtro o se sai già quali syscalls saranno necessari ai bambini, un approccio alla whitelist è migliore e dovresti fare l'opposto di quanto sopra: crea un filtro con l'azione predefinita di SCMP_ACT_KILLe consenti le syscalls necessarie con SCMP_ACT_ALLOW. In termini di codice la differenza è minima (la whitelist è probabilmente più lunga, ma i passaggi sono gli stessi).

Ecco un esempio di quanto sopra (lo sto facendo exit(-1)in caso di errore solo per semplicità):

#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);
}

Ora, nel tuo programma, puoi chiamare la funzione sopra per applicare il filtro seccomp subito dopo fork(), in questo modo:

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

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

    // Child code here...

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

Alcune note importanti su seccomp:

  • Un filtro seccomp, una volta applicato, non può essere rimosso o modificato dal processo.
  • Se fork(2)o clone(2)sono consentiti dal filtro, tutti i processi figlio saranno vincolati dallo stesso filtro.
  • Se execve(2)consentito, il filtro esistente verrà conservato durante una chiamata a execve(2).
  • Se la prctl(2)syscall è consentita, il processo è in grado di applicare ulteriori filtri.

2
Lista nera per un sandbox? Generalmente una cattiva idea, si desidera invece la whitelisting.
Deduplicatore il

@Deduplicator Lo so, ma un approccio di whitelist non si applica molto bene alla situazione di OP poiché vogliono solo impedire l'apertura di nuovi descrittori di file. Aggiungerò una nota alla fine.
Marco Bonelli,

Grazie per la risposta, ecco di cosa ho bisogno. Una whitelist è davvero migliore per l'applicazione che inizialmente intendevo. Non avevo in mente che ci fossero più cose che si dovrebbero limitare oltre all'apertura dei descrittori di file.
jklmnn,

@jklmnn sì, esattamente. In effetti ho appena dimenticato che socket(2)può anche creare un fd, quindi anche questo dovrebbe essere bloccato. Se conosci il processo figlio è meglio un approccio alla whitelist.
Marco Bonelli,

@MarcoBonelli Una whitelist è decisamente migliore. Lì per lì, creat(), dup(), e dup2()sono tutte le chiamate di sistema Linux che descrittori di file di ritorno. Ci sono molti modi per aggirare una lista nera ...
Andrew Henle,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.