Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

README.md

Autostart, systemd --user, SSH local, portais e isolamento de GPU: 10 lugares onde superfície de ataque se esconde num desktop Linux moderno

Varredura ampla de uma máquina desktop Linux (systemd + Wayland), motivada pela pergunta "isso é coisa de invasão?" ao notar um mecanismo de autostart não documentado. Nenhum dos dez pontos abaixo é exclusivo de uma distro específica — a maioria se aplica a qualquer sistema systemd recente com sessão gráfica Wayland. Complementa a auditoria de firewall/controle de aplicação (portas/egress, não repetido aqui em detalhe) e o documento de redução de superfície systemd.

Achado 1: .desktop de autostart vira unit systemd --user automaticamente

Compositores Wayland minimalistas (Hyprland, Sway e afins) não leem /etc/xdg/autostart nativamente. Mas gerenciadores de sessão como o uwsm (Universal Wayland Session Manager) amarram xdg-desktop-autostart.target ao ciclo de vida da sessão gráfica. Na prática:

systemctl --user list-units 'app-*' --all

mostra uma unit app-*@autostart.service para cada .desktop gerado a partir de /etc/xdg/autostart/*.desktop e ~/.config/autostart/*.desktop, a cada login — sem precisar de GNOME/KDE completo.

Isso importa porque é um vetor de persistência real e nada óbvio: qualquer .desktop malicioso solto em ~/.config/autostart roda silenciosamente a cada login, sem precisar de cron, systemd unit própria nem entrada em .bashrc. Numa auditoria real, os arquivos existentes foram verificados como legítimos (dono de pacote confirmado via pacman -Qo ou gerenciador de pacote equivalente), mas o mecanismo em si merece registro por não ser óbvio até alguém procurar.

Decisão de mitigação — não mascarar unit por unit (não fecha a porta pra um .desktop novo), mascarar o gatilho:

systemctl --user mask xdg-desktop-autostart.target

Isso é diferente de mascarar systemd --user inteiro (nem faria sentido — em setups uwsm/Hyprland, systemd --user é a espinha dorsal da sessão inteira). O alvo é só a ponte .desktop → systemd unit.

Trade-off a considerar antes de aplicar: isso também impede o autostart automático de coisas como um keyring de credenciais (usado por ferramentas de linha de comando e navegadores para armazenar tokens/senhas). Se algo passar a reclamar de token/senha não encontrado depois do mask, a correção é subir esse serviço manualmente via exec-once (ou equivalente) na config do compositor, não desfazer o mask.

Achado 2: socket de SSH local ativo mesmo com sshd.service desabilitado

Em systemd ≥256, systemd-ssh-generator cria incondicionalmente sshd-unix-local.socket a cada boot, independente do estado de sshd.service. Ou seja: sshd.service pode estar disabled/inactive (nunca respondendo na porta 22 de rede) e ainda assim existir um socket AF_UNIX local ativo escutando.

Não é exposição de rede, mas é superfície que a maioria dos desktops pessoais não usa (a menos que dependam de acesso SSH local a containers/VMs via socket Unix/VSOCK). Mascarar a família inteira:

systemctl mask sshd.service sshd@.service sshdgenkeys.service ssh-access.target sshd-unix-local.socket
systemctl stop sshd-unix-local.socket   # mask sozinho não para o que já estava ativo

Lição que se repete neste documento inteiro: mask sem --now/stop não para uma unit já ativa — só impede reativação futura.

Achado 3: verificar ausência, não só presença

Parte de uma auditoria de superfície é confirmar que serviços de rede que você acha que não estão instalados realmente não estão (pacman -Qs samba vazio, unit not-found, etc.) — fechar esse loop explicitamente evita reabrir a mesma pergunta numa auditoria futura.

Achado 4: DNS/resolver — cadeia completa auditada, e um bug de configuração (não de segurança) que vale documentar

Cadeia completa auditada: status do resolver, /etc/resolv.conf, drop-ins de configuração, quem escuta na porta 53. Um padrão comum em sistemas com systemd-resolved: dois stubs oficiais do próprio systemd (127.0.0.53 full-stub, 127.0.0.54 modo proxy sem DNSSEC/cache) — confirmável via strings no binário e contra o man resolved.conf, não é nada injetado por terceiros.

Achado real, mas inofensivo: uma linha em /etc/hosts com uma variável de template (ex.: $hostname) nunca interpolada por algum script/instalador — o próprio systemd-resolved loga isso como hostname inválido. Vale registrar numa auditoria pra não ser redescoberto do zero achando que é sinal de algo malicioso, quando na verdade é só um instalador que deixou um placeholder sem substituir.

Achado 5: reconfirmação de portas — nada novo além do que já é conhecido

ss -tulnp deve mostrar só o que já está mapeado (stubs de resolver em loopback, cliente DHCP em porta efêmera, portas efêmeras de navegador para WebRTC). Nenhuma porta TCP nova exposta é o resultado esperado — o valor de reconfirmar é notar rapidamente se algo novo apareceu desde a última auditoria.

Achado 6: processo órfão rodando como root rastreado até a causa exata via journal (metodologia)

Encontrar processos dbus-daemon/keyring rodando como root, com PPid=1 (órfãos, sem parent rastreável em /proc), é um padrão clássico de "algo suspeito" à primeira vista. Antes de agir, cross-check no journal pelo horário exato de início:

journalctl --since "<hora-1min>" --until "<hora+1min>"

Isso costuma revelar a causa exata, com PID e comando de quem pediu — por exemplo, uma ferramenta de linha de comando rodada como root (numa sessão sudo -i separada) tentando ler um token salvo via D-Bus Secret Service API; como root nunca tinha sessão D-Bus própria, o dbus auto-lança um bus + keyring privados só pra atender aquele pedido pontual, e ninguém fecha depois. Benigno, mas revela um gap comum: auditd frequentemente está rodando mas não gravando onde ferramentas como ausearch esperam — vale checar /var/log/audit/audit.log separadamente.

Lição de metodologia: PPid=1 sozinho não prova nada (é reparenting normal de daemon órfão, não indício de esconderijo). O journal do serviço que fez a ativação (não do processo em si) costuma ter o comm= de quem pediu, mesmo depois do processo original ter saído.

Achado 7: stack de portais de desktop mascarado com decisão explícita sobre trade-off

xdg-document-portal.service (acesso a arquivo fora do sandbox pra Flatpak/Snap) é candidato seguro a mascarar se flatpak/snap não estiverem instalados (which flatpak vazio) — nada usa esse componente especificamente.

O resto do stack (xdg-desktop-portal.service, -gtk, -hyprland/equivalente do compositor, xdg-permission-store.service) costuma ter dependência reversa real e ativa: aplicativos Electron/GTK sandboxed dependem disso pra diálogo nativo de abrir/salvar arquivo, e navegadores dependem disso pra compartilhamento de tela em chamada de vídeo. Verificar com pacman -Qi xdg-desktop-portal | grep 'Required By' (ou equivalente) antes de mascarar — e só mascarar depois de decisão consciente de aceitar perder essas funções. Não é superfície de rede (tudo D-Bus local), então o ganho é redução de processo/API disponível pra qualquer coisa rodando com o mesmo UID, não fechamento de porta.

systemctl --user mask xdg-document-portal.service                                    # órfão real, se flatpak/snap ausente
systemctl --user mask xdg-desktop-portal.service xdg-desktop-portal-gtk.service \
    xdg-desktop-portal-<backend-do-compositor>.service xdg-permission-store.service   # trade-off consciente

Achado 8: /dev/nvidia* em 666 e Compute Mode Default — avaliado e decidido não mitigar (num caso de usuário único)

crw-rw-rw- em /dev/nvidia0 e afins, mais nvidia-smi --query-gpu=compute_mode retornando Default, é o padrão de fábrica do driver proprietário em desktop Linux — não algo alterado. Default compute mode não garante isolamento estrito de memória de GPU entre processos concorrentes (docs oficiais NVIDIA sobre Default vs Exclusive_Process) — classe de risco real, mas documentada pela própria NVIDIA como relevante principalmente pra GPU compartilhada multi-tenant (nuvem, laboratório), não desktop single-user.

Quando isso muda de "aceitável" pra "revisitar": awk -F: '$3>=1000 && $3<60000' /etc/passwd — se existir mais de uma conta de login local com shell, o argumento de "não tem outro usuário pra isolar de" deixa de valer, e apertar a permissão via regra udev própria (660 root:video) passa a fazer sentido.

Achado 9: LSM eBPF — confirmar diretamente no kernel, não confiar só no que o systemctl status diz

Auditoria direta do kernel, sem confiar em "o gerenciador de serviços diz que só existe um programa":

cat /sys/kernel/security/lsm                    # confirma 'bpf' entre os LSMs habilitados
bpftool prog list | grep '^\S*: lsm'             # filtra só programas tipo LSM, kernel inteiro
find /sys/fs/bpf -mindepth 1                     # objetos pinados independentes de processo

O que se busca: confirmar que só os hooks esperados existem, sem duplicata (sinal de crash-loop reintroduzindo o mesmo hook repetidamente) e sem programa de terceiros não reconhecido. find /sys/fs/bpf vazio é o resultado esperado — algo pinado e sobrevivendo fora do ciclo de vida normal de um processo é a técnica clássica de persistência de rootkit via eBPF (programa pinado continua ativo mesmo se o processo que carregou morrer, sem PID pra rastrear).

Corroboração extra, não só "existe, então confio": inspecionar os mapas por trás de um hook próprio (bpftool map show id <n>) e conferir se os dados internos batem com o que já se sabe sobre a ferramenta (ex.: capacidade do mapa batendo com um limite conhecido de uma auditoria anterior) é mais confiável do que só confirmar que "um programa com esse nome existe".

Achado 10: verificação final pós-mitigação e pós-atualização — mitigação não é garantidamente permanente

Depois de aplicar mitigações (masks, hardening), reauditar pontualmente pra confirmar que nada ficou preso nem reapareceu. E — ponto frequentemente esquecido — repetir essa verificação depois de atualizações do sistema: um mask de unit não é garantidamente permanente, porque uma atualização de pacote pode reinstalar/reabilitar a unit e desfazer o mask silenciosamente.

Duas checagens extras que valem sempre, não só depois de uma mitigação pontual — técnicas clássicas de esconder processo malicioso:

# processo rodando a partir de local típico de payload solto por script/download
for p in /proc/[0-9]*; do
  readlink "$p/exe" 2>/dev/null | grep -qE '^/(tmp|dev/shm|var/tmp)/' && echo "$p"
done

# binário deletado do disco mas ainda rodando em memória (esconde de "ls", não de /proc)
for p in /proc/[0-9]*; do
  readlink "$p/exe" 2>/dev/null | grep -q '(deleted)' && echo "$p"
done

Ambas vazias é o resultado esperado. O sinal de que um mask "pegou de verdade" e sobreviveu a atualizações é nenhuma das units mascaradas reaparecer na listagem de units ativas/geradas depois de um pacman -Syu/equivalente — se algo reaparecer, é sinal de que o pacote reinstalou o unit file e desfez o mask.

Checklist reutilizável

  • systemctl --user list-units 'app-*' --all — algum .desktop novo/desconhecido em /etc/xdg/autostart ou ~/.config/autostart virou unit?
  • systemctl --user is-enabled xdg-desktop-autostart.target sshd-unix-local.socket xdg-document-portal.service xdg-desktop-portal.service xdg-desktop-portal-gtk.service xdg-permission-store.service — todos masked conforme decidido?
  • systemctl is-enabled sshd.service sshd@.service sshdgenkeys.service ssh-access.target sshd-unix-local.socket — mascarados a nível de sistema também, se aplicável?
  • ss -tulnp — nenhuma porta TCP nova exposta além dos stubs de loopback do resolver
  • resolvectl status + cat /etc/hosts — DNS servers batem com o gateway esperado, sem entrada de /etc/hosts nova e não intencional
  • ps aux com qualquer processo root de nome inesperado — antes de suspeitar, cruzar journalctl --since/--until no segundo exato do início pra achar o comm= de quem ativou (não confiar só em PPid, que costuma virar 1 por reparenting normal)
  • ls -la /dev/nvidia* + awk -F: '$3>=1000 && $3<60000' /etc/passwd — se uma segunda conta de login aparecer, revisitar a permissão de dispositivo NVIDIA
  • bpftool prog list | grep '^\S*: lsm' — só os hooks esperados, sem duplicata (crash-loop) e sem programa de terceiros
  • find /sys/fs/bpf -mindepth 1 — vazio, ou só objetos que você reconhece; algo pinado e não rastreável a um processo vivo é bandeira vermelha
  • processo rodando a partir de /tmp, /dev/shm ou /var/tmp (readlink /proc/<pid>/exe) — local típico de payload solto por script/download
  • binário deletado do disco mas ainda rodando em memória (readlink /proc/<pid>/exe termina em (deleted)) — esconde de ls, não de /proc
  • Repetir todo o checklist depois de qualquer atualização grande do sistema — masks e hardening não são garantidamente permanentes

Fontes