DEV Community

Igor Melo
Igor Melo

Posted on

mayi: o CLI educado que pede antes de mexer nos seus arquivos

Uma coisa que sempre me incomodou em usar Linux é que os programas tem permissão muito ampla.

Qualquer programa rodando no seu usuário pode ler suas credenciais, apagar sua home inteira, gravar seu som, tirar foto da câmera, desligar o pc... Só pra citar uns exemplos.

Uma das coisas que gostei no Mac no pouco tempo que usei foi que quando um programa precisa acessar qualquer coisa mais sensível, o sistema solicita permissão do usuário.

Mac solicitando permissão de acesso a câmera

Eu procurei soluções similares para Linux, mas todas que achei geralmente se baseavam numa pré-configuração, e eu queria algo "em runtime" como é no Mac.

Talvez exista e eu só não saiba buscar, mas de qualquer forma o projeto pessoal valeu a pena pelo aprendizado.

Vou mostrar o mayi na prática, depois fazer um overview do funcionamento com trechos de código.

Código: https://github.com/igorcafe/mayi

Exemplos

Ao rodar um programa que tenta ler um arquivo, o mayi interrompe esse programa e te pede permissão para lê-lo:

$ mayi bash steal_ssh.sh
May I read your '/home/igor/.ssh/id_rsa'?
[Y/n]: n
cat: /home/igor/.ssh/id_rsa: Permission denied
Enter fullscreen mode Exit fullscreen mode

O mesmo vale para rename:

$ mayi mv old new
May I move or rename '/home/igor/old' to '/home/igor/new'?
[Y/n]: 
Enter fullscreen mode Exit fullscreen mode

E também para remoção:

$ mayi rm yourfile
May I delete your '/home/igor/yourfile'?
[Y/n]: n
rm: cannot remove 'yourfile': Permission denied
Enter fullscreen mode Exit fullscreen mode

Obs.: o prompt por enquanto é feito via terminal, mas pretendo suportar alguma forma de popup também.

E você também pode pré-configurar:

# config global
[*]
/etc/.* = read write
/run/.* = read write
/var/.* = read write
/usr/.* = read write

# config por programa
[emacs]
$PWD/.* = read write
$HOME = read
$HOME/.gitconfig = read
$HOME/\.emacs.* = read write
Enter fullscreen mode Exit fullscreen mode

Obs.: escrevendo isso eu pensei que seria uma boa implementar a questão de permissão de acesso ao microfone e câmera, que atualmente não tem.

Como funciona

O mayi foi inicialmente escrito em C, porque foi o que encontrei mais recurso sobre e foi mais fácil de começar.

A medida que fui adicionando mais coisas senti vontade de migrar pra Go para ficar mais fácil de desenvolver, e por enquanto foi uma boa decisão porque a stdlib do Go é muito completa para o que preciso.

Agora para entendermos como o mayi funciona precisamos falar sobre algumas coisas primeiro...

O que são system calls

O sistema operacional não te deixa acessar o hardware diretamente.

Para acessar o hardware você precisa solicitar ao sistema operacional através de system calls, e você pode ler mais sobre isso nesse meu outro post.

Através de system calls você consegue:

  • Criar processos e threads
  • Interagir com o sistema de arquivos
  • Alocar memória
  • Usar sockets para para comunicação via rede
  • Gerenciar usuários e permissões

Só para citar alguns exemplos... O Linux tem praticamente 500 syscalls!

O sistema operacional já gerencia as permissões por processo.
Se um processo tenta apagar um arquivo que não pertence ao usuário dele e ele não tem permissão, essa operação vai falhar.

O problema é que praticamente todos os programas e aplicações são rodadas por padrão no seu usuário, então elas podem fazer tudo que você pode fazer.

Daí que entra o seccomp.

Seccomp

Seccomp (Security Computing Mode) é uma funcionalidade de segurança do Linux que permite controlar as system calls que um processo pode fazer.

Você cria um mini programa usando BPF para avaliar as system calls como quiser.
Você pode aprovar, recusar, ou até encerrar o processo.

O interessante é que esse mini programa é um bytecode que roda em kernel space numa máquina virtual.
Por rodar em kernel space, isso faz com que ele seja muito rápido, por não ter que trocar de contexto pra user space pra cada system call.

