Mergulhe profundamente nos namespaces do Linux, parte 2

Na parte anterior, apenas mergulhamos os pés nas águas do espaço para nome e ao mesmo tempo vimos como era fácil iniciar o processo em um espaço para nome UTS isolado. Nesta postagem, abordaremos o namespace do usuário.


Entre outros recursos relacionados à segurança, os namespaces de usuário isolam os identificadores de usuários e grupos no sistema. Nesta postagem, focaremos apenas os recursos de identificação de usuário e grupo (UID e GID, respectivamente), uma vez que eles desempenham um papel fundamental na realização de verificações de permissões e outras atividades relacionadas à segurança em todo o sistema.


No Linux, esses IDs são simplesmente números inteiros que identificam usuários e grupos no sistema. E alguns deles são atribuídos a cada processo para definir a quais operações / recursos esse processo pode e não pode ter acesso. A capacidade de um processo causar danos depende das permissões associadas aos IDs atribuídos.


Namespaces de usuário


Ilustraremos os recursos dos namespaces de usuário usando apenas IDs de usuário. Exatamente as mesmas ações se aplicam aos IDs de grupo, que abordaremos mais adiante nesta postagem.

O espaço para nome do usuário possui sua própria cópia dos identificadores de usuário e grupo. O isolamento permite associar o processo a outro conjunto de IDs, dependendo do espaço de nome do usuário ao qual ele pertence atualmente. Por exemplo, o processo $pid pode ser executado a partir da root (UID 0) no espaço de nome do usuário P e repentinamente continua a ser executado no proxy (UID 13) após alternar para outro espaço de nome do usuário Q.


Os espaços do usuário podem ser aninhados! Isso significa que uma instância de um espaço para nome personalizado (pai) pode ter zero ou mais espaços para nome filho, e cada espaço para nome filho, por sua vez, pode ter seus próprios espaços filho e assim por diante ... (até atingir o limite de 32 níveis de aninhamento). Quando um novo espaço para nome C é criado, o Linux define o espaço para nome do usuário atual do processo P, criando C como pai de C, e isso não pode ser alterado posteriormente. Como resultado, todos os espaços de nome de usuário têm exatamente um pai, formando uma estrutura de árvore de espaços de nome. E, como no caso de árvores, uma exceção a essa regra está no topo, onde temos o espaço para nome raiz (ou inicial, padrão). Isso, se você ainda não está fazendo algum tipo de mágica de contêiner, é provavelmente o espaço para nome do usuário ao qual todos os seus processos pertencem, pois esse é o único espaço para nome do usuário desde que o sistema foi iniciado.


Nesta postagem, usaremos os prompts de comando P $ e C $ para indicar o shell que está sendo executado no momento no namespace de usuário pai P e filho C, respectivamente.

Mapeamentos de ID do Usuário


O espaço para nome do usuário, de fato, contém um conjunto de identificadores e algumas informações que conectam esses IDs com um conjunto de IDs de outro espaço para nome do usuário - esse dueto define uma idéia completa dos IDs dos processos disponíveis no sistema. Vamos ver como isso pode parecer:


 P$ whoami iffy P$ id uid=1000(iffy) gid=1000(iffy) 

Em outra janela do terminal, vamos iniciar o shell usando o unshare (o sinalizador -U cria um processo no novo espaço de nome do usuário):


 P$ whoami iffy P$ unshare -U bash #    ,     user namespace C$ whoami nobody C$ id uid=65534(nobody) gid=65534(nogroup) C$ ls -l my_file -rw-r--r-- 1 nobody nogroup 0 May 18 16:00 my_file 

Espere um minuto, quem? Agora que estamos em um shell aninhado em C , o usuário atual se torna ninguém? Podemos adivinhar que, como C é um novo espaço para nome de usuário, o processo pode ter um tipo diferente de ID. Portanto, provavelmente não esperávamos que ele permanecesse iffy , mas nobody é engraçado. Por outro lado, é ótimo porque conseguimos o isolamento que queríamos. Nosso processo agora tem uma substituição de ID diferente (embora quebrada) no sistema - atualmente vê todos como nobody e cada grupo como nogroup .


