PEP 578 – Python Runtime Audit Hooks
- Autor:
- Steve Dower <steve.dower at python.org>
- BDFL-Delegate:
- Christian Heimes <christian at python.org>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 16. Juni 2018
- Python-Version:
- 3.8
- Post-History:
- 28. März 2019, 07. Mai 2019
Zusammenfassung
Diese PEP beschreibt Erweiterungen der Python-API und spezifische Verhaltensweisen für die CPython-Implementierung, die Aktionen, die von der Python-Laufzeitumgebung ausgeführt werden, für Audit-Tools sichtbar machen. Die Sichtbarkeit dieser Aktionen bietet Testframeworks, Logging-Frameworks und Sicherheitstools die Möglichkeit, von der Laufzeitumgebung ausgeführte Aktionen zu überwachen und optional zu begrenzen.
Diese PEP schlägt zwei API-Erweiterungen vor, um Einblicke in eine laufende Python-Anwendung zu ermöglichen: eine für beliebige Ereignisse und eine weitere speziell für das Modulimportsystem. Die APIs sollen in allen Python-Implementierungen verfügbar sein, obwohl die spezifischen Meldungen und Werte hier nicht spezifiziert sind, um den Implementierungen die Freiheit zu geben, selbst zu entscheiden, wie sie Informationen am besten an ihre Benutzer weitergeben. Einige Beispiele, die wahrscheinlich in CPython verwendet werden, werden zu Erklärungszwecken angegeben.
Siehe PEP 551 für Diskussionen und Empfehlungen zur Verbesserung der Sicherheit einer Python-Laufzeitumgebung, die diese Audit-APIs nutzt.
Hintergrund
Python bietet Zugriff auf eine breite Palette von Low-Level-Funktionalitäten auf vielen gängigen Betriebssystemen. Während dies für „einmal schreiben, überall ausführen“-Skripte äußerst nützlich ist, erschwert es auch die Überwachung von in Python geschriebener Software. Da Python native System-APIs direkt verwendet, leiden bestehende Überwachungstools entweder unter eingeschränktem Kontext oder umgehen die Auditerfassung.
Eingeschränkter Kontext tritt auf, wenn die Systemüberwachung melden kann, dass eine Aktion stattgefunden hat, aber die Ereigniskette, die dazu geführt hat, nicht erklären kann. Beispielsweise kann die Netzwerküberwachung auf Betriebssystemebene melden, dass „das Lauschen auf Port 5678 gestartet wurde“, aber möglicherweise nicht die Prozess-ID, die Kommandozeile, den Elternprozess oder den lokalen Zustand des Programms zum Zeitpunkt der Auslösung der Aktion angeben. Firewall-Kontrollen zur Verhinderung solcher Aktionen sind ähnlich eingeschränkt, typischerweise auf Prozessnamen oder einen globalen Zustand wie den aktuellen Benutzer beschränkt, und stellen in jedem Fall selten eine nützliche Protokolldatei bereit, die mit anderen Anwendungsnachrichten korreliert ist.
Umgehung der Auditerfassung kann auftreten, wenn das typische Systemwerkzeug für eine Aktion seine Verwendung normalerweise melden würde, aber der Zugriff auf die APIs über Python dies nicht auslöst. Beispielsweise kann die Verwendung von „curl“ für HTTP-Anfragen in einem auditierten System speziell überwacht werden, aber die Python-Funktion „urlretrieve“ nicht.
Innerhalb einer lang laufenden Python-Anwendung, insbesondere einer, die vom Benutzer bereitgestellte Informationen verarbeitet, wie z. B. eine Webanwendung, besteht das Risiko unerwarteten Verhaltens. Dies kann auf Fehler im Code oder auf vorsätzliche Einwirkungen eines bösartigen Benutzers zurückzuführen sein. In beiden Fällen kann die normale Anwendungsregistrierung umgangen werden, was keine Anzeichen dafür liefert, dass etwas Ungewöhnliches geschehen ist.
Darüber hinaus, und in Python etwas einzigartig, ist es sehr einfach, den in einer Anwendung ausgeführten Code zu beeinflussen, indem man entweder den Suchpfad des Importsystems manipuliert oder Dateien früher auf dem Pfad platziert als beabsichtigt. Dies geschieht oft, wenn Entwickler ein Skript mit demselben Namen wie das Modul erstellen, das sie verwenden möchten – zum Beispiel eine random.py-Datei, die versucht, das random-Modul der Standardbibliothek zu importieren.
Dies ist keine Sandboxing, da dieser Vorschlag nicht versucht, bösartiges Verhalten zu verhindern (obwohl er einige neue Optionen dafür ermöglicht). Weitere Diskussionen finden Sie im Abschnitt Warum keine Sandbox unten.
Überblick über Änderungen
Ziel dieser Änderungen ist es, sowohl Anwendungsentwicklern als auch Systemadministratoren zu ermöglichen, Python in ihre bestehenden Überwachungssysteme zu integrieren, ohne vorzuschreiben, wie diese Systeme aussehen oder sich verhalten.
Wir schlagen zwei API-Änderungen vor, um dies zu ermöglichen: einen Audit Hook und einen Verified Open Hook. Beide sind von Python und nativem Code aus verfügbar, was es Anwendungen und Frameworks, die in reinem Python-Code geschrieben sind, ermöglicht, von den zusätzlichen Meldungen zu profitieren, während gleichzeitig Einbettungsanbietern oder Systemadministratoren ermöglicht wird, Builds von Python bereitzustellen, bei denen die Auditerfassung immer aktiviert ist.
Nur CPython ist verpflichtet, die hier beschriebenen nativen APIs bereitzustellen. Andere Implementierungen sollten die reinen Python-APIs bereitstellen und können native Versionen bereitstellen, die für ihre zugrunde liegenden Laufzeiten geeignet sind. Audit-Ereignisse gelten ebenfalls als implementierungsspezifisch, unterliegen jedoch normalen Garantien für die Funktionskompatibilität.
Audit Hook
Um Aktionen der Laufzeitumgebung (im Auftrag des Aufrufers) zu beobachten, ist eine API erforderlich, um Meldungen aus bestimmten Operationen auszulösen. Diese Operationen liegen typischerweise tief in der Python-Laufzeitumgebung oder der Standardbibliothek, wie z. B. dynamische Codekompilierung, Modulimporte, DNS-Auflösung oder die Verwendung bestimmter Module wie ctypes.
Die folgenden neuen C-APIs ermöglichen es Einbettungsanbietern und CPython-Implementierern, Audit Hook-Meldungen zu senden und zu empfangen.
# Add an auditing hook
typedef int (*hook_func)(const char *event, PyObject *args,
void *userData);
int PySys_AddAuditHook(hook_func hook, void *userData);
# Raise an event with all auditing hooks
int PySys_Audit(const char *event, PyObject *args);
Die neuen Python-APIs zum Empfangen und Auslösen von Audit Hooks sind:
# Add an auditing hook
sys.addaudithook(hook: Callable[[str, tuple]])
# Raise an event with all auditing hooks
sys.audit(str, *args)
Hooks werden durch Aufruf von PySys_AddAuditHook() aus C zu jeder Zeit, auch vor Py_Initialize(), oder durch Aufruf von sys.addaudithook() aus Python-Code hinzugefügt. Hooks können nicht entfernt oder ersetzt werden. Für CPython sind von C hinzugefügte Hooks global, während von Python hinzugefügte Hooks nur für den aktuellen Interpreter gelten. Globale Hooks werden vor Interpreter-Hooks ausgeführt.
Wenn interessierende Ereignisse auftreten, kann Code entweder PySys_Audit() aus C (während der GIL gehalten wird) oder sys.audit() aufrufen. Das String-Argument ist der Name des Ereignisses und das Tupel enthält Argumente. Ein bestimmter Ereignisname sollte ein festes Schema für Argumente haben, das als öffentliche API (für jede x.y-Versionsfreigabe) betrachtet werden sollte und somit nur zwischen Feature-Releases mit aktualisierter Dokumentation geändert werden sollte. Um den Overhead zu minimieren und die Handhabung in nativen Hook-Implementierungen zu vereinfachen, werden benannte Argumente nicht unterstützt.
Für maximale Kompatibilität sollten Ereignisse, die denselben Namen wie ein Ereignis im Referenzinterpreter CPython verwenden, alles tun, um kompatible Argumente zu verwenden. Das Einbeziehen des Namens oder einer Abkürzung der Implementierung in implementierungsspezifischen Ereignisnamen hilft auch, Kollisionen zu vermeiden. Beispielsweise ist ein pypy.jit_invoked-Ereignis klar von einem ipy.jit_invoked-Ereignis zu unterscheiden. Von Python-Modulen ausgelöste Ereignisse sollten ihren Modul- oder Paketnamen in den Ereignisnamen aufnehmen.
Während Ereignisnamen beliebige UTF-8-Strings sein können, wird zur Konsistenz über Implementierungen hinweg empfohlen, gültige Python-Punktnamen zu verwenden und keine spezifischen Kodierungsdetails im Namen zu speichern. Zum Beispiel ist ein import-Ereignis mit dem Modulnamen spam als Argument vorzuziehen, anstatt eines spam module imported-Ereignisses ohne Argumente. Vermeiden Sie die Verwendung eingebetteter Nullzeichen, da Sie sonst diejenigen verärgern, die Hooks mit C implementieren.
Wenn ein Ereignis auditiert wird, wird jeder Hook in der Reihenfolge aufgerufen, in der er hinzugefügt wurde (soweit möglich), wobei der Ereignisname und die Argumente übergeben werden. Wenn ein Hook mit einer gesetzten Ausnahme zurückkehrt, werden spätere Hooks ignoriert und *im Allgemeinen* sollte die Python-Laufzeitumgebung beendet werden – Ausnahmen von Hooks sind nicht dazu bestimmt, behandelt oder als erwartete Vorkommnisse betrachtet zu werden. Dies ermöglicht es Hook-Implementierungen zu entscheiden, wie auf ein bestimmtes Ereignis reagiert werden soll. Typische Reaktionen werden sein, das Ereignis zu protokollieren, die Operation mit einer Ausnahme abzubrechen oder den Prozess sofort mit einem Betriebssystem-Exit-Aufruf zu beenden.
Wenn ein Ereignis auditiert wird, aber keine Hooks gesetzt wurden, sollte die Funktion audit() einen minimalen Overhead verursachen. Idealerweise ist jedes Argument ein Verweis auf vorhandene Daten und kein Wert, der nur für den Audit-Aufruf berechnet wurde.
Da Hooks Python-Objekte sein können, müssen sie während der Interpreter- oder Laufzeitfinalisierung freigegeben werden. Diese sollten zu keinem anderen Zeitpunkt ausgelöst werden und einen Ereignis-Hook auslösen, um sicherzustellen, dass unerwartete Aufrufe beobachtet werden.
Unten in Vorgeschlagene Audit Hook-Speicherorte empfehlen wir einige wichtige Operationen, die Audit-Ereignisse auslösen sollten. Im Allgemeinen sollten Ereignisse auf der niedrigstmöglichen Ebene ausgelöst werden. Wenn die Wahl zwischen dem Auslösen eines Ereignisses aus Python-Code oder nativem Code besteht, sollte das Auslösen aus nativem Code bevorzugt werden.
Python-Implementierungen sollten dokumentieren, welche Operationen Audit-Ereignisse auslösen, zusammen mit dem Ereignisschema. Es ist beabsichtigt, dass sys.addaudithook(print) eine triviale Möglichkeit ist, alle Meldungen anzuzeigen.
Verified Open Hook
Die meisten Betriebssysteme verfügen über einen Mechanismus, um zwischen ausführbaren und nicht ausführbaren Dateien zu unterscheiden. Dies kann beispielsweise ein Ausführungsbit im Berechtigungsfeld sein, ein verifizierter Hash des Dateiinhalts zur Erkennung potenzieller Code-Manipulationen oder Beschränkungen des Dateisystempfads. Dies sind wichtige Sicherheitsmechanismen, um sicherzustellen, dass nur Code ausgeführt wird, der für eine bestimmte Umgebung genehmigt wurde.
Die meisten Kernel bieten Möglichkeiten, von ihnen geladene und ausgeführte Binärdateien einzuschränken oder zu auditieren. Von Python erstellte Dateitypen erscheinen als reguläre Daten und diese Funktionen sind nicht anwendbar. Dieser Open-Hook ermöglicht es Python-Einbettungsanbietern, bei der Ausführung von Skripten oder beim Importieren von Python-Code mit der Betriebssystemunterstützung zu interagieren.
Die neue öffentliche C-API für den Verified Open Hook ist:
# Set the handler
typedef PyObject *(*hook_func)(PyObject *path, void *userData)
int PyFile_SetOpenCodeHook(hook_func handler, void *userData)
# Open a file using the handler
PyObject *PyFile_OpenCode(const char *path)
Die neue öffentliche Python-API für den Verified Open Hook ist:
# Open a file using the handler
io.open_code(path : str) -> io.IOBase
Die Funktion io.open_code() ist ein Drop-in-Ersatz für open(abspath(str(pathlike)), 'rb'). Ihr Standardverhalten ist das Öffnen einer Datei für rohen Binärzugriff. Um das Verhalten zu ändern, sollte ein neuer Handler gesetzt werden. Handlerfunktionen akzeptieren nur str-Argumente. Die C-API-Funktion PyFile_OpenCode geht von UTF-8-Kodierung aus. Pfade müssen absolut sein, und es liegt in der Verantwortung des Aufrufers, sicherzustellen, dass der vollständige Pfad korrekt aufgelöst wird.
Ein benutzerdefinierter Handler kann jederzeit durch Aufruf von PyFile_SetOpenCodeHook() aus C gesetzt werden, auch vor Py_Initialize(). Wenn jedoch bereits ein Hook gesetzt wurde, schlägt der Aufruf fehl. Wenn open_code() mit einem gesetzten Hook aufgerufen wird, erhält der Hook den Pfad und sein Rückgabewert wird direkt zurückgegeben. Das zurückgegebene Objekt sollte ein geöffnetes dateiähnliches Objekt sein, das das Lesen von rohen Bytes unterstützt. Dies ist explizit dazu gedacht, eine BytesIO-Instanz zu ermöglichen, wenn der Öffnungs-Handler die gesamte Datei bereits in den Speicher gelesen hat.
Beachten Sie, dass diese Hooks die Funktion _io.open() auf CPython importieren und aufrufen können, ohne sich selbst auszulösen. Sie können auch _io.BytesIO verwenden, um ein kompatibles Ergebnis mit einem In-Memory-Puffer zurückzugeben.
Wenn der Hook feststellt, dass die Datei nicht geladen werden sollte, sollte er eine Ausnahme seiner Wahl auslösen und darüber hinausgehende Protokollierung vornehmen.
Alle Import- und Ausführungsfunktionen, die Code aus einer Datei betreffen, werden so geändert, dass sie open_code() bedingungslos verwenden. Es ist wichtig zu beachten, dass Aufrufe von compile(), exec() und eval() nicht über diese Funktion laufen – ein Audit Hook, der den Code aus diesen Aufrufen einschließt, ist die beste Gelegenheit, aus Dateien gelesenen Code zu validieren. Angesichts der aktuellen Entkopplung zwischen Import und Ausführung in Python wird die meiste importierte Code sowohl durch open_code() als auch durch den Log Hook für compile laufen, daher ist Vorsicht geboten, um Verifizierungsschritte nicht zu wiederholen.
Dateizugriffe, die nicht explizit Code ausführen sollen, werden voraussichtlich diese Funktion nicht verwenden. Dazu gehören das Laden von Pickles, XML- oder YAML-Dateien, bei denen Codeausführung im Allgemeinen als bösartig und nicht als beabsichtigt gilt. Diese Operationen sollten ihre eigenen Audit-Ereignisse bereitstellen, vorzugsweise Unterscheidung zwischen normaler Funktionalität (z. B. Unpickler.load) und Codeausführung (Unpickler.find_class).
Einige Beispiele: Wenn der Dateityp normalerweise ein Ausführungsbit erfordert (unter POSIX) oder eine Warnung ausgibt, wenn er als aus dem Internet heruntergeladen gekennzeichnet wird (unter Windows), sollte er wahrscheinlich open_code() anstelle von reinem open() verwenden. Das Öffnen von ZIP-Dateien mit der ZipFile-Klasse sollte open() verwenden, während das Öffnen über zipimport open_code() verwenden sollte, um die korrekte Absicht zu signalisieren. Code, der die falsche Funktion für einen bestimmten Kontext verwendet, kann den Hook umgehen, was in CPython und der Standardbibliothek als Fehler betrachtet werden sollte. Die Verwendung einer Kombination aus open_code Hooks und Audit Hooks ist erforderlich, um alle ausgeführten Quellen im Falle von beliebigem Code nachzuverfolgen.
Es gibt keine Python-API zur Änderung des Open Hooks. Um das Importverhalten aus Python-Code zu ändern, verwenden Sie die vorhandene Funktionalität, die von importlib bereitgestellt wird.
API-Verfügbarkeit
Während alle hier hinzugefügten Funktionen als öffentliche und stabile API gelten, ist das Verhalten der Funktionen implementierungsspezifisch. Die meisten Beschreibungen hier beziehen sich auf die CPython-Implementierung, und obwohl andere Implementierungen die Funktionen bereitstellen sollten, gibt es keine Anforderung, dass sie sich gleich verhalten.
Beispielsweise sollten sys.addaudithook() und sys.audit() vorhanden sein, aber möglicherweise nichts tun. Dies ermöglicht es Code, Aufrufe an sys.audit() zu tätigen, ohne auf dessen Existenz testen zu müssen, aber es sollte nicht davon ausgegangen werden, dass sein Aufruf Auswirkungen hat. (Das Einschließen von Existenztests in sicherheitskritischem Code ermöglicht einen weiteren Vektor zur Umgehung der Auditerfassung, daher ist es vorzuziehen, dass die Funktion immer vorhanden ist.)
io.open_code(path) sollte zumindest immer _io.open(path, 'rb') zurückgeben. Code, der die Funktion verwendet, sollte keine weiteren Annahmen darüber treffen, was passieren kann, und Implementierungen, die von CPython abweichen, müssen den Entwicklern nicht gestatten, das Verhalten dieser Funktion mit einem Hook zu überschreiben.
Vorgeschlagene Audit Hook-Speicherorte
Die Speicherorte und Parameter bei Aufrufen von sys.audit() oder PySys_Audit() werden von einzelnen Python-Implementierungen bestimmt. Dies soll maximale Freiheit für Implementierungen ermöglichen, die für ihre Plattform relevantesten Operationen offenzulegen und potenziell teure oder laute Ereignisse zu vermeiden oder zu ignorieren.
Tabelle 1 dient sowohl als Vorschlag für Operationen, die auf allen Implementierungen Audit-Ereignisse auslösen sollten, als auch als Beispiele für Ereignisschemata.
Tabelle 2 liefert weitere Beispiele, die nicht zwingend erforderlich sind, aber wahrscheinlich in CPython verfügbar sein werden.
Beziehen Sie sich auf die Dokumentation Ihrer Python-Version, um zu sehen, welche Operationen Audit-Ereignisse bereitstellen.
| API-Funktion | Ereignisname | Argumente | Begründung |
|---|---|---|---|
PySys_AddAuditHook |
sys.addaudithook |
Erkennt, wann neue Audit Hooks hinzugefügt werden. | |
PyFile_SetOpenCodeHook |
cpython.PyFile_SetOpenCodeHook |
Erkennt jeden Versuch, den open_code Hook zu setzen. |
|
compile, exec, eval, PyAst_CompileString, PyAST_obj2mod |
compile |
(code, filename_or_none) |
Erkennt dynamische Codekompilierung, bei der code eine Zeichenkette oder ein AST sein kann. Beachten Sie, dass dies für reguläre Importe von Quellcode aufgerufen wird, einschließlich derjenigen, die mit open_code geöffnet wurden. |
exec, eval, run_mod |
exec |
(code_object,) |
Erkennt dynamische Ausführung von Codeobjekten. Dies tritt nur bei expliziten Aufrufen auf und wird nicht bei normaler Funktionsaufrufung ausgelöst. |
import |
import |
(module, filename, sys.path, sys.meta_path, sys.path_hooks) |
Erkennt, wann Module importiert werden. Dies wird ausgelöst, bevor der Modulname in eine Datei aufgelöst wird. Alle Argumente außer dem Modulnamen können None sein, wenn sie nicht verwendet werden oder nicht verfügbar sind. |
open |
io.open |
(path, mode, flags) |
Erkennt, wann eine Datei geöffnet werden soll. *path* und *mode* sind die üblichen Parameter für open, falls verfügbar, während *flags* in einigen Fällen anstelle von *mode* bereitgestellt werden. |
PyEval_SetProfile |
sys.setprofile |
Erkennt, wann Code Trace-Funktionen injiziert. Aufgrund der Implementierung werden Ausnahmen, die vom Hook ausgelöst werden, die Operation abbrechen, aber nicht im Python-Code ausgelöst. Beachten Sie, dass threading.setprofile letztendlich diese Funktion aufruft, sodass das Ereignis für jeden Thread auditiert wird. |
|
PyEval_SetTrace |
sys.settrace |
Erkennt, wann Code Trace-Funktionen injiziert. Aufgrund der Implementierung werden Ausnahmen, die vom Hook ausgelöst werden, die Operation abbrechen, aber nicht im Python-Code ausgelöst. Beachten Sie, dass threading.settrace letztendlich diese Funktion aufruft, sodass das Ereignis für jeden Thread auditiert wird. |
|
_PyObject_GenericSetAttr, check_set_special_type_attr, object_set_class, func_set_code, func_set_[kw]defaults |
object.__setattr__ |
(object, attr, value) |
Erkennt Monkey-Patching von Typen und Objekten. Dieses Ereignis wird für das Attribut __class__ und jedes Attribut auf type-Objekten ausgelöst. |
_PyObject_GenericSetAttr |
object.__delattr__ |
(object, attr) |
Erkennt das Löschen von Objektattributen. Dieses Ereignis wird für jedes Attribut auf type-Objekten ausgelöst. |
Unpickler.find_class |
pickle.find_class |
(module_name, global_name) |
Erkennt Importe und globale Namensauflösung beim Unpickling. |
| API-Funktion | Ereignisname | Argumente | Begründung |
|---|---|---|---|
_PySys_ClearAuditHooks |
sys._clearaudithooks |
Benachrichtigt Hooks, dass sie bereinigt werden, hauptsächlich falls das Ereignis unerwartet ausgelöst wird. Dieses Ereignis kann nicht abgebrochen werden. | |
code_new |
code.__new__ |
(bytecode, filename, name) |
Erkennt die dynamische Erstellung von Codeobjekten. Dies tritt nur bei direkter Instanziierung auf und wird nicht bei normaler Kompilierung ausgelöst. |
func_new_impl |
function.__new__ |
(code,) |
Erkennt die dynamische Erstellung von Funktions-Objekten. Dies tritt nur bei direkter Instanziierung auf und wird nicht bei normaler Kompilierung ausgelöst. |
_ctypes.dlopen, _ctypes.LoadLibrary |
ctypes.dlopen |
(module_or_path,) |
Erkennt, wann native Module verwendet werden. |
_ctypes._FuncPtr |
ctypes.dlsym |
(lib_object, name) |
Sammelt Informationen über bestimmte Symbole, die aus nativen Modulen abgerufen werden. |
_ctypes._CData |
ctypes.cdata |
(ptr_as_int,) |
Erkennt, wenn Code mit ctypes auf beliebigen Speicher zugreift. |
new_mmap_object |
mmap.__new__ |
(fileno, map_size, access, offset) |
Erkennt die Erstellung von mmap-Objekten. Unter POSIX kann access aus den Argumenten prot und flags berechnet worden sein. |
sys._getframe |
sys._getframe |
(frame_object,) |
Erkennt, wenn Code Frames direkt abruft. |
sys._current_frames |
sys._current_frames |
Erkennt, wenn Code Frames direkt abruft. | |
socket.bind, socket.connect, socket.connect_ex, socket.getaddrinfo, socket.getnameinfo, socket.sendmsg, socket.sendto |
socket.address |
(socket, address,) |
Erkennt den Zugriff auf Netzwerkressourcen. Die Adresse ist unverändert gegenüber dem ursprünglichen Aufruf. |
member_get, func_get_code, func_get_[kw]defaults |
object.__getattr__ |
(object, attr) |
Erkennt den Zugriff auf eingeschränkte Attribute. Dieses Ereignis wird für alle integrierten Mitglieder ausgelöst, die als eingeschränkt gekennzeichnet sind, und für Mitglieder, die möglicherweise Importe umgehen. |
urllib.urlopen |
urllib.Request |
(url, data, headers, method) |
Erkennt URL-Anfragen. |
Leistungsauswirkungen
Die wesentliche Auswirkung auf die Leistung betrifft den Fall, dass Ereignisse ausgelöst werden, aber keine Hooks angehängt sind. Dies ist der unvermeidliche Fall – sobald ein Entwickler Audit Hooks hinzugefügt hat, hat er sich ausdrücklich für den Kompromiss zwischen Leistung und Funktionalität entschieden. Leistungseinbußen mit hinzugefügten Hooks sind hier nicht von Interesse, da dies eine Opt-in-Funktionalität ist.
Die Analyse mit der Python Performance Benchmark Suite [1] zeigt keine signifikante Auswirkung, wobei die überwiegende Mehrheit der Benchmarks zwischen 1,05x schneller und 1,05x langsamer ist.
Unserer Meinung nach ist die Leistungsauswirkung des in dieser PEP beschriebenen Satzes von Auditing-Punkten vernachlässigbar.
Abgelehnte Ideen
Separates Modul für Audit Hooks
Der Vorschlag ist, ein neues Modul für Audit Hooks hinzuzufügen, hypothetisch audit. Dies würde die API und Implementierung vom sys-Modul trennen und es ermöglichen, die C-Funktionen PyAudit_AddHook und PyAudit_Audit zu benennen, anstatt der aktuellen Varianten.
Ein solches Modul müsste ein integriertes Modul sein, das garantiert immer vorhanden ist. Die Natur dieser Hooks ist, dass sie bedingungslos aufrufbar sein müssen, da jede bedingte Importation oder jeder bedingte Aufruf Möglichkeiten zum Abfangen, Unterdrücken oder Modifizieren von Ereignissen bietet.
Da es eines der Kernmodule ist, ist das sys-Modul etwas vor Angriffen durch Modul-Shadowing geschützt. Das Ersetzen von sys durch ein ausreichend funktionales Modul, das die Anwendung immer noch ausführen kann, ist eine wesentlich komplexere Aufgabe als das Ersetzen eines Moduls mit nur einer Funktion von Interesse. Ein Angreifer, der die Fähigkeit hat, das sys-Modul zu überschatten, ist bereits in der Lage, beliebigen Code aus Dateien auszuführen, während ein audit-Modul mit einer einzigen Zeile in einer .pth-Datei überall im Suchpfad ersetzt werden könnte.
import sys; sys.modules['audit'] = type('audit', (object,),
{'audit': lambda *a: None, 'addhook': lambda *a: None})
Mehrere Schutzschichten existieren bereits gegen Monkey-Patching-Angriffe auf sys oder audit, aber Zuweisungen oder Einfügungen in sys.modules werden nicht auditiert.
Diese Idee wird abgelehnt, da sie es trivial macht, alle Aufrufe an audit zu unterdrücken.
Flag in sys.flags zur Kennzeichnung des „auditierten“ Modus
Der Vorschlag ist, einen Wert in sys.flags hinzuzufügen, um anzuzeigen, wann Python im „sicheren“ oder „auditierten“ Modus läuft. Dies würde es Anwendungen ermöglichen, zu erkennen, wann bestimmte Funktionen aktiviert sind oder wann Hooks hinzugefügt wurden, und ihr Verhalten entsprechend anzupassen.
Derzeit sind uns keine legitimen Gründe bekannt, warum ein Programm im Beisein von Audit Hooks anders reagieren sollte.
Sowohl die auf Anwendungsebene verfügbaren APIs sys.audit als auch io.open_code sind immer vorhanden und funktionsfähig, unabhängig davon, ob der reguläre python-Eintrittspunkt oder ein alternativer Eintrittspunkt verwendet wird. Aufrufer können nicht feststellen, ob Hooks hinzugefügt wurden (außer durch Seitenkanalanalysen), noch müssen sie das. Die Aufrufe sollten schnell genug sein, dass Aufrufer sie nicht vermeiden müssen, und das Programm ist dafür verantwortlich, sicherzustellen, dass alle hinzugefügten Hooks schnell genug sind, um die Anwendungsleistung nicht zu beeinträchtigen.
Das Argument, dass dies „Sicherheit durch Obskurität“ sei, ist gültig, aber irrelevant. Sicherheit durch Obskurität ist nur dann ein Problem, wenn keine anderen Schutzmechanismen vorhanden sind; Obskurität als erster Schritt zur Vermeidung eines Angriffs wird dringend empfohlen (siehe diesen Artikel zur Diskussion).
Diese Idee wird abgelehnt, da es keine angemessenen Gründe gibt, warum eine Anwendung ihr Verhalten basierend auf der Nutzung dieser APIs ändern sollte.
Warum keine Sandbox
Sandboxing von CPython wurde in der Vergangenheit viele Male versucht, und jeder frühere Versuch ist fehlgeschlagen. Grundsätzlich besteht das Problem darin, dass bestimmte Funktionalitäten eingeschränkt werden müssen, wenn der sandboxed Code ausgeführt wird, aber ansonsten für den normalen Betrieb von Python verfügbar sein müssen. Beispielsweise bricht das vollständige Entfernen der Fähigkeit, Zeichenketten in Bytecode zu kompilieren, auch die Möglichkeit, Module aus Quellcode zu importieren, und wenn es nicht vollständig entfernt wird, gibt es zu viele Möglichkeiten, indirekt auf diese Funktionalität zuzugreifen. Es gibt noch keinen praktikablen Weg, generisch zu bestimmen, ob eine gegebene Operation „sicher“ ist oder nicht. Weitere Informationen und Referenzen finden Sie unter [2].
Dieser Vorschlag versucht nicht, die Funktionalität einzuschränken, sondern deckt einfach die Tatsache auf, dass die Funktionalität genutzt wird. Insbesondere für Einbruchsszenarien ist die Erkennung deutlich wichtiger als die frühzeitige Verhinderung (da eine frühzeitige Verhinderung Angreifer im Allgemeinen dazu veranlasst, einen alternativen, weniger nachweisbaren Ansatz zu verwenden). Die Verfügbarkeit von Audit-Hooks ändert die Angriffsfläche von Python in keiner Weise, ermöglicht es Verteidigern jedoch, Python auf Arten in ihre Umgebung zu integrieren, die derzeit nicht möglich sind.
Da Audit-Hooks die Möglichkeit haben, das Auftreten einer Operation sicher zu verhindern, ermöglicht diese Funktion die Bereitstellung eines gewissen Maßes an Sandboxing. In den meisten Fällen ist jedoch die Absicht, die Protokollierung zu ermöglichen, anstatt eine Sandbox zu erstellen.
Beziehung zu PEP 551
Diese API wurde ursprünglich als Teil von PEP 551 Security Transparency in the Python Runtime vorgestellt.
Zur einfacheren Überprüfung und aufgrund der breiteren Anwendbarkeit dieser APIs über Sicherheitsaspekte hinaus wird das API-Design nun separat dargestellt.
PEP 551 ist ein informativer PEP, der erörtert, wie Python in eine sichere oder auditierte Umgebung integriert werden kann.
Referenzen
Urheberrecht
Copyright (c) 2019 von Microsoft Corporation. Dieses Material darf nur unter den Bedingungen der Open Publication License, v1.0 oder später (die neueste Version ist derzeit verfügbar unter https://spdx.org/licenses/OPUBL-1.0.html) vertrieben werden.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0578.rst
Zuletzt geändert: 2024-06-03 14:51:21 GMT