O filtro que eu uso atualmente é assim:

filter := []unix.SockFilter{     
  // resumidamente, pega o número da syscall e guarda no registrador padrão                                   
  bpfStmt(unix.BPF_LD|unix.BPF_W|unix.BPF_ABS, 0),                   

  // vá para linha abaixo se o número da syscall == SYS_OPEN, senão pula ela                                               
  bpfJump(unix.BPF_JMP|unix.BPF_JEQ|unix.BPF_K, unix.SYS_OPEN, 0, 1),

  // retorna que deve interceptar essa syscall (porque é SYS_OPEN)
  bpfStmt(unix.BPF_RET|unix.BPF_K, unix.SECCOMP_RET_USER_NOTIF),     

  // só chega aqui se syscall != SYS_OPEN
  ...

  // se nenhum filtro se aplicar, aprova
  bpfStmt(unix.BPF_RET|unix.BPF_K, unix.SECCOMP_RET_ALLOW),
Enter fullscreen mode Exit fullscreen mode

O filtro é um pouco intimidador, porque ele é um assembly só que mais estranho, mas lendo com calma dá pra entender.

Decodificando as system calls

Não duvido que dá para fazer isso direto no BPF, mas nesse caso eu prefiro fazer isso pelo Go.

Os dados da system call vem no seguinte formato:

type SeccompNotif struct {
    ID    uint64       // identificador do request, não da syscall
    Pid   uint32       // id do processo que fez a syscall
    Flags uint32       // atualmente não é usado
    Data  SeccompData  // veja abaixo
}

type SeccompData struct {
    Nr   int32      // número da syscall, ex.: unix.SYS_OPEN
    Arch uint32     // pra identificar a ABI (não uso, assumo x64)
    IP   uint64     // endereço da instrução (não uso)
    Args [6]uint64  // Argumentos da syscall
}
Enter fullscreen mode Exit fullscreen mode

O Args[6] são os argumentos da syscall, que variam de acordo com a syscall sendo chamada e pela arquitetura em questão.
A syscall é identificada pelo número Nr, que também varia por arquitetura.

Cada syscall vai ser diferente. Alguns exemplos na arquitetura x86-64 são:

Nome Número Arg 0 Arg 1 Arg 2 Arg 3
open 2 path (string) flags (int32) mode (int32) -
openat 257 dirfd (int32) path (string) flags (int32) mode (int32)
creat 85 path (string) mode (int32) - -

O problema é que o arg do path é só um ponteiro para o início da string, e essa string não está na memória do nosso processo, e sim na do processo filho.

Para ler essa memória eu usei a system call process_vm_readv.
Curiosos podem ver a função:

func readProcessString(pid int, addr uintptr) (string, error)
Enter fullscreen mode Exit fullscreen mode

O esquema de permissões

Eu chamei de Intent o que a syscall pretende fazer:

type Intent struct {
    Program string
    Actions []Action
}

type Action struct {
    Path  string
    Read  bool
    Write bool
}
Enter fullscreen mode Exit fullscreen mode

Por exemplo, um mv abc 123 tem o seguinte Intent:

return Intent{                                                                       
        Program: "mv",
        Actions: []Action{                                                           
                {                                                                    
                        Path:  oldPath,                                              
                        Read:  false,                                                
                        Write: true,                                                 
                },                                                                   
                {                                                                    
                        Path:  newPath,                                              
                        Read:  false,                                                
                        Write: true,                                                 
                },                                                                   
        },                                                                           
}  
Enter fullscreen mode Exit fullscreen mode

As configurações são parseadas de um arquivo .ini e seguem o formato:

# regexp = perms...
/nix/.* = read write
$HOME/.ssh/.* = read:ask write:deny
Enter fullscreen mode Exit fullscreen mode

A função permForIntent retorna uma permissão PermDeny | PermAsk | PermAllow dado um Intent e as configurações.

Por exemplo, na configuração acima, se você passasse um Intent de deletar o ~/.ssh/id_rsa.pub, ele iria retornar um PermDeny.

Conclusão

Tem mais coisas que eu poderia falar, tipo a comunicação entre supervisor e processo filho via socket, a transferência de file descriptor entre eles, mas acho que pode ficar pra um outro post.

Top comments (0)