Set-UID, ambiente e descritores
Seguir a autoridade de um processo, a interpretação pela shell e os recursos herdados.
Um programa de consulta pode abrir um ficheiro com autoridade que o utilizador não tem. A segurança depende de como escolhe o programa externo, interpreta os argumentos e entrega os recursos que já abriu. A mudança de UID é apenas uma parte desse percurso.
Ambiente em fork e execve
As variáveis de ambiente são pares de nomes e valores disponíveis ao processo. Por exemplo, PATH influencia a procura de executáveis em chamadas que fazem essa procura. Não é uma lista de permissões: escolher onde procurar e poder executar são decisões diferentes.
Considera um processo com PATH=/usr/bin:/bin e uma variável própria MODO=consulta:
| Operação | Ambiente resultante |
|---|---|
fork() | O filho recebe uma cópia do ambiente do pai. |
O filho muda MODO | A sua cópia muda; não altera automaticamente o ambiente do pai. |
execve(caminho, argv, envp) | O novo programa recebe os pares indicados por envp. |
execve substitui o programa no mesmo processo. Não cria um filho e não escolhe automaticamente o ambiente do pai. Passar environ transmite o ambiente corrente; passar uma lista terminada em ponteiro nulo permite selecionar os pares. A variante com envp=NULL usada pelo guião é aceite em Linux, mas não é uma interface portátil para um ambiente vazio.
UID real e UID efetivo
O UID real identifica normalmente o utilizador que iniciou o processo. O UID efetivo participa nas decisões de acesso. Ao executar um binário Set-UID, o sistema pode mudar o UID efetivo para o dono do executável, conservando o UID real. Por exemplo, um binário de root iniciado por um utilizador com UID 1000 pode ter UID real 1000 e efetivo 0.
Isto depende das condições de execução. Montagens nosuid, no_new_privs e outras restrições podem impedir o efeito Set-UID. Também não se deve extrapolar de um binário para um script interpretado: Linux ignora Set-UID no script.
O ambiente continua a exigir análise. O carregador dinâmico filtra variáveis como LD_PRELOAD em execução segura; isso não significa que todas as variáveis externas sejam removidas. O programa privilegiado precisa de controlar os valores que usa para decidir operações.
UID guardado e redução de privilégios
O UID guardado, ou saved set-user-ID, permite certas mudanças posteriores do UID efetivo. Em Linux com glibc atual, seteuid(u) muda apenas o efetivo, preservando o real e o guardado. Já setuid(u), quando o processo tem a autoridade CAP_SETUID, muda os três. Sem essa autoridade, setuid não tem o mesmo efeito geral: as mudanças permitidas dependem dos UIDs atuais.
Segue duas alternativas independentes, ambas a partir de um binário Set-UID de root iniciado pelo utilizador 1000. As chamadas indicadas têm sucesso, e não há outra autoridade que permita mudar UIDs:
| Estado | Real | Efetivo | Guardado |
|---|---|---|---|
| Inicial | 1000 | 0 | 0 |
Após seteuid(1000) | 1000 | 1000 | 0 |
Após setuid(1000) a partir do inicial | 1000 | 1000 | 1000 |
Na primeira alternativa, seteuid(0) pode recuperar o efetivo 0 porque este continua guardado. Na segunda, essa chamada já não é permitida apenas pelos UIDs: o 0 deixou de ser real, efetivo ou guardado. Uma redução temporária não cumpre um pedido de abandono permanente de privilégios.
Verifica sempre o resultado das chamadas, mesmo a partir de root. Analisa separadamente grupos, Linux capabilities e recursos já abertos. Mudar os três UIDs não fecha descritores, como veremos a seguir.
PATH e interpretação pela shell
system("ls") pede a uma shell que interprete ls. A procura pode usar PATH: se uma pasta controlada pelo requerente vier primeiro, o nome pode selecionar outro executável. Fixar o formato do comando C não fixa o programa finalmente escolhido.
Agora o serviço usa um caminho absoluto, mas concatena um nome de ficheiro:
/* Esquema de uma chamada, não um programa completo. */
system("/bin/cat relatorio; printf MARCA");
A shell interpreta o ponto e vírgula como separador. Executa a consulta e depois outra operação. A segunda operação deste exemplo só imprime uma marca; demonstra por que tratar o argumento como parte de um comando aumenta a autoridade do requerente.
Com uma chamada direta a execve, o caminho do executável e os elementos de argv ficam separados. Um argumento relatorio; printf MARCA passa como um único nome, sem a shell o transformar em duas operações. Ainda é necessário autorizar o ficheiro, controlar opções aceites pelo executável e escolher um ambiente adequado. Retirar a shell não resolve qualquer uso perigoso de argumentos.
O efeito privilegiado de system depende também da shell. No ambiente descrito pelo guião, dash baixa privilégios quando deteta a diferença de UID; o laboratório compara com outra configuração. Não se conclui que toda chamada system num processo Set-UID executa a operação final como root.
Descritores depois de retirar privilégios
O serviço abre relatorio para escrita enquanto tem autoridade. O sistema concede um descritor, aqui 3. O serviço baixa depois os seus UIDs e executa outro programa. Se o descritor não foi fechado nem marcado close-on-exec, o novo programa pode herdá-lo.
A nova abertura verifica as permissões atuais do processo. Uma escrita pelo descritor já aberto usa a referência obtida antes; baixar o UID não repete automaticamente a decisão da abertura. No guião, este caso chama-se capability leaking. O recurso concreto é o descritor aberto, não necessariamente uma Linux capability.
Se o programa seguinte não deve receber essa autoridade, fecha o descritor antes da transferência e define close-on-exec para impedir herança por execve. Close-on-exec não fecha o descritor por baixar o UID nem impede o programa atual de o usar. Quando possível, retira privilégios antes de abrir o recurso, verifica erros e mantém apenas a autoridade necessária.
Evidência no LOGBOOK
O Moodle da semana 4 pede as Tasks 1–6, a comparação com system da Task 8 e a fuga de recurso da Task 9. Usa o ambiente isolado indicado pelo docente. Os exemplos do navegador de programação defensiva não reproduzem UID, processos ou permissões POSIX.
Para cada observação, regista o estado anterior, a operação, o resultado e a condição que o explica. Em PATH, identifica o executável escolhido e os UIDs relevantes. Em injeção de comandos, distingue o argumento recebido da operação interpretada pela shell. Na fuga de descritor, identifica quando foi aberto e por que permanece utilizável. Uma captura sem esta ligação não explica o mecanismo.
Segue o guião para a preparação e restauração do ambiente. A comparação que altera a shell do laboratório exige repor a configuração original no fim. Não transfiras essas alterações para o sistema de trabalho.
Exercícios
Executável fixo, argumento interpretado
Um serviço usa system("cat relatorio"). O requerente controla a primeira pasta de PATH. A equipa muda para system("/bin/cat " + nome), onde nome é texto externo. Explica que risco a alteração retira e qual permanece. Compara o argumento relatorio; printf MARCA nas duas chamadas seguintes: o comando concatenado para system e uma chamada direta a /bin/cat por execve com esse texto num único elemento de argv. Não assumes que a shell conserva privilégios.
Pista
Quem escolhe o executável e quem interpreta os caracteres do argumento?
Solução
O caminho absoluto retira a procura de cat por PATH; a shell continua a interpretar o comando. Em system, o ponto e vírgula separa a consulta e printf MARCA. Em execve, o texto passa como um único argumento: sem shell, o ponto e vírgula não cria outra operação. Ainda são necessários autorização do objeto, controlo das opções e ambiente adequado. O UID da operação final depende também da shell e das condições de execução; não basta saber que o serviço é Set-UID.
Confere a tua resposta:
- Distingui procura por PATH de interpretação pela shell.
- Expliquei o efeito do ponto e vírgula em system e em argv.
- Não tratei execve como autorização de qualquer ficheiro.
Duas formas de chegar ao mesmo ficheiro
Um serviço com UID efetivo 0 abre um ficheiro 0644 de root para escrita e recebe o descritor 3. Baixa todos os seus UIDs para 1000, com sucesso. Uma nova abertura para escrita é negada, mas o descritor continua válido. Assume permissões tradicionais, sem outras políticas ou restrições. Explica por que a escrita pelo descritor 3 ainda pode ocorrer. Se o serviço marcar o descritor close-on-exec, fica fechado ao baixar o UID? E após um execve bem-sucedido?
Pista
Identifica o momento em que foi concedida a referência de escrita.
Solução
A abertura verificou autoridade quando o UID efetivo era 0. Baixar os UIDs não fecha o descritor nem reaplica a decisão de abertura a cada escrita. A nova abertura usa a autoridade atual e é negada, enquanto o descritor 3 conserva a referência de escrita. Close-on-exec fecha-o num execve bem-sucedido, não na mudança de UID. O próprio serviço ainda o pode usar antes de execve; se já não precisa dele, deve fechá-lo.
Confere a tua resposta:
- Identifiquei a abertura privilegiada como origem do descritor.
- Distingui nova abertura de escrita pela referência existente.
- Expliquei quando close-on-exec fecha e quando não fecha.
Erros frequentes
O recurso deste caso é um descritor. Retirar Linux capabilities não o fecha automaticamente.
Cópia do ambiente e substituição do programa
O pai tem MODO=consulta e cria um filho com fork. O filho muda a sua variável para MODO=editar e faz execve com uma lista envp que contém apenas MODO=restrito, terminada por ponteiro nulo. As operações têm sucesso. Que valor fica no pai? Que valor recebe o novo programa? A chamada a execve criou mais um processo?
Solução
O pai mantém MODO=consulta: o filho alterou a sua cópia. O novo programa recebe MODO=restrito, escolhido em envp, e não o valor anterior do filho. fork criou o filho; execve substituiu o seu programa no mesmo processo. Distinguir estes momentos permite controlar o ambiente transmitido a uma operação sensível.
Confere a tua resposta:
- Mantive o ambiente do pai independente da alteração do filho.
- Usei envp como ambiente do novo programa.
- Distingui criar um filho de substituir o programa no mesmo processo.
Redução temporária ou permanente
Em Linux com glibc atual, duas alternativas partem de UIDs real 1000, efetivo 0 e guardado 0. A faz seteuid(1000); B, com CAP_SETUID, faz setuid(1000). Estas chamadas têm sucesso. Depois não há Linux capabilities nem outra autoridade que permita recuperar root. Indica os três UIDs resultantes e decide se seteuid(0) pode ter sucesso em cada alternativa. Um descritor aberto para escrita antes da redução fica automaticamente fechado?
Pista
A regra de seteuid permite ao processo sem privilégio selecionar o UID real, efetivo ou guardado.
Solução
A fica com real 1000, efetivo 1000 e guardado 0. Pode voltar a efetivo 0 por seteuid(0), usando o guardado. B fica com os três a 1000: nas hipóteses dadas, 0 já não é uma escolha autorizada e a recuperação falha. Nenhuma das mudanças fecha automaticamente o descritor. A capacidade de mudar UIDs e a referência a um ficheiro já aberto são autoridades distintas; para retirar a segunda, fecha o recurso ou controla a sua transferência.
Confere a tua resposta:
- Segui os três UIDs em cada alternativa independente.
- Justifiquei a recuperação apenas na alternativa com UID 0 guardado.
- Separei perda de autoridade de mudança de UID e fecho de descritores.