O processo e o modelo de sessão do Linux como parte do alerta e do monitoramento de segurança
Com o modelo de processo do Linux, disponível no Elastic, os usuários podem escrever regras de alerta bem direcionadas e obter insights mais profundos sobre o que exatamente está acontecendo em seus servidores e desktops Linux.
Neste post, forneceremos informações básicas sobre o modelo de processo do Linux, um aspecto importante de como as cargas de trabalho do Linux são representadas.
O Linux segue o modelo de processo do Unix da década de 70, que foi ampliado com as sessões conceituais da década de 80, a julgar pela época em que a chamada de sistema setsid() foi introduzida pelos primeiros documentos POSIX
O modelo de processo do Linux é uma boa abstração para registrar cargas de trabalho de computador (quais programas são executados) e para escrever regras para reagir a esses eventos. Ele oferece uma representação clara de quem fez o que, quando e em qual servidor para alerta, conformidade e caça a ameaças.
Capturar a criação de processos, a escalação de privilégios e a expectativa de vida oferece uma visão profunda de como as aplicações e os serviços são implementados e seus padrões normais de execução de programas. Depois que os padrões de execução normais são identificados, é possível escrever regras para enviar alertas quando ocorrerem padrões de execução anômalos.
Com informações detalhadas do processo, podemos escrever regras bem direcionadas para alertas, o que reduz a ocorrência de falsos positivos e fadiga de alertas. Isso também permite que as sessões do Linux sejam categorizadas como uma das seguintes:
- serviços autônomos iniciados na inicialização (por exemplo, cron)
- serviços que fornecem acesso remoto (por exemplo, sshd)
- acesso remoto interativo (provavelmente humano) (por exemplo, um terminal bash iniciado via ssh)
- acesso remoto não interativo (por exemplo, instalação de software pelo Ansible via ssh)
Essas categorizações permitem a criação de regras e revisões muito precisas. Por exemplo, é possível revisar todas as sessões interativas em servidores específicos em um período selecionado.
Este artigo descreve como funciona o modelo de processo do Linux e ajudará na criação de regras de alerta e resposta para eventos de carga de trabalho. Uma compreensão do modelo de processo do Linux também é um primeiro passo essencial para compreender os containers e os espaços de nome e os cgroups dos quais eles são compostos.
Captura de modelo de processo vs. logs de chamadas de sistema
Capturar alterações no modelo de sessão em termos de novos processos, novas sessões, processos existentes etc. é mais simples e claro do que capturar as chamadas de sistema usadas para implementar essas alterações. O Linux tem aproximadamente 400 chamadas de sistema e não as reestrutura depois de lançadas. Essa abordagem mantém uma interface binária de aplicação (ABI, pelas iniciais em inglês) estável, o que significa que os programas compilados para execução no Linux anos atrás devem continuar a ser executados no Linux hoje sem que sejam reconstruídos a partir do código-fonte.
Novas chamadas de sistema são adicionadas para melhorar as funcionalidades ou a segurança em vez de reestruturar as chamadas de sistema existentes (evita quebrar a ABI). O resultado é que o mapeamento de uma lista ordenada por tempo de chamadas de sistema e seus parâmetros para as ações lógicas que elas executam requer uma experiência significativa. Além disso, chamadas de sistema mais recentes, como as de io_uring, tornam possível ler e gravar arquivos e soquetes sem chamadas de sistema adicionais, usando memória mapeada entre o kernel e o espaço do usuário.
Por outro lado, o modelo de processo é estável (não mudou muito desde a década de 70), mas ainda cobre de forma abrangente as ações tomadas em um sistema quando se inclui acesso a arquivos, rede e outras operações lógicas.
Formação do processo: init é o primeiro processo após a inicialização
Quando o kernel do Linux é iniciado, ele cria um processo especial chamado “processo init”. Um processo incorpora a execução de um ou mais programas. O processo init sempre tem o ID de processo (PID) 1 e é executado com um ID de usuário 0 (root). A maioria das distribuições modernas do Linux usa o systemd como programa executável do processo init.
O trabalho do init é iniciar os serviços configurados, como bancos de dados, servidores web e serviços de acesso remoto, como o sshd. Esses serviços são normalmente encapsulados em suas próprias sessões, o que simplifica o início e a interrupção dos serviços, agrupando todos os processos de cada serviço sob um único ID de sessão (SID).
O acesso remoto, como por meio do protocolo SSH a um serviço sshd, criará uma nova sessão do Linux para o usuário que o acessa. Essa sessão executará inicialmente o programa solicitado pelo usuário remoto (geralmente um shell interativo), e todos os processos associados terão o mesmo SID.
A mecânica de criação de um processo
Todo processo, exceto o processo init, tem um único processo pai. Cada processo tem um PPID, o ID do processo pai (0/no-parent no caso do init). A reparação do pai poderá ocorrer se um processo pai sair de uma forma que não encerre também os processos filho.
A reparação geralmente escolhe o init como o novo pai, e o init tem um código especial para limpeza dos filhos adotados quando eles saem. Sem essa adoção e o código de limpeza, os processos filho órfãos se tornariam processos “zumbis” (sem brincadeira!). Eles ficam por aí até que seus pais os colham para que possam examinar seu código de saída — um indicador para saber se o programa filho concluiu suas tarefas com êxito.
O advento de “containers”, em particular os espaços de nome pid, exigiu a capacidade de designar processos diferentes do init como “sub-reapers” (processos dispostos a adotar processos órfãos). Normalmente, os sub-reapers são o primeiro processo em um container. Isso é feito porque os processos no container não conseguem “ver” processos nos espaços de nome pid ancestrais (ou seja, seu valor de PPID não faria sentido se o pai estivesse em um espaço de nome pid ancestral).
Para criar um processo filho, o pai se clona por meio da chamada de sistema fork() ou clone(). Após a bifurcação/clone, a execução continua imediatamente tanto no pai quanto no filho (ignorando a opção CLONE_VFORK das chamadas vfork() e clone()), mas ao longo de caminhos de código diferentes em virtude do valor do código de retorno de fork()/clone().
Você leu corretamente: uma chamada de sistema fork()/clone() fornece um código de retorno em dois processos diferentes! O pai recebe o PID do filho como seu código de retorno, e o filho recebe 0 para que o código compartilhado do pai e do filho possa se ramificar com base nesse valor. Existem algumas nuances de clonagem com pais multithread e memória copy-on-write para eficiência que precisam ser elaboradas aqui. O processo filho herda o estado da memória do pai e seus arquivos abertos, soquetes de rede e terminal de controle, se houver.
Normalmente, o processo pai capturará o PID do filho para monitorar seu ciclo de vida (veja a colheita acima). O comportamento do processo filho depende do programa que se clonou (ele fornece um caminho de execução para ser seguido com base no código de retorno de fork()).
Um servidor web como o nginx pode se clonar, criando um processo filho para lidar com conexões http. Em casos como esse, o processo filho não executa um novo programa, mas simplesmente executa um caminho de código diferente no mesmo programa para lidar com conexões http. Lembre-se de que o valor de retorno de um clone ou bifurcação informa ao filho que ele é o filho para que ele possa escolher esse caminho de código.
Processos de shell interativos (por exemplo, bash, sh, fish, zsh etc. com um terminal de controle), possivelmente de uma sessão ssh, clonam-se sempre que um comando é inserido. O processo filho, ainda executando um caminho de código do pai/shell, faz muito trabalho configurando descritores de arquivo para redirecionamento de E/S, definindo o grupo de processos e muito mais antes que o caminho de código no filho chame execve() ou uma chamada de sistema similar para executar um programa diferente dentro desse processo.
Se você digitar ls em seu shell, ele bifurca seu shell, a configuração descrita acima é feita pelo shell/filho e, então, o programa ls (geralmente do arquivo /pt/usr/bin/ls) é executado para substituir o conteúdo desse processo pelo código de máquina para ls. Este artigo sobre a implementação do controle de trabalhos do shell oferece uma excelente visão sobre o funcionamento interno dos shells e grupos de processos.
É importante observar que um processo pode chamar execve() mais de uma vez e, portanto, os modelos de dados de captura de carga de trabalho também devem lidar com isso. Isso significa que um processo pode se tornar muitos programas diferentes antes de sair — não apenas seu programa de processo pai opcionalmente seguido por um único programa. Veja o exec builtin command (comando integrado de execução) do shell para conhecer uma maneira de fazer isso em um shell (ou seja, substituir o programa do shell por outro no mesmo processo).
Outro aspecto da execução de um programa em um processo é que alguns descritores de arquivos abertos (aqueles marcados como close-on-exec) podem ser fechados antes da execução do novo programa, enquanto outros podem permanecer disponíveis para o novo programa. Lembre-se de que uma única chamada fork()/clone() fornece um código de retorno em dois processos, o pai e o filho. A chamada de sistema execve() também é estranha no sentido de que um execve() bem-sucedido não tem um código de retorno para sucesso porque resulta na execução de um novo programa, portanto não há para onde retornar, exceto quando execve() falha.
Criando novas sessões
Atualmente, o Linux cria novas sessões com uma única chamada de sistema, setsid(), que é chamada pelo processo que se torna o novo líder da sessão. Essa chamada de sistema geralmente faz parte do caminho de código do filho clonado executado antes da execução de outro programa nesse processo (ou seja, é planejada e incluída no código do processo pai). Todos os processos dentro de uma sessão compartilham o mesmo SID, que é igual ao PID do processo chamado setsid(), também conhecido como líder da sessão. Em outras palavras, um líder de sessão é qualquer processo com um PID que corresponda ao seu SID. A saída do processo líder da sessão disparará o encerramento de seus grupos de processos filho imediatos.
Criando novos grupos de processos
O Linux usa grupos de processos para identificar um grupo de processos trabalhando juntos em uma sessão. Todos eles terão o mesmo SID e ID de grupo de processo (PGID). O PGID é o PID do líder do grupo de processos. Não há um status especial para o líder do grupo de processos; ele pode sair sem ter nenhum efeito sobre os demais membros do grupo de processos, e eles retêm o mesmo PGID, mesmo que o processo com esse PID não exista mais.
Observe que, mesmo com o pid-wrap (reutilização de um PID usado recentemente em sistemas ocupados), o kernel do Linux garante que o PID de um líder de grupo de processos que saiu não seja reutilizado até que todos os membros desse grupo de processos tenham saído (ou seja, não há nenhum maneira de seu PGID se referir acidentalmente a um novo processo).
Os grupos de processos são valiosos para comandos de pipeline de shell como:
cat foo.txt | grep bar | wc -l
Isso cria três processos para três programas diferentes (cat, grep e wc) e os conecta com pipes. Os shells criarão um novo grupo de processos mesmo para comandos de programa único, como o ls. O objetivo dos grupos de processos é permitir o direcionamento de sinais para um conjunto de processos e identificar um conjunto de processos (o grupo de processos de primeiro plano) com permissão de acesso total de leitura e gravação ao terminal de controle de sua sessão, se houver.
Em outras palavras, control-C no seu shell enviará um sinal de interrupção para todos os processos no grupo de processos de primeiro plano (o valor de PGID negativo como o PID alvo do sinal discrimina entre o grupo e o próprio processo líder do grupo de processos). A associação do terminal de controle garante que os processos que estiverem lendo a entrada do terminal não concorram entre si e não causem problemas (a saída do terminal pode ser permitida de grupos de processos que não sejam de primeiro plano).
Usuários e grupos
Conforme mencionamos acima, o processo init tem o ID de usuário 0 (root). Cada processo tem um usuário e um grupo associados, e estes podem ser usados para restringir o acesso a chamadas de sistema e arquivos. Usuários e grupos têm IDs numéricos e podem ter um nome associado como root ou ms. O usuário raiz (root) é o superusuário que pode fazer qualquer coisa e só deve ser usado quando for absolutamente necessário, por motivos de segurança.
O kernel do Linux só se preocupa com os IDs. Os nomes são opcionais e são fornecidos para conveniência humana pelos arquivos /etc/passwd e /etc/group. O Name Service Switch (NSS) permite que esses arquivos sejam estendidos com usuários e grupos do LDAP e de outros diretórios (use getent passwd se quiser ver a combinação de /pt/etc/passwd e usuários fornecidos pelo NSS).
Cada processo pode ter vários usuários e grupos associados a ele (grupos reais, efetivos, salvos e suplementares). Veja mais informações em man 7 credentials (em inglês).
O aumento do uso de containeres cujos sistemas de arquivos raiz são definidos por imagens de containers aumentou a probabilidade de /pt/etc/passwd e /pt/etc/group estarem ausentes ou faltarem alguns nomes de IDs de usuários e grupos que possam estar em uso. Como o kernel do Linux não se importa com esses nomes, apenas com os IDs, fica tudo bem.
Resumo
O modelo de processo do Linux fornece uma maneira precisa e sucinta de representar as cargas de trabalho do servidor, o que, por sua vez, permite a criação de regras e revisões de alerta bem direcionadas. Uma representação por sessão fácil de entender do modelo de processo no seu navegador forneceria uma excelente visão das cargas de trabalho do servidor.
Você pode começar com uma avaliação gratuita do Elastic Cloud por 14 dias. Ou baixe a versão autogerenciada do Elastic Stack também gratuitamente.
Aprendendo mais
As páginas de manual do Linux são uma excelente fonte de informações. As páginas de manual abaixo (todas em inglês) contêm detalhes do modelo de processo do Linux descrito acima:
- man 2 fork
- man 2 clone
- man 2 setsid
- man 2 execve
- man 2 exit
- man 7 credentials
- man 7 namespaces
- process groups - implementing shell job control (grupos de processos — implementando controle de trabalhos do shell)
- shell builtin commands (comandos integrados do shell)