Impedir que o processo abra um novo descritor de arquivos no Linux, mas permita o recebimento de descritores de arquivos por soquetes

9

Atualmente, estou trabalhando em um projeto no qual tenho um processo pai que configura um socketpair, bifurca e usa esse socketpair para se comunicar. O filho, se quiser abrir um arquivo (ou qualquer outro recurso baseado em descritor de arquivo) deve sempre ir para o pai, solicitar o recurso e receber o fdenvio por meio do socketpair. Além disso, quero impedir que a criança abra qualquer descritor de arquivo por si só.

Eu tropecei sobre o setrlimitque impede com êxito o filho de abrir novos descritores de arquivo, mas também parece invalidar quaisquer descritores de arquivo enviados pela conexão inicial do soquete. Existe algum método no Linux que permita que um único processo abra qualquer arquivo, envie seu descritor de arquivo para outros processos e permita que eles os usem sem permitir que esses outros processos abram qualquer descritor de arquivo por si mesmos?

Para o meu caso de uso, pode ser qualquer configuração do kernel, chamada do sistema etc., desde que possa ser aplicada após o fork e desde que seja aplicável a todos os descritores de arquivos (não apenas arquivos, mas também sockets, socketpairs, etc.).

jklmnn
fonte
11
Você pode estar interessado em seccomp.
user253751 14/01

Respostas:

6

O que você tem aqui é exatamente o caso de uso do seccomp .

Usando o seccomp, você pode filtrar os syscalls de maneiras diferentes. O que você quer fazer nesta situação é, logo depois fork(), instalar um seccompfiltro que não permite o uso de open(2), openat(2), socket(2)(e mais). Para fazer isso, você pode fazer o seguinte:

  1. Primeiro, crie um contexto seccomp usando seccomp_init(3)o comportamento padrão de SCMP_ACT_ALLOW.
  2. Em seguida, adicione uma regra ao contexto usando seccomp_rule_add(3)para cada syscall que você deseja negar. Você pode usar SCMP_ACT_KILLpara interromper o processo se tentar o syscall, SCMP_ACT_ERRNO(val)fazer com que o syscall falhe ao retornar o errnovalor especificado ou qualquer outro actionvalor definido na página do manual.
  3. Carregue o contexto usando seccomp_load(3)para torná-lo eficaz.

Antes de continuar, NOTE que uma abordagem de lista negra como esta é geralmente mais fraca que uma abordagem de lista de permissões. Ele permite qualquer syscall que não seja explicitamente proibido e pode resultar em um desvio do filtro . Se você acredita que o processo filho que você deseja executar pode estar tentando maliciosamente evitar o filtro, ou se você já sabe quais syscalls serão necessários pelos filhos, uma abordagem da lista de desbloqueio é melhor e você deve fazer o oposto do acima: crie um filtro com a ação padrão de SCMP_ACT_KILLe permita os syscalls necessários com SCMP_ACT_ALLOW. Em termos de código, a diferença é mínima (a lista de permissões provavelmente é mais longa, mas as etapas são as mesmas).

Aqui está um exemplo do exposto acima (estou fazendo exit(-1)em caso de erro apenas por uma questão de simplicidade):

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

Agora, no seu programa, você pode chamar a função acima para aplicar o filtro seccomp logo após o fork()seguinte:

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

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

    // Child code here...

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

Algumas notas importantes sobre o seccomp:

  • Um filtro seccomp, uma vez aplicado, não pode ser removido ou alterado pelo processo.
  • Se fork(2)ou clone(2)for permitido pelo filtro, qualquer processo filho será restringido pelo mesmo filtro.
  • Se execve(2)for permitido, o filtro existente será preservado em uma chamada para execve(2).
  • Se o prctl(2)syscall for permitido, o processo poderá aplicar outros filtros.
Marco Bonelli
fonte
2
Lista negra de uma caixa de areia? Geralmente, é uma péssima idéia, você deseja colocar a lista de permissões.
Deduplicator
@ Desduplicador, eu sei, mas uma abordagem de lista de permissões não se aplica muito bem à situação do OP, pois eles querem proibir a abertura de novos descritores de arquivos. Vou adicionar uma nota no final.
Marco Bonelli
Obrigado pela resposta, é disso que eu preciso. Uma lista de permissões é realmente melhor para o aplicativo que eu pretendia originalmente. Eu simplesmente não tinha em mente que há mais coisas que se deve restringir do que apenas abrir descritores de arquivo.
jklmnn 14/01
@jklmnn sim, exatamente. Na verdade, eu esqueci que socket(2)também pode criar um fd, então isso também deve ser bloqueado. Se você conhece o processo filho, uma abordagem da lista de permissões é melhor.
Marco Bonelli
@MarcoBonelli Uma lista de permissões é definitivamente melhor. De imediato, creat(), dup()e dup2()são todas as chamadas de sistema Linux que os descritores de arquivo retorno. Há uma série de maneiras em torno de uma lista negra ...
Andrew Henle