Proteger com segurança scripts de usuário em um programa C ++

8

Eu tenho trabalhado em um projeto pessoal em C # cujo objetivo é mais ou menos permitir ao usuário executar scripts escritos por outros usuários e restringir as permissões desse script. Meu programa compila os scripts usando uma biblioteca de terceiros, coloca-os na caixa de areia usando os mecanismos do .NET Code Access Security e garante que eles tenham apenas as permissões que o usuário deseja conceder.

Em termos gerais, meus requisitos de segurança são:

  • o usuário deve poder restringir o acesso do script não confiável a apenas certas partes do sistema de arquivos, incluindo a proibição de todo acesso ao sistema de arquivos
  • o usuário deve poder restringir as conexões de rede do script não confiável a apenas determinados endereços IP ou nomes de host, incluindo a proibição de todas as conexões de rede
  • tudo bem se o script do usuário conseguir travar ou encerrar o aplicativo host, mas o script do usuário não puder contornar as restrições de permissão (por exemplo, negação de serviço é aceitável, violação não é)

Estou pensando em tentar fazer algo semelhante em C ++, como uma espécie de exercício pessoal. Obviamente, as coisas são mais complicadas ao executar o código nativo diretamente, mesmo que os scripts do usuário sejam escritos em uma linguagem de script como Lua.

A primeira abordagem em que consigo pensar é inserir meus próprios ganchos nas funções da biblioteca padrão dos ambientes de script. Por exemplo, se a linguagem de script for Lua, em vez de expor o io.open normalmente, eu teria que expor um wrapper que verificasse os argumentos nas permissões do script antes de passá-los para a implementação original.

Minha preocupação com essa abordagem é que ela aumenta drasticamente a quantidade de meu próprio código responsável pela segurança e, portanto, uma potencial vulnerabilidade de segurança que eu mesmo escrevi. Em outras palavras, ao trabalhar com o .NET CAS, posso confiar que a Microsoft fez seu trabalho bem no código de área restrita, em vez de precisar confiar no meu próprio código de área restrita.

Existem alternativas que não conheço?

Vojislav Stojkovic
fonte
As técnicas de conteinerização do @RobertHarvey Linux, como o Seccomp, geralmente ajudam apenas ao executar o código não confiável em um processo separado, mas não são aplicáveis ​​ao isolamento no processo. Obviamente, uma solução bastante simples para o problema poderia incluir exatamente isso, um processo separado que atua como um serviço para o programa principal e se comunica apenas pelos mecanismos do IPC. Gostaria de saber se o Windows tem recursos semelhantes de isolamento no nível do sistema operacional?
amon
1
@ amon: Você quer dizer como um ... processo do Windows ? Claro que sim.
Robert Harvey
Primeiro, você já introduziu a maior vulnerabilidade em seu sistema: Código Estrangeiro . Tudo o resto é uma tentativa de mitigar esse dano. Essencialmente, você está construindo um intérprete que, esperançosamente, executa esse código estrangeiro com segurança. Continue aprimorando sua compreensão de interpretação e aplique essas idéias para reforçar os comportamentos e a segurança relativa que você deseja. Em segundo lugar, não presuma que uma caixa de areia é segura; sempre adicione camadas extras de defesa.
Kain0_0 14/03/19
1
@VojislavStojkovic Você não pode simplesmente usar um mecanismo javascript? Por padrão, esses mecanismos não têm acesso ao sistema de arquivos (não tem muita certeza da rede, mas você pode verificar). Um exemplo é o QJSEngine . Também com esses mecanismos, você pode fornecer funções personalizadas de código c ++ ao javascript para interoperabilidade.
RandomGuy

Respostas:

1

Como outros já declararam, a execução de código estrangeiro é o maior problema nesse tipo de implementação. Como o comentário de Kain0_0 sugeriu, uma VM seria a maneira mais apropriada de manter a liberdade de código externo sem comprometer a máquina host (demais). Isso é basicamente o que os serviços de Integração Contínua como o CircleCI fazem.

Isso também facilita muito a implementação da interface, pois você pode obter imagens com todos os recursos de configuração e segurança desejados. Você também não precisa se preocupar se o código deles conseguirá ser executado em seu host.

Então, para isso, eu:

  • Faça instantâneos dos ambientes de script do usuário que quero cobrir com o Docker (um ambiente para C #, outro para Python, etc, com as configurações de segurança apropriadas)

  • Mediante solicitação do usuário, gire as instâncias relevantes do Docker pelo gatilho de código, injetando o script externo no ponto de entrada da instância do Docker.

  • O código é executado na instância do Docker com permissões de usuário, os arquivos são gravados, talvez uma conexão seja feita aqui e ali, obtenha a saída e o ambiente seja destruído

  • Como os contêineres do Docker são executados como processos, eles podem ser finalizados com bastante facilidade, ou seja, se excederem um determinado limite de tempo. Se houver um erro, eles podem terminar imediatamente.

Basicamente, faça com que seu código principal gerencie a lógica de acionamento do usuário, injeção de ponto de entrada e destruição automática do Docker, que é responsável por fazer o sandbox para esse tipo de operação.

lucasgcb
fonte
1
Ótima resposta. Se houver algo a acrescentar à sua resposta, todas as soluções populares de sandbox apresentam inúmeras falhas, permitindo que o código malicioso escape da sandbox. Isso é verdade para o CAS (por exemplo, CVE-2015-2504) e ainda mais verdadeiro para Java (explora o plug-in Java do navegador muito numeroso para listar aqui). Embora o Docker também tenha vulnerabilidades, tenho a impressão de que a superfície de ataque é muito menor se comparada a qualquer coisa implementada no nível de uma determinada linguagem / estrutura.
Arseni Mourzenko