Remote-Debugging-Anbindungsprotokoll¶
Dieses Protokoll ermöglicht externen Tools, sich an einen laufenden CPython-Prozess anzubinden und Python-Code remote auszuführen.
Die meisten Plattformen erfordern erhöhte Berechtigungen, um sich an einen anderen Python-Prozess anzubinden.
Berechtigungsanforderungen¶
Das Anhängen an einen laufenden Python-Prozess für Remote-Debugging erfordert auf den meisten Plattformen erhöhte Berechtigungen. Die spezifischen Anforderungen und Schritte zur Fehlerbehebung hängen von Ihrem Betriebssystem ab.
Linux
Der Tracer-Prozess muss die CAP_SYS_PTRACE-Fähigkeit oder äquivalente Berechtigungen besitzen. Sie können nur Prozesse verfolgen, die Ihnen gehören und die Sie signalisieren können. Die Verfolgung kann fehlschlagen, wenn der Prozess bereits verfolgt wird oder wenn er mit Set-User-ID oder Set-Group-ID läuft. Sicherheitsmodule wie Yama können die Verfolgung weiter einschränken.
Um ptrace-Beschränkungen vorübergehend zu lockern (bis zum Neustart), führen Sie Folgendes aus:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Hinweis
Das Deaktivieren von ptrace_scope reduziert die Systemsicherheit und sollte nur in vertrauenswürdigen Umgebungen erfolgen.
Wenn Sie sich in einem Container befinden, verwenden Sie --cap-add=SYS_PTRACE oder --privileged und führen Sie den Befehl bei Bedarf als root aus.
Versuchen Sie, den Befehl mit erhöhten Berechtigungen erneut auszuführen
sudo -E !!
macOS
Um sich an einen anderen Prozess anzubinden, müssen Sie Ihr Debugging-Tool normalerweise mit erhöhten Berechtigungen ausführen. Dies kann durch die Verwendung von sudo oder durch Ausführen als root erfolgen.
Selbst beim Anhängen an Prozesse, die Ihnen gehören, kann macOS das Debugging blockieren, es sei denn, der Debugger wird aufgrund von Systemsicherheitsbeschränkungen mit Root-Berechtigungen ausgeführt.
Windows
Um sich an einen anderen Prozess anzubinden, müssen Sie Ihr Debugging-Tool in der Regel mit Administratorrechten ausführen. Starten Sie die Eingabeaufforderung oder das Terminal als Administrator.
Einige Prozesse sind möglicherweise auch mit Administratorrechten unzugänglich, es sei denn, Sie verfügen über die Berechtigung SeDebugPrivilege.
Um Probleme mit Datei- oder Ordnerzugriffen zu beheben, passen Sie die Sicherheitseinstellungen an
Klicken Sie mit der rechten Maustaste auf die Datei oder den Ordner und wählen Sie **Eigenschaften**.
Gehen Sie zur Registerkarte **Sicherheit**, um Benutzer und Gruppen mit Zugriff anzuzeigen.
Klicken Sie auf **Bearbeiten**, um Berechtigungen zu ändern.
Wählen Sie Ihr Benutzerkonto aus.
Prüfen Sie unter **Berechtigungen** die Optionen **Lesen** oder **Vollzugriff**, je nach Bedarf.
Klicken Sie auf **Übernehmen** und dann auf **OK**, um die Änderungen zu bestätigen.
Hinweis
Stellen Sie sicher, dass Sie alle Berechtigungsanforderungen erfüllt haben, bevor Sie fortfahren.
Dieser Abschnitt beschreibt das Low-Level-Protokoll, das es externen Tools ermöglicht, ein Python-Skript in einen laufenden CPython-Prozess einzuschleusen und auszuführen.
Dieser Mechanismus bildet die Grundlage für die Funktion sys.remote_exec(), die einen entfernten Python-Prozess anweist, eine .py-Datei auszuführen. Dieser Abschnitt dokumentiert jedoch nicht die Verwendung dieser Funktion. Stattdessen bietet er eine detaillierte Erklärung des zugrunde liegenden Protokolls, das die pid eines Ziel-Python-Prozesses und den Pfad zu einer auszuführenden Python-Quelldatei als Eingabe nimmt. Diese Informationen unterstützen die unabhängige Neuentwicklung des Protokolls, unabhängig von der Programmiersprache.
Warnung
Die Ausführung des eingeschleusten Skripts hängt davon ab, ob der Interpreter einen sicheren Auswertungspunkt erreicht. Daher kann die Ausführung je nach Laufzeitstatus des Zielprozesses verzögert werden.
Nach dem Einschleusen wird das Skript vom Interpreter im Zielprozess ausgeführt, sobald ein sicherer Auswertungspunkt erreicht wird. Dieser Ansatz ermöglicht Remote-Ausführungsmöglichkeiten, ohne das Verhalten oder die Struktur der laufenden Python-Anwendung zu ändern.
Die folgenden Abschnitte beschreiben das Protokoll Schritt für Schritt, einschließlich Techniken zum Auffinden von Interpreterstrukturen im Speicher, zum sicheren Zugriff auf interne Felder und zum Auslösen der Codeausführung. Plattformspezifische Variationen werden nach Bedarf vermerkt, und Beispielimplementierungen werden zur Verdeutlichung jeder Operation bereitgestellt.
Fundort der PyRuntime-Struktur¶
CPython platziert die PyRuntime-Struktur in einem dedizierten Binärabschnitt, um externen Tools die Laufzeitfindung zu erleichtern. Der Name und das Format dieses Abschnitts variieren je nach Plattform. Zum Beispiel wird .PyRuntime auf ELF-Systemen verwendet und __DATA,__PyRuntime auf macOS. Tools können den Offset dieser Struktur finden, indem sie die Binärdatei auf der Festplatte untersuchen.
Die PyRuntime-Struktur enthält den globalen Interpreterstatus von CPython und bietet Zugriff auf weitere interne Daten, einschließlich der Liste der Interpreter, Thread-Status und Debugger-Unterstützungsfelder.
Um mit einem entfernten Python-Prozess zu arbeiten, muss ein Debugger zuerst die Speicheradresse der PyRuntime-Struktur im Zielprozess finden. Diese Adresse kann nicht fest codiert oder aus einem Symbolnamen berechnet werden, da sie davon abhängt, wo das Betriebssystem die Binärdatei geladen hat.
Die Methode zum Finden von PyRuntime hängt von der Plattform ab, aber die Schritte sind im Allgemeinen die gleichen
Finden Sie die Basisadresse, an der die Python-Binärdatei oder die freigegebene Bibliothek im Zielprozess geladen wurde.
Verwenden Sie die Binärdatei auf der Festplatte, um den Offset des Abschnitts
.PyRuntimezu finden.Addieren Sie den Abschnitts-Offset zur Basisadresse, um die Adresse im Speicher zu berechnen.
Die folgenden Abschnitte erklären, wie dies auf jeder unterstützten Plattform durchgeführt wird, und enthalten Beispielcode.
Linux (ELF)
So finden Sie die PyRuntime-Struktur unter Linux
Lesen Sie die Speicherzuordnung des Prozesses (z. B.
/proc/<pid>/maps), um die Adresse zu finden, an der die Python-ausführbare Datei oderlibpythongeladen wurde.Analysieren Sie die ELF-Abschnittsüberschriften in der Binärdatei, um den Offset des Abschnitts
.PyRuntimezu erhalten.Addieren Sie diesen Offset zur Basisadresse aus Schritt 1, um die Speicheradresse von
PyRuntimezu erhalten.
Die folgende Implementierung ist ein Beispiel
def find_py_runtime_linux(pid: int) -> int:
# Step 1: Try to find the Python executable in memory
binary_path, base_address = find_mapped_binary(
pid, name_contains="python"
)
# Step 2: Fallback to shared library if executable is not found
if binary_path is None:
binary_path, base_address = find_mapped_binary(
pid, name_contains="libpython"
)
# Step 3: Parse ELF headers to get .PyRuntime section offset
section_offset = parse_elf_section_offset(
binary_path, ".PyRuntime"
)
# Step 4: Compute PyRuntime address in memory
return base_address + section_offset
Auf Linux-Systemen gibt es zwei Hauptansätze, um Speicher von einem anderen Prozess zu lesen. Der erste ist über das /proc-Dateisystem, insbesondere durch Lesen aus /proc/[pid]/mem, das direkten Zugriff auf den Speicher des Prozesses bietet. Dies erfordert entsprechende Berechtigungen – entweder als derselbe Benutzer wie der Zielprozess oder als Root. Der zweite Ansatz ist die Verwendung des process_vm_readv()-Systemaufrufs, der eine effizientere Möglichkeit bietet, Speicher zwischen Prozessen zu kopieren. Obwohl die PTRACE_PEEKTEXT-Operation von ptrace ebenfalls zum Lesen von Speicher verwendet werden kann, ist sie erheblich langsamer, da sie nur ein Wort auf einmal liest und mehrere Kontextwechsel zwischen den Tracer- und Tracee-Prozessen erfordert.
Für die Analyse von ELF-Abschnitten werden die ELF-Dateiformatstrukturen aus der Binärdatei auf der Festplatte gelesen und interpretiert. Der ELF-Header enthält einen Zeiger auf die Abschnittsüberschriften-Tabelle. Jede Abschnittsüberschrift enthält Metadaten zu einem Abschnitt, einschließlich seines Namens (gespeichert in einer separaten Zeichentabelle), seines Offsets und seiner Größe. Um einen bestimmten Abschnitt wie .PyRuntime zu finden, müssen Sie diese Überschriften durchlaufen und den Abschnittsnamen abgleichen. Die Abschnittsüberschrift liefert dann den Offset, an dem sich dieser Abschnitt in der Datei befindet, der zur Berechnung seiner Laufzeitadresse verwendet werden kann, wenn die Binärdatei in den Speicher geladen wird.
Sie können mehr über das ELF-Dateiformat in der ELF-Spezifikation lesen.
macOS (Mach-O)
So finden Sie die PyRuntime-Struktur unter macOS
Rufen Sie
task_for_pid()auf, um denmach_port_t-Task-Port für den Zielprozess zu erhalten. Dieses Handle wird benötigt, um Speicher mit APIs wiemach_vm_read_overwriteundmach_vm_regionzu lesen.Scannen Sie die Speicherbereiche, um denjenigen zu finden, der die Python-ausführbare Datei oder
libpythonenthält.Laden Sie die Binärdatei von der Festplatte und analysieren Sie die Mach-O-Header, um den Abschnitt namens
PyRuntimeim Segment__DATAzu finden. Unter macOS werden Symbolnamen automatisch mit einem Unterstrich versehen, sodass das SymbolPyRuntimein der Symboltabelle als_PyRuntimeerscheint, der Abschnittsname bleibt jedoch unberührt.
Die folgende Implementierung ist ein Beispiel
def find_py_runtime_macos(pid: int) -> int:
# Step 1: Get access to the process's memory
handle = get_memory_access_handle(pid)
# Step 2: Try to find the Python executable in memory
binary_path, base_address = find_mapped_binary(
handle, name_contains="python"
)
# Step 3: Fallback to libpython if the executable is not found
if binary_path is None:
binary_path, base_address = find_mapped_binary(
handle, name_contains="libpython"
)
# Step 4: Parse Mach-O headers to get __DATA,__PyRuntime section offset
section_offset = parse_macho_section_offset(
binary_path, "__DATA", "__PyRuntime"
)
# Step 5: Compute the PyRuntime address in memory
return base_address + section_offset
Unter macOS erfordert der Zugriff auf den Speicher eines anderen Prozesses die Verwendung von Mach-O-spezifischen APIs und Dateiformaten. Der erste Schritt besteht darin, ein task_port-Handle über task_for_pid() zu erhalten, das Zugriff auf den Adressraum des Zielprozesses bietet. Dieses Handle ermöglicht Speicheroperationen über APIs wie mach_vm_read_overwrite().
Der Prozessspeicher kann mit mach_vm_region() gescannt werden, um den virtuellen Adressraum zu durchsuchen, während proc_regionfilename() hilft, zu identifizieren, welche Binärdateien in jedem Speicherbereich geladen sind. Wenn die Python-Binärdatei oder -Bibliothek gefunden wurde, müssen ihre Mach-O-Header analysiert werden, um die PyRuntime-Struktur zu lokalisieren.
Das Mach-O-Format organisiert Code und Daten in Segmenten und Abschnitten. Die PyRuntime-Struktur befindet sich in einem Abschnitt namens __PyRuntime innerhalb des Segments __DATA. Die eigentliche Laufzeitadressberechnung beinhaltet das Auffinden des __TEXT-Segments, das als Basisadresse der Binärdatei dient, und dann das Auffinden des Segments __DATA, das unseren Zielabschnitt enthält. Die endgültige Adresse wird durch die Kombination der Basisadresse mit den entsprechenden Abschnitts-Offsets aus den Mach-O-Headern berechnet.
Beachten Sie, dass der Zugriff auf den Speicher eines anderen Prozesses unter macOS in der Regel erhöhte Berechtigungen erfordert – entweder Root-Zugriff oder spezielle Sicherheitsberechtigungen, die dem Debugging-Prozess gewährt werden.
Windows (PE)
So finden Sie die PyRuntime-Struktur unter Windows
Verwenden Sie die ToolHelp-API, um alle im Zielprozess geladenen Module aufzulisten. Dies geschieht mit Funktionen wie CreateToolhelp32Snapshot, Module32First und Module32Next.
Identifizieren Sie das Modul, das
python.exeoderpythonXY.dllentspricht, wobeiXundYdie Haupt- und Nebenversionsnummern der Python-Version sind, und zeichnen Sie dessen Basisadresse auf.Suchen Sie den Abschnitt
PyRuntim. Aufgrund des 8-Zeichen-Limits des PE-Formats für Abschnittsnamen (definiert alsIMAGE_SIZEOF_SHORT_NAME) wird der ursprüngliche NamePyRuntimeabgekürzt. Dieser Abschnitt enthält diePyRuntime-Struktur.Rufen Sie die relative virtuelle Adresse (RVA) des Abschnitts ab und addieren Sie sie zur Basisadresse des Moduls.
Die folgende Implementierung ist ein Beispiel
def find_py_runtime_windows(pid: int) -> int:
# Step 1: Try to find the Python executable in memory
binary_path, base_address = find_loaded_module(
pid, name_contains="python"
)
# Step 2: Fallback to shared pythonXY.dll if the executable is not
# found
if binary_path is None:
binary_path, base_address = find_loaded_module(
pid, name_contains="python3"
)
# Step 3: Parse PE section headers to get the RVA of the PyRuntime
# section. The section name appears as "PyRuntim" due to the
# 8-character limit defined by the PE format (IMAGE_SIZEOF_SHORT_NAME).
section_rva = parse_pe_section_offset(binary_path, "PyRuntim")
# Step 4: Compute PyRuntime address in memory
return base_address + section_rva
Unter Windows erfordert der Zugriff auf den Speicher eines anderen Prozesses die Verwendung von Windows-API-Funktionen wie CreateToolhelp32Snapshot() und Module32First()/Module32Next(), um geladene Module aufzulisten. Die Funktion OpenProcess() stellt ein Handle zum Zugriff auf den Speicherbereich des Zielprozesses bereit und ermöglicht Speicheroperationen über ReadProcessMemory().
Der Prozessspeicher kann durch Auflisten geladener Module untersucht werden, um die Python-Binärdatei oder DLL zu finden. Wenn sie gefunden wurde, müssen ihre PE-Header analysiert werden, um die PyRuntime-Struktur zu lokalisieren.
Das PE-Format organisiert Code und Daten in Abschnitten. Die PyRuntime-Struktur befindet sich in einem Abschnitt namens „PyRuntim“ (abgekürzt von „PyRuntime“ aufgrund des 8-Zeichen-Namenslimits von PE). Die eigentliche Laufzeitadressberechnung beinhaltet das Auffinden der Basisadresse des Moduls aus dem Modul-Eintrag und dann das Auffinden unseres Zielabschnitts in den PE-Headern. Die endgültige Adresse wird durch die Kombination der Basisadresse mit der virtuellen Adresse des Abschnitts aus den PE-Abschnittsüberschriften berechnet.
Beachten Sie, dass der Zugriff auf den Speicher eines anderen Prozesses unter Windows in der Regel entsprechende Berechtigungen erfordert – entweder Administratorzugriff oder die Berechtigung SeDebugPrivilege, die dem Debugging-Prozess gewährt wird.
Lesen von _Py_DebugOffsets¶
Sobald die Adresse der PyRuntime-Struktur ermittelt wurde, besteht der nächste Schritt darin, die _Py_DebugOffsets-Struktur zu lesen, die sich am Anfang des PyRuntime-Blocks befindet.
Diese Struktur enthält versionsspezifische Feld-Offsets, die benötigt werden, um Interpreter- und Thread-Status-Speicher sicher zu lesen. Diese Offsets variieren zwischen CPython-Versionen und müssen vor der Verwendung überprüft werden, um sicherzustellen, dass sie kompatibel sind.
Um die Debug-Offsets zu lesen und zu überprüfen, gehen Sie wie folgt vor:
Lesen Sie Speicher vom Zielprozess beginnend an der
PyRuntime-Adresse, wobei die Anzahl der Bytes der_Py_DebugOffsets-Struktur abgedeckt wird. Diese Struktur befindet sich ganz am Anfang desPyRuntime-Speicherblocks. Ihr Layout ist in den internen Headern von CPython definiert und bleibt innerhalb einer bestimmten Nebenversion gleich, kann sich aber in Hauptversionen ändern.Stellen Sie sicher, dass die Struktur gültige Daten enthält
Das Feld
cookiemuss mit dem erwarteten Debug-Marker übereinstimmen.Das Feld
versionmuss mit der Version des Python-Interpreters übereinstimmen, die vom Debugger verwendet wird.Wenn entweder der Debugger oder der Zielprozess eine Vorabversion (z. B. Alpha, Beta oder Release Candidate) verwendet, müssen die Versionen exakt übereinstimmen.
Das Feld
free_threadedmuss im Debugger und im Zielprozess den gleichen Wert haben.
Wenn die Struktur gültig ist, können die darin enthaltenen Offsets verwendet werden, um Felder im Speicher zu lokalisieren. Wenn eine Überprüfung fehlschlägt, sollte der Debugger den Vorgang abbrechen, um das Lesen von Speicher in einem falschen Format zu vermeiden.
Die folgende Implementierung liest und prüft _Py_DebugOffsets
def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets:
# Step 1: Read memory from the target process at the PyRuntime address
data = read_process_memory(
pid, address=py_runtime_addr, size=DEBUG_OFFSETS_SIZE
)
# Step 2: Deserialize the raw bytes into a _Py_DebugOffsets structure
debug_offsets = parse_debug_offsets(data)
# Step 3: Validate the contents of the structure
if debug_offsets.cookie != EXPECTED_COOKIE:
raise RuntimeError("Invalid or missing debug cookie")
if debug_offsets.version != LOCAL_PYTHON_VERSION:
raise RuntimeError(
"Mismatch between caller and target Python versions"
)
if debug_offsets.free_threaded != LOCAL_FREE_THREADED:
raise RuntimeError("Mismatch in free-threaded configuration")
return debug_offsets
Warnung
Prozessunterbrechung empfohlen
Um Race Conditions zu vermeiden und die Speicherintegrität zu gewährleisten, wird dringend empfohlen, den Zielprozess zu unterbrechen, bevor Vorgänge zum Lesen oder Schreiben interner Interpreterzustände durchgeführt werden. Die Python-Laufzeitumgebung kann während der normalen Ausführung gleichzeitig Interpreterdatenstrukturen mutieren – z. B. Threads erstellen oder zerstören. Dies kann zu ungültigen Speicherlese- oder Schreibvorgängen führen.
Ein Debugger kann die Ausführung unterbrechen, indem er sich mit ptrace an den Prozess anhängt oder ein SIGSTOP-Signal sendet. Die Ausführung sollte erst fortgesetzt werden, nachdem die Speicheroperationen auf der Debuggerseite abgeschlossen sind.
Hinweis
Einige Tools, wie z. B. Profiler oder sampling-basierte Debugger, können auf einem laufenden Prozess ohne Unterbrechung arbeiten. In solchen Fällen müssen Tools explizit so konzipiert sein, dass sie teilweise aktualisierte oder inkonsistente Speicherdaten verarbeiten können. Für die meisten Debugger-Implementierungen bleibt die Unterbrechung des Prozesses der sicherste und robusteste Ansatz.
Fundort des Interpreters und des Thread-Status¶
Bevor Code in einem entfernten Python-Prozess eingeschleust und ausgeführt werden kann, muss der Debugger einen Thread auswählen, in dem die Ausführung geplant wird. Dies ist notwendig, da die Steuerungsfelder, die zur Durchführung der Remote-Code-Injektion verwendet werden, in der Struktur _PyRemoteDebuggerSupport liegen, die in einem PyThreadState-Objekt eingebettet ist. Diese Felder werden vom Debugger geändert, um die Ausführung von eingeschleusten Skripten anzufordern.
Die Struktur PyThreadState repräsentiert einen Thread, der innerhalb eines Python-Interpreters läuft. Sie verwaltet den Ausführungskontext des Threads und enthält die für die Debugger-Koordination erforderlichen Felder. Das Auffinden eines gültigen PyThreadState ist daher eine wichtige Voraussetzung für die Auslösung der Ausführung aus der Ferne.
Ein Thread wird normalerweise basierend auf seiner Rolle oder ID ausgewählt. In den meisten Fällen wird der Hauptthread verwendet, aber einige Tools können einen bestimmten Thread anhand seiner nativen Thread-ID anvisieren. Sobald der Zielthread ausgewählt ist, muss der Debugger sowohl den Interpreter als auch die zugehörigen Thread-Status-Strukturen im Speicher finden.
Die relevanten internen Strukturen sind wie folgt definiert:
PyInterpreterStaterepräsentiert eine isolierte Python-Interpreterinstanz. Jeder Interpreter verwaltet seine eigene Sammlung von importierten Modulen, integriertem Zustand und eine Liste von Thread-Status. Obwohl die meisten Python-Anwendungen einen einzigen Interpreter verwenden, unterstützt CPython mehrere Interpreter im selben Prozess.PyThreadStaterepräsentiert einen Thread, der innerhalb eines Interpreters läuft. Er enthält den Ausführungsstatus und die Steuerungsfelder, die vom Debugger verwendet werden.
So finden Sie einen Thread:
Verwenden Sie den Offset
runtime_state.interpreters_head, um die Adresse des ersten Interpreters in derPyRuntime-Struktur zu erhalten. Dies ist der Einstiegspunkt in die verkettete Liste der aktiven Interpreter.Verwenden Sie den Offset
interpreter_state.threads_main, um auf den Hauptthread-Status zuzugreifen, der dem ausgewählten Interpreter zugeordnet ist. Dies ist in der Regel der zuverlässigste Thread, der anvisiert werden kann.Optional können Sie den Offset
interpreter_state.threads_headverwenden, um die verkettete Liste aller Thread-Status zu durchlaufen. JedePyThreadState-Struktur enthält ein Feldnative_thread_id, das mit einer Ziel-Thread-ID verglichen werden kann, um einen bestimmten Thread zu finden.Sobald ein gültiger
PyThreadStategefunden wurde, kann seine Adresse in späteren Schritten des Protokolls verwendet werden, z. B. beim Schreiben von Debugger-Steuerungsfeldern und beim Planen der Ausführung.
Die folgende Implementierung zeigt, wie der Hauptthread-Status gefunden wird
def find_main_thread_state(
pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets,
) -> int:
# Step 1: Read interpreters_head from PyRuntime
interp_head_ptr = (
py_runtime_addr + debug_offsets.runtime_state.interpreters_head
)
interp_addr = read_pointer(pid, interp_head_ptr)
if interp_addr == 0:
raise RuntimeError("No interpreter found in the target process")
# Step 2: Read the threads_main pointer from the interpreter
threads_main_ptr = (
interp_addr + debug_offsets.interpreter_state.threads_main
)
thread_state_addr = read_pointer(pid, threads_main_ptr)
if thread_state_addr == 0:
raise RuntimeError("Main thread state is not available")
return thread_state_addr
Das folgende Beispiel zeigt, wie ein Thread anhand seiner nativen Thread-ID gefunden wird
def find_thread_by_id(
pid: int,
interp_addr: int,
debug_offsets: DebugOffsets,
target_tid: int,
) -> int:
# Start at threads_head and walk the linked list
thread_ptr = read_pointer(
pid,
interp_addr + debug_offsets.interpreter_state.threads_head
)
while thread_ptr:
native_tid_ptr = (
thread_ptr + debug_offsets.thread_state.native_thread_id
)
native_tid = read_int(pid, native_tid_ptr)
if native_tid == target_tid:
return thread_ptr
thread_ptr = read_pointer(
pid,
thread_ptr + debug_offsets.thread_state.next
)
raise RuntimeError("Thread with the given ID was not found")
Sobald ein gültiger Thread-Status gefunden wurde, kann der Debugger mit der Änderung seiner Steuerungsfelder und der Planung der Ausführung fortfahren, wie im nächsten Abschnitt beschrieben.
Schreiben von Steuerungsinformationen¶
Sobald eine gültige PyThreadState-Struktur identifiziert wurde, kann der Debugger Steuerungsfelder darin ändern, um die Ausführung eines angegebenen Python-Skripts zu planen. Diese Steuerungsfelder werden regelmäßig vom Interpreter überprüft und lösen bei korrekter Einstellung die Ausführung von Remote-Code an einem sicheren Punkt in der Ausführungsschleife aus.
Jede PyThreadState enthält eine Struktur _PyRemoteDebuggerSupport, die für die Kommunikation zwischen dem Debugger und dem Interpreter verwendet wird. Die Speicherorte ihrer Felder werden durch die Struktur _Py_DebugOffsets definiert und umfassen Folgendes:
debugger_script_path: Ein Puffer fester Größe, der den vollständigen Pfad zu einer Python-Quelldatei (.py) enthält. Diese Datei muss für den Zielprozess zugänglich und lesbar sein, wenn die Ausführung ausgelöst wird.debugger_pending_call: Ein Integer-Flag. Wenn dieser Wert auf1gesetzt wird, wird dem Interpreter mitgeteilt, dass ein Skript zur Ausführung bereit ist.eval_breaker: Ein Feld, das vom Interpreter während der Ausführung überprüft wird. Durch das Setzen von Bit 5 (_PY_EVAL_PLEASE_STOP_BIT, Wert1U << 5) in diesem Feld wird der Interpreter angehalten und auf Debugger-Aktivitäten überprüft.
Um die Einschleusung abzuschließen, muss der Debugger folgende Schritte ausführen:
Schreiben Sie den vollständigen Skriptpfad in den Puffer
debugger_script_path.Setzen Sie
debugger_pending_callauf1.Lesen Sie den aktuellen Wert von
eval_breaker, setzen Sie Bit 5 (_PY_EVAL_PLEASE_STOP_BIT) und schreiben Sie den aktualisierten Wert zurück. Dies signalisiert dem Interpreter, auf Debugger-Aktivitäten zu prüfen.
Die folgende Implementierung ist ein Beispiel
def inject_script(
pid: int,
thread_state_addr: int,
debug_offsets: DebugOffsets,
script_path: str
) -> None:
# Compute the base offset of _PyRemoteDebuggerSupport
support_base = (
thread_state_addr +
debug_offsets.debugger_support.remote_debugger_support
)
# Step 1: Write the script path into debugger_script_path
script_path_ptr = (
support_base +
debug_offsets.debugger_support.debugger_script_path
)
write_string(pid, script_path_ptr, script_path)
# Step 2: Set debugger_pending_call to 1
pending_ptr = (
support_base +
debug_offsets.debugger_support.debugger_pending_call
)
write_int(pid, pending_ptr, 1)
# Step 3: Set _PY_EVAL_PLEASE_STOP_BIT (bit 5, value 1 << 5) in
# eval_breaker
eval_breaker_ptr = (
thread_state_addr +
debug_offsets.debugger_support.eval_breaker
)
breaker = read_int(pid, eval_breaker_ptr)
breaker |= (1 << 5)
write_int(pid, eval_breaker_ptr, breaker)
Nachdem diese Felder gesetzt sind, kann der Debugger den Prozess fortsetzen (falls er angehalten wurde). Der Interpreter verarbeitet die Anfrage am nächsten sicheren Auswertungspunkt, lädt das Skript von der Festplatte und führt es aus.
Es liegt in der Verantwortung des Debuggers, sicherzustellen, dass die Skriptdatei während der Ausführung für den Zielprozess vorhanden und zugänglich bleibt.
Hinweis
Die Skriptausführung ist asynchron. Die Skriptdatei kann nach dem Einschleusen nicht sofort gelöscht werden. Der Debugger sollte warten, bis das eingeschleuste Skript eine beobachtbare Wirkung erzielt hat, bevor die Datei entfernt wird. Diese Wirkung hängt davon ab, was das Skript tun soll. Ein Debugger könnte beispielsweise warten, bis der entfernte Prozess eine Socket-Verbindung zurück aufbaut, bevor er das Skript entfernt. Sobald eine solche Wirkung beobachtet wird, kann davon ausgegangen werden, dass die Datei nicht mehr benötigt wird.
Zusammenfassung¶
Um ein Python-Skript in einen entfernten Prozess einzuschleusen und auszuführen:
Lokalisieren Sie die
PyRuntime-Struktur im Speicher des Zielprozesses.Lesen und validieren Sie die
_Py_DebugOffsets-Struktur am Anfang vonPyRuntime.Verwenden Sie die Offsets, um eine gültige
PyThreadStatezu lokalisieren.Schreiben Sie den Pfad zu einem Python-Skript in
debugger_script_path.Setzen Sie das Flag
debugger_pending_callauf1.Setzen Sie
_PY_EVAL_PLEASE_STOP_BITim Feldeval_breaker.Setzen Sie den Prozess fort (falls angehalten). Das Skript wird am nächsten sicheren Auswertungspunkt ausgeführt.