As informações que vinculam um UID de um espaço para nome de usuário a outro são chamadas de mapeamento de ID do usuário . É uma tabela de pesquisa para identificações correspondentes no namespace de usuário atual para identificações em outro namespace e cada namespace de usuário está associado a exatamente um mapeamento de UID (além de outro mapeamento de GID para o ID de grupo).


Esse mapeamento é o que está quebrado em nosso shell de unshare . Acontece que os novos espaços de nome de usuário começam com o mapeamento vazio e, como resultado, o Linux usa o usuário horrível nobody por padrão. Precisamos corrigir isso antes de podermos realizar qualquer trabalho útil em nosso novo espaço para nome. Por exemplo, atualmente, as chamadas do sistema (como setuid ) que tentam trabalhar com o UID falharão. Mas não tenha medo! Fiel à tradição de arquivos , o Linux apresenta esse mapeamento usando o sistema de arquivos /proc em /proc/$pid/uid_map (em /proc/$pid/gid_map para o GID), onde $pid é o ID do processo. Vamos chamar esses dois arquivos de arquivos de mapeamento.


Arquivos de mapa


Arquivos de mapa são arquivos especiais no sistema. O que são especiais? Bem, retornando conteúdos diferentes cada vez que você os lê, dependendo do que seu processo está lendo. Por exemplo, o arquivo de mapa /proc/$pid/uid_maps retorna o mapeamento de UIDs do espaço de nome do usuário que possui o processo $pid para UIDs no espaço de nome do usuário do processo de leitura. E, como resultado, o conteúdo retornado ao processo X pode diferir do que retornou ao processo Y , mesmo se eles lerem o mesmo arquivo de mapa ao mesmo tempo.


Em particular, o processo X , que lê o arquivo de mapa UID /proc/$pid/uid_map , recebe um conjunto de strings. Cada linha mapeia um intervalo contínuo de UIDs para o espaço de nome do usuário C do processo $pid , correspondendo a um intervalo de UIDs em outro espaço de nome.


Cada linha tem o formato $fromID $toID $length , em que:


  • $fromID é o UID inicial do intervalo para o namespace do usuário do processo $pid
  • $lenght é o comprimento do intervalo.
  • A tradução de $toID depende do processo de leitura X. Se X pertencer a outro espaço de nome de usuário U , $toID é o UID inicial do intervalo em U que mapeia a partir de $fromID . Caso contrário, $toID é o UID inicial do intervalo em P , o namespace do usuário pai do processo C.

Por exemplo, se um processo lê o arquivo /proc/1409/uid_map e, entre as linhas recebidas, você pode ver 15 22 5 , os UIDs de 15 a 19 no espaço de nome de usuário do processo 1409 mapeados para os UIDs 22-26 de um espaço de nome de usuário separado do processo de leitura.


Por outro lado, se um processo lê do arquivo /proc/$$/uid_map (ou um arquivo de mapa de qualquer processo pertencente ao mesmo espaço de nome de usuário que o processo de leitura) e recebe 15 22 5 , então UIDs de 15 a 19 em o namespace C do usuário é mapeado nos UIDs de 22 a 26 do pai para o namespace do usuário C.


Vamos tentar:


 P$ echo $$ 1442 #   user namespace... C$ echo $$ 1409 # C      ,     C$ cat /proc/1409/uid_map #  #   namespace P      # UIDs    UID    P$ cat /proc/1442/uid_map 0 0 4294967295 # UIDs  0  4294967294  P  #  4294967295 -  ID no user -  C. C$ cat /proc/1409/uid_map 0 4294967295 4294967295 

Bem, isso não foi muito emocionante, pois foram dois casos extremos, mas isso diz algumas coisas:


  1. O espaço para nome do usuário recém-criado realmente terá arquivos de mapas vazios.
  2. O UID 4294967295 não é mapeável e inadequado para uso mesmo no espaço para nome do usuário root . O Linux usa esse UID especificamente para indicar a ausência de um ID do usuário .

Gravando arquivos de mapa UID


