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.
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
O mesmo vale para rename:
$ mayi mv old new
May I move or rename '/home/igor/old' to '/home/igor/new'?
[Y/n]:
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
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
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),
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
}
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)
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
}
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,
},
},
}
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
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)