Zapobiegaj procesowi otwierania nowego deskryptora pliku w systemie Linux, ale zezwalaj na odbieranie deskryptorów plików przez gniazda

9

Obecnie pracuję nad projektem, w którym mam proces nadrzędny, który konfiguruje parę gniazd, rozwidla, a następnie używa tej pary gniazd do komunikacji. Dziecko, jeśli chce otworzyć plik (lub dowolny inny zasób oparty na deskryptorze pliku), powinno zawsze udać się do elementu nadrzędnego, zażądać zasobu i pobrać fdgo za pośrednictwem pary gniazd. Ponadto chcę uniemożliwić dziecku samodzielne otwieranie dowolnego deskryptora pliku.

Natknąłem się na to, setrlimitco skutecznie uniemożliwia dziecku otwieranie nowych deskryptorów plików, ale wydaje się również, że unieważnia wszelkie deskryptory plików wysyłane przez początkowe połączenie gniazda. Czy jest jakaś metoda w systemie Linux, która pozwala jednemu procesowi otworzyć dowolny plik, wysłać deskryptor pliku do innych procesów i pozwolić im z nich korzystać, nie pozwalając tym innym procesom na samodzielne otwieranie dowolnego deskryptora pliku?

W moim przypadku może to być dowolna konfiguracja jądra, wywołanie systemowe itp., O ile można ją zastosować po rozwidleniu i tak długo, jak dotyczy wszystkich deskryptorów plików (nie tylko plików, ale także gniazd, par gniazd itp.).

jklmnn
źródło
1
Możesz być zainteresowany seccomp.
user253751

Odpowiedzi:

6

To, co masz tutaj, to dokładnie przypadek użycia seccomp .

Za pomocą seccomp możesz filtrować połączenia systemowe na różne sposoby. Co chcesz robić w tej sytuacji jest to, zaraz po fork(), aby zainstalować seccompfiltr, który wykluczy stosowanie open(2), openat(2), socket(2)(i więcej). Aby to zrobić, możesz wykonać następujące czynności:

  1. Najpierw utwórz kontekst seccomp przy użyciu seccomp_init(3)domyślnego zachowania SCMP_ACT_ALLOW.
  2. Następnie dodaj regułę do kontekstu, używając seccomp_rule_add(3)dla każdego połączenia systemowego, którego chcesz odmówić. Możesz użyć SCMP_ACT_KILLdo zabicia procesu, jeśli próba syscall zostanie podjęta, SCMP_ACT_ERRNO(val)aby syscall nie zwrócił określonej errnowartości lub innej actionwartości zdefiniowanej na stronie podręcznika.
  3. Załaduj kontekst za pomocą, seccomp_load(3)aby był skuteczny.

Zanim przejdziesz dalej, ZAUWAŻ, że takie podejście do czarnej listy jest ogólnie słabsze niż podejście do białej listy. Pozwala na każde wywołanie systemowe, które nie jest wyraźnie niedozwolone i może spowodować obejście filtra . Jeśli uważasz, że proces potomny, który chcesz wykonać, może złośliwie próbować ominąć filtr lub jeśli już wiesz, które wywołania systemowe będą potrzebne dzieciom, lepiej zastosować podejście z białej listy i powinieneś zrobić coś przeciwnego: utwórz filtr z domyślną akcją SCMP_ACT_KILLi zezwól na wymagane wywołania systemowe za pomocą SCMP_ACT_ALLOW. Pod względem kodu różnica jest minimalna (biała lista jest prawdopodobnie dłuższa, ale kroki są takie same).

Oto przykład powyższego (robię to exit(-1)w przypadku błędu tylko dla uproszczenia):

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

Teraz w swoim programie możesz wywołać powyższą funkcję, aby zastosować filtr seccomp zaraz po fork():

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

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

    // Child code here...

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

Kilka ważnych uwag na temat seccomp:

  • Raz zastosowanego filtra seccomp nie można usunąć ani zmienić w procesie.
  • Jeśli filtr zezwala fork(2)lub clone(2)jest dozwolony, wszelkie procesy potomne będą ograniczone przez ten sam filtr.
  • Jeśli execve(2)jest to dozwolone, istniejący filtr zostanie zachowany podczas połączenia z execve(2).
  • Jeśli prctl(2)zezwolenie na syscall jest dozwolone, proces może zastosować kolejne filtry.
Marco Bonelli
źródło
2
Czarna lista piaskownicy? Ogólnie zły pomysł, zamiast tego chcesz dodać do białej listy.
Deduplicator
@Deduplicator Wiem, ale podejście do białej listy nie ma zastosowania do sytuacji OP, ponieważ chcą po prostu zabronić otwierania nowych deskryptorów plików. Dodaję notatkę na końcu.
Marco Bonelli
Dzięki za odpowiedź, właśnie tego potrzebuję. Biała lista jest rzeczywiście lepsza dla aplikacji, którą pierwotnie zamierzałem. Po prostu nie miałem na myśli, że jest więcej rzeczy, które należy ograniczyć niż tylko otwieranie deskryptorów plików.
jklmnn
@ jklmnn tak, dokładnie. Rzeczywiście po prostu zapomniałem, że socket(2)to też może stworzyć fd, więc to też powinno zostać zablokowane. Jeśli znasz proces potomny, podejście do białej listy jest lepsze.
Marco Bonelli
@MarcoBonelli Biała lista jest zdecydowanie lepsza. Bezceremonialny, creat(), dup(), i dup2()są wszystkie wywołania systemowe Linux że deskryptorów powrotu. Na czarnej liście jest wiele sposobów ...
Andrew Henle,