Para corrigir nosso recém-criado espaço para nome de usuário C , precisamos fornecer nossos mapeamentos necessários, escrevendo seu conteúdo para mapear arquivos para qualquer processo que pertença a C (não podemos atualizar esse arquivo depois de gravá-lo). Escrever neste arquivo informa ao Linux duas coisas:


  1. Quais UIDs estão disponíveis para processos relacionados ao espaço de nomes de usuário C. de destino
  2. Quais UIDs no namespace de usuário atual correspondem aos UIDs em C.

Por exemplo, se escrevermos o seguinte no espaço de nome do usuário pai P no arquivo de mapa do espaço de nome filho C :


 0 1000 1 3 0 1 

essencialmente dizemos ao Linux que:


  1. Para processos em C , os únicos UIDs existentes no sistema são os UIDs 0 e 3 . Por exemplo, a chamada do sistema setuid(9) sempre termina com algo como um ID de usuário inválido .
  2. UIDs 1000 e 0 em P correspondem aos UIDs 0 e 3 em C. Por exemplo, se um processo em execução com o UID 1000 em P alternar para C , ele descobrirá que, após a troca, seu UID se tornará root 0 .

Proprietário de espaço para nome e privilégio


Em uma postagem anterior, mencionamos que, ao criar novos espaços para nome, o acesso com nível de superusuário é necessário. Os espaços para nome do usuário não impõem esse requisito. De fato, outro recurso é que eles podem possuir outros namespaces.


Sempre que um espaço para nome não-usuário N é criado , o Linux atribui o espaço para nome do usuário atual P do processo que cria N ao proprietário do espaço para nome N. Se P for criado junto com outros espaços para nome na mesma chamada de sistema clone , o Linux garantirá que P seja criado primeiro e se torne o proprietário de outros espaços para nome.


O proprietário dos espaços para nome é importante porque um processo que solicita uma ação privilegiada em um recurso que não seja um espaço para nome do usuário terá seus privilégios de UID verificados no proprietário desse espaço para nome do usuário e não no espaço para nome raiz do usuário. Por exemplo, digamos que P é o namespace do usuário pai do filho C , e P e C possuem seu próprio namespace de rede M e N, respectivamente. Um processo pode não ter privilégios para criar os dispositivos de rede incluídos no M , mas pode fazê-lo para N.


A conseqüência de ter um proprietário de namespace para nós é que podemos descartar o requisito sudo ao executar comandos usando o unshare ou o isolate se solicitarmos também a criação de um namespace de usuário. Por exemplo, unshare -u bash exigirá sudo , mas a opção unshare -Uu bash não será mais:


 # UID 1000 --      user namespace P. P$ id uid=1000(iffy) gid=1000(iffy) #           # network namespace. P$ ip link add type veth RTNETLINK answers: Operation not permitted #     ,     #  user  network namespace P$ unshare -nU bash # :  sudo C$ ip link add type veth RTNETLINK answers: Operation not permitted # ,  . ,  # UID 0 (root)    ,  #     nobody.   . C$ echo $$ 13294 #   P,   UID 1000  P  UID 0  C P$ echo "0 1000 1" > /proc/13294/uid_map #   ? C$ id uid=0(root) gid=65534(nogroup) C$ ip link add type veth # ! 

Infelizmente, reaplicaremos o requisito de superusuário na próxima postagem, pois o isolate precisa de privilégios de root no espaço para nome do usuário raiz para configurar corretamente o espaço para nome Mount e Network. Mas, certamente, removeremos os privilégios do processo da equipe para garantir que a equipe não tenha permissões desnecessárias.

Como os IDs são resolvidos


Acabamos de ver um processo em execução como um usuário comum 1000 repente mudado para o root . Não se preocupe, não houve aumento de privilégios. Lembre-se de que este é apenas um ID de mapeamento : enquanto nosso processo pensa que é o root no sistema, o Linux sabe que root - no seu caso - significa o UID 1000 habitual (graças ao nosso mapeamento). Portanto, em um momento em que os namespaces pertencentes a seu novo namespace de usuário (como namespace de rede em C ) reconhecem seus direitos como root , outros (como namespace de rede em P ) não. Portanto, o processo não pode fazer nada que o usuário 1000 não consiga.


Sempre que um processo em um espaço de nome de usuário aninhado executa uma operação que requer verificação de permissão - por exemplo, criando um arquivo - seu UID nesse espaço de nome de usuário é comparado com o ID do usuário equivalente no espaço de nome do usuário raiz, passando os mapeamentos na árvore do espaço de nome para a raiz. Há um movimento na direção oposta, por exemplo, quando ele lê IDs de usuário, como fazemos com ls -l my_file . O UID do proprietário my_file mapeado do espaço de nome do usuário raiz para o atual e o ID correspondente final (ou ninguém, se o mapeamento estiver ausente em algum lugar da árvore inteira) é fornecido ao processo de leitura.


ID do grupo


Mesmo se estivéssemos enraizados em C , ainda estamos associados ao terrível nogroup como nosso ID de grupo. Nós apenas precisamos fazer o mesmo para o correspondente /proc/$pid/gid_map . Antes de podermos fazer isso, precisamos desativar a chamada do sistema setgroups (isso não é necessário se nosso usuário já tiver um recurso CAP_SETGID em P , mas não assumiremos isso, pois isso geralmente vem com privilégios de superusuário) escrevendo "deny "para o arquivo proc/$pid/setgroups :


 #  13294 -- pid  unshared  C$ id uid=0(root) gid=65534(nogroup) P$ echo deny > /proc/13294/setgroups P$ echo "0 1000 1" > /proc/13294/gid_map #  group ID   C$ id uid=0(root) gid=0(root) 

Implementação


O código fonte desta publicação pode ser encontrado aqui .

Como você pode ver, há muitas dificuldades associadas ao gerenciamento de namespaces de usuário, mas a implementação é bastante simples. Tudo o que precisamos fazer é escrever um monte de linhas em um arquivo - era triste descobrir o que e onde escrever. Sem mais delongas, eis nossos objetivos:


  1. Clone um processo de equipe em seu próprio espaço para nome de usuário.
  2. Escreva nos arquivos de mapa UID e GID do processo de equipe.
  3. Redefina todos os privilégios de superusuário antes de executar o comando.

1 alcançado simplesmente adicionando o sinalizador CLONE_NEWUSER à nossa chamada do sistema de clone .


 int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER; 

Para 2 adicionamos a função prepare_user_ns , que representa cuidadosamente um usuário comum 1000 como root .


 static void prepare_userns(int pid) { char path[100]; char line[100]; int uid = 1000; sprintf(path, "/proc/%d/uid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); sprintf(path, "/proc/%d/setgroups", pid); sprintf(line, "deny"); write_file(path, line); sprintf(path, "/proc/%d/gid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); } 

E vamos chamá-lo do processo principal no espaço de nomes do usuário pai antes de sinalizar o processo de comando.


  ... //      . int pipe = params.fd[1]; //      namespace ... prepare_userns(cmd_pid); //   ,     . ... 

Para a etapa 3 atualizamos a função cmd_exec para garantir que o comando seja executado a partir do usuário não privilegiado usual 1000 que fornecemos no mapeamento (lembre-se de que o usuário raiz 0 no espaço para nome do usuário do processo da equipe é o usuário 1000 ):


  ... //   ' '   . await_setup(params->fd[0]); if (setgid(0) == -1) die("Failed to setgid: %m\n"); if (setuid(0) == -1) die("Failed to setuid: %m\n"); ... 

E isso é tudo! isolate agora inicia o processo em um namespace de usuário isolado.


 $ ./isolate sh ===========sh============ $ id uid=0(root) gid=0(root) 

Havia alguns detalhes nesta postagem sobre como os espaços de nome de usuário funcionam, mas no final, a configuração da instância foi relativamente simples. Na próxima postagem, veremos a possibilidade de executar um comando em nosso próprio espaço para nome Mount usando isolate (revelando o segredo por trás da instrução FROM do Dockerfile ). Lá precisaremos ajudar um pouco mais o Linux para configurar corretamente a instância.

Source: https://habr.com/ru/post/pt459574/


All Articles