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

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

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

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

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

Категории

Все категории
Loading categories
regreSSHion — This is a POC I wrote for CVE-2024-6387 | Kitploit
Инструменты/GitHubGitHub/teamos-hub/regresshion
Vulnerability AnalysisExploitationPenetration TestingRed TeamingRemote Access ToolBinary Exploitation
GitHubteamos-hub/regresshion

regreSSHion

This is a POC I wrote for CVE-2024-6387

Репозиторий
12 лет назадЕщё не проверено

Популярное

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

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

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

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

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

Qualys Security Advisory

regreSSHion: RCE в сервере OpenSSH на системах Linux на базе glibc (CVE-2024-6387)

======================================================================== Содержание

Краткое описание SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, от 2005)

  • Теория
  • Практика
  • Время SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, от 2006)
  • Теория, первая попытка
  • Теория, вторая попытка
  • Практика
  • Время SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, от 2024)
  • Теория
  • Практика
  • Время В направлении эксплойта для amd64 Патчи и смягчение Благодарности Хронология

======================================================================== Краткое описание

root@kitploit:~
Всё, что нужно — это прыжок веры
    -- The Interrupters, "Leap of Faith"

Предварительное замечание: OpenSSH — одно из самых защищённых программных обеспечений в мире; эта уязвимость — одна оплошность в иначе почти безупречной реализации. Его архитектура защиты в глубину и код являются образцом и источником вдохновения, и мы благодарим разработчиков OpenSSH за их образцовую работу.

Мы обнаружили уязвимость (состояние гонки в обработчике сигналов) в сервере OpenSSH (sshd): если клиент не проходит аутентификацию в течение LoginGraceTime секунд (по умолчанию 120, 600 в старых версиях OpenSSH), то обработчик SIGALRM sshd вызывается асинхронно, но этот обработчик сигнала вызывает различные функции, которые не являются async-signal-safe (например, syslog()). Это состояние гонки затрагивает sshd в его конфигурации по умолчанию.

В ходе расследования мы поняли, что эта уязвимость на самом деле является регрессией CVE-2006-5051 ("Состояние гонки в обработчике сигналов в OpenSSH до версии 4.4, позволяющее удалённым злоумышленникам вызвать отказ в обслуживании (аварийное завершение) и, возможно, выполнить произвольный код"), которая была сообщена в 2006 году Марком Даудом.

Эта регрессия была введена в октябре 2020 года (OpenSSH 8.5p1) коммитом 752250c ("revised log infrastructure for OpenSSH"), который случайно удалил "#ifdef DO_LOG_SAFE_IN_SIGHAND" из sigdie(), функции, которая напрямую вызывается обработчиком SIGALRM в sshd. Другими словами:

  • OpenSSH < 4.4p1 уязвим к этому состоянию гонки в обработчике сигналов, если не был пропатчен с обратной совместимостью против CVE-2006-5051 или не исправлен против CVE-2008-4109, который был некорректным исправлением для CVE-2006-5051;

  • 4.4p1 <= OpenSSH < 8.5p1 не уязвим к этому состоянию гонки в обработчике сигналов (потому что "#ifdef DO_LOG_SAFE_IN_SIGHAND", добавленный в sigdie() патчем для CVE-2006-5051, превратил эту небезопасную функцию в безопасный вызов _exit(1));

  • 8.5p1 <= OpenSSH < 9.8p1 снова уязвим к этому состоянию гонки в обработчике сигналов (потому что "#ifdef DO_LOG_SAFE_IN_SIGHAND" был случайно удалён из sigdie()).

Эта уязвимость эксплуатируется удалённо на системах Linux на базе glibc, где syslog() сам вызывает async-signal-unsafe функции (например, malloc() и free()): неаутентифицированное удалённое выполнение кода с правами root, поскольку затрагивает привилегированный код sshd, который не изолирован (sandboxed) и работает с полными правами. Мы не исследовали другие libc или операционные системы; но OpenBSD, заметим, не уязвим, потому что его обработчик SIGALRM вызывает syslog_r(), более безопасную для async-signal версию syslog(), которая была изобретена OpenBSD в 2001 году.

Чтобы удалённо эксплуатировать эту уязвимость (насколько нам известно, CVE-2006-5051 ранее никогда не был успешно эксплуатирован), мы черпали вдохновение из провидческой статьи "Delivering Signals for Fun and Profit", опубликованной в 2001 году Михалом Залевским:

https://lcamtuf.coredump.cx/signals.txt

Тем не менее, мы сразу столкнулись с тремя основными проблемами:

  • С теоретической точки зрения, мы должны найти полезный путь кода, который, если его прервать в нужный момент сигналом SIGALRM, оставляет sshd в нестабильном состоянии, и затем мы должны эксплуатировать это нестабильное состояние внутри обработчика SIGALRM.

  • С практической точки зрения, мы должны найти способ достичь этого полезного пути кода в sshd и максимизировать наши шансы прервать его в нужный момент.

  • С точки зрения времени, мы должны найти способ ещё больше увеличить наши шансы прервать этот полезный путь кода в нужный момент удалённо.

Чтобы сосредоточиться на этих трёх проблемах, не борясь сразу со всеми современными защитами операционной системы (в частности, ASLR и NX), мы решили сначала эксплуатировать старые версии OpenSSH на i386, а затем, основываясь на этом опыте, современные версии:

  • Сначала, "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3", из "debian-30r6-dvd-i386-binary-1_NONUS.iso": это первая версия Debian, в которой разделение привилегий включено по умолчанию и которая пропатчена против всех критических уязвимостей той эпохи (в частности, CVE-2003-0693 и CVE-2002-0640).

    Чтобы удалённо эксплуатировать эту версию, мы прерываем вызов free() сигналом SIGALRM (внутри кода разбора открытого ключа sshd), оставляем кучу в нестабильном состоянии и эксплуатируем это нестабильное состояние во время другого вызова free() внутри обработчика SIGALRM.

    В наших экспериментах в среднем требуется ~10 000 попыток, чтобы выиграть это состояние гонки; т.е. при 10 соединениях (MaxStartups), принятых за 600 секунд (LoginGraceTime), в среднем требуется ~1 неделя, чтобы получить удалённую root-оболочку.

  • Во-вторых, "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3", из "ubuntu-6.06.1-server-i386.iso": это последняя версия Ubuntu, которая ещё уязвима к CVE-2006-5051 ("Состояние гонки в обработчике сигналов в OpenSSH до версии 4.4").

    Чтобы удалённо эксплуатировать эту версию, мы прерываем вызов pam_start() сигналом SIGALRM, оставляем одну из структур PAM в нестабильном состоянии и эксплуатируем это нестабильное состояние во время вызова pam_end() внутри обработчика SIGALRM.

    В наших экспериментах в среднем требуется ~10 000 попыток, чтобы выиграть это состояние гонки; т.е. при 10 соединениях (MaxStartups), принятых за 120 секунд (LoginGraceTime), в среднем требуется ~1-2 дня, чтобы получить удалённую root-оболочку.

  • Наконец, "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", из "debian-12.5.0-i386-DVD-1.iso": это текущая стабильная версия Debian, и она уязвима к регрессии CVE-2006-5051.

    Чтобы удалённо эксплуатировать эту версию, мы прерываем вызов malloc() сигналом SIGALRM (внутри кода разбора открытого ключа sshd), оставляем кучу в нестабильном состоянии и эксплуатируем это нестабильное состояние во время другого вызова malloc() внутри обработчика SIGALRM (точнее, внутри syslog()).

    В наших экспериментах в среднем требуется ~10 000 попыток, чтобы выиграть это состояние гонки, так что ~3-4 часа при 100 соединениях (MaxStartups), принятых за 120 секунд (LoginGraceTime). В конечном счёте, в среднем требуется ~6-8 часов, чтобы получить удалённую root-оболочку, потому что мы можем правильно угадать адрес glibc только в половине случаев (из-за ASLR).

Это исследование ещё в процессе:

  • мы нацеливались только на виртуальные машины, не на bare-metal серверы, на в основном стабильной сетевой линии (~10ms джиттер пакетов);

  • мы убеждены, что различные аспекты наших эксплойтов могут быть значительно улучшены;

  • мы начали работу над эксплойтом для amd64, что гораздо сложнее из-за более сильного ASLR.

Через несколько дней после начала нашей работы над amd64 мы заметили следующий отчёт об ошибке (в публичном Bugzilla OpenSSH) о взаимоблокировке в обработчике SIGALRM sshd:

https://bugzilla.mindrot.org/show_bug.cgi?id=3690

Поэтому мы решили немедленно связаться с разработчиками OpenSSH (чтобы сообщить им, что эта взаимоблокировка вызвана эксплуатируемой уязвимостью), приостановили нашу работу над amd64 и начали писать это уведомление.

======================================================================== SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, от 2005)


Теория

root@kitploit:~
Но это не похоже на меня, я освобождаюсь
    -- The Interrupters, "Haven't Seen the Last of Me"

Обработчик SIGALRM этой версии OpenSSH вызывает packet_close(), который вызывает buffer_free(), который вызывает xfree() и, следовательно, free(), что не является async-signal-safe:


302 grace_alarm_handler(int sig) 303 { ... 307 packet_close();

329 packet_close(void) 330 { ... 341 buffer_free(&input); 342 buffer_free(&output); 343 buffer_free(&outgoing_packet); 344 buffer_free(&incoming_packet);

35 buffer_free(Buffer *buffer) 36 { 37 memset(buffer->buf, 0, buffer->alloc); 38 xfree(buffer->buf); 39 }

51 xfree(void *ptr) 52 { 53 if (ptr == NULL) 54 fatal("xfree: NULL pointer given as argument"); 55 free(ptr); 56 }

Следовательно, мы начали читать код malloc этой glibc Debian (2.2.5), чтобы увидеть, можно ли прервать первый вызов free() сигналом SIGALRM и эксплуатировать его во время второго вызова free() внутри обработчика SIGALRM (в строках 341-344 выше). Поскольку malloc этой glibc не защищён от техники unlink(), впервые предложенной Solar Designer в 2000 году, мы быстро заметили интересный путь кода в chunk_free() (которая вызывается внутри free()):


1028 struct malloc_chunk 1029 { 1030 INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). / 1031 INTERNAL_SIZE_T size; / Size in bytes, including overhead. / 1032 struct malloc_chunk fd; /* double links -- used only if free. / 1033 struct malloc_chunk bk; 1034 };

2516 #define unlink(P, BK, FD)
2517 {
2518 BK = P->bk;
2519 FD = P->fd;
2520 FD->bk = BK;
2521 BK->fd = FD;
2522 } \

3160 chunk_free(arena ar_ptr, mchunkptr p) .... 3164 { 3165 INTERNAL_SIZE_T hd = p->size; / its head field / .... 3177 sz = hd & ~PREV_INUSE; 3178 next = chunk_at_offset(p, sz); 3179 nextsz = chunksize(next); .... 3230 if (!(inuse_bit_at_offset(next, nextsz))) / consolidate forward / 3231 { .... 3241 unlink(next, bck, fwd); .... 3244 } 3245 else 3246 set_head(next, nextsz); / clear inuse bit */ .... 3251 frontlink(ar_ptr, p, sz, idx, bck, fwd);

Чтобы эксплуатировать этот путь кода, мы организуем кучу sshd со следующим расположением (chunk_X, chunk_Y и chunk_Z — это выделенные malloc() блоки памяти, а p, s, f, b — их поля prev_size, size, fd и bk):

-----|---+---------------|---+---------------|---+---------------|----- ... |p|s|f|b| chunk_X |p|s|f|b| chunk_Y |p|s|f|b| chunk_Z | ... -----|---+---------------|---+---------------|---+---------------|----- |<------------->| user data

  • Во-первых, если вызов free(chunk_Y) прерывается сигналом SIGALRM после строки 3246, но до строки 3251, то chunk_Y уже помечен как свободный (потому что бит PREV_INUSE chunk_Z очищен в строке 3246), но ещё не связан в свой двусвязный список (в строке 3251): другими словами, указатели fd и bk chunk_Y всё ещё содержат пользовательские данные (данные, контролируемые злоумышленником).

  • Во-вторых, если (внутри обработчика SIGALRM) packet_close() вызывает free(chunk_X), то входим в блок кода в строках 3230-3244 (поскольку chunk_Y помечен как свободный) и chunk_Y раз связывается (unlink()) (в строке 3241): так называемая примитивная операция aa4bmo (почти произвольная 4-байтовая зеркальная перезапись), потому что указатели fd и bk chunk_Y всё ещё контролируются злоумышленником. Для получения дополнительной информации о технике unlink() и примитиве aa4bmo:

    https://www.openwall.com/articles/JPEG-COM-Marker-Vulnerability#exploit http://phrack.org/issues/61/6.html#article

  • Наконец, с помощью этого примитива aa4bmo мы перезаписываем указатель функции __free_hook в glibc (в этой старой версии Debian нет ASLR, а также NX) адресом нашего шелл-кода в куче, тем самым достигая удалённого выполнения кода во время следующего вызова free() в packet_close().


Практика

root@kitploit:~
Теперь они берут верх и полностью контролируют
    -- The Interrupters, "Liberty"

Чтобы провести эту атаку на sshd, мы прерываем вызов free() внутри кода разбора открытого ключа DSA в sshd (т.е. строка 144 ниже — это наш free(chunk_Y)) и эксплуатируем его во время одного из вызовов free() в packet_close() (т.е. одна из строк 341-344 выше — это наш free(chunk_X)):


136 buffer_get_bignum2(Buffer *buffer, BIGNUM *value) 137 { 138 u_int len; 139 u_char *bin = buffer_get_string(buffer, &len); ... 143 BN_bin2bn(bin, len, value); 144 xfree(bin); 145 }

Однако поначалу нам никогда не удавалось выиграть это состояние гонки (т.е. прервать вызов free() в строке 144 в нужный момент). В конце концов мы поняли, что можем значительно улучшить наши шансы на победу в этой гонке: код разбора открытого ключа DSA позволяет нам вызвать free() четыре раза (в строках 704-707 ниже), и, более того, sshd позволяет нам попробовать шесть аутентификаций пользователя (AUTH_FAIL_MAX); если любой из этих 24 вызовов free() будет прерван в нужный момент, то затем мы добьёмся удалённого выполнения кода внутри обработчика SIGALRM.


678 key_from_blob(u_char *blob, int blen) 679 { ... 693 switch (type) { ... 702 case KEY_DSA: 703 key = key_new(type); 704 buffer_get_bignum2(&b, key->dsa->p); 705 buffer_get_bignum2(&b, key->dsa->q); 706 buffer_get_bignum2(&b, key->dsa->g); 707 buffer_get_bignum2(&b, key->dsa->pub_key);

С этим улучшением мы наконец выиграли состояние гонки примерно через ~1 месяц: мы были счастливы (и станцевали танец root-оболочки), но также почувствовали, что ещё есть возможности для улучшения.


Время

root@kitploit:~
Не волнуйся, просто подожди и увидишь
    -- The Interrupters, "Haven't Seen the Last of Me"

Поэтому мы реализовали следующую трёхчастную стратегию времени:

  • Мы не ждём последнего момента, чтобы отправить наш (довольно большой) пакет с открытым ключом DSA в sshd: вместо этого мы отправляем весь пакет минус один байт (последний байт) задолго до LoginGraceTime и отправляем самый последний байт в самый последний момент, чтобы минимизировать влияние сетевых задержек. (И мы отключаем алгоритм Нейгла.)

  • Мы отслеживаем медианное время кругового пути (регулярно отправляя пакеты, которые вызывают ответ от sshd), и отслеживаем разницу между моментом, когда мы ожидаем, что наше соединение будет закрыто sshd (по сути, момент получения первого байта баннера sshd плюс LoginGraceTime), и моментом, когда наше соединение действительно закрыто sshd, и соответственно корректируем наше время (т.е. момент отправки последнего байта нашего пакета DSA).

    Эти разницы во времени позволяют нам отслеживать расхождения часов и сетевые задержки, которые со временем демонстрируют предсказуемые закономерности: мы экспериментировали с линейной и сплайн-регрессией, но в итоге ничто не сработало лучше, чем простое повторное использование самого последнего измерения. Возможно, глубокое обучение может дать ещё лучшие результаты; это оставлено в качестве упражнения для заинтересованного читателя.

  • Что более важно, мы дополнительно увеличиваем наши шансы на победу в этом состоянии гонки, медленно корректируя наше время через непроизвольную обратную связь от sshd:

    • если мы получаем ответ (SSH2_MSG_USERAUTH_FAILURE) на наш пакет с открытым ключом DSA, значит, мы отправили его слишком рано (sshd успел получить наш пакет в непривилегированном дочернем процессе, разобрать его, отправить привилегированному дочернему процессу, разобрать там и отправить ответ обратно нам);

    • если мы не можем даже отправить последний байт нашего пакета DSA, значит, мы ждали слишком долго (sshd уже получил SIGALRM и закрыл наше соединение);

    • если мы можем отправить последний байт нашего пакета DSA и не получаем ответа до того, как sshd закроет наше соединение, значит, наше время было достаточно точным.

    Эта обратная связь позволяет нам нацелиться на то, что мы называем "большим" окном гонки: попадание в него не гарантирует, что мы выиграем состояние гонки, но внутри этого большого окна находятся 24 "маленьких" окна гонки (внутри 24 вызовов free()), которые, если в них попасть, гарантируют, что мы выиграем состояние гонки.

С этими улучшениями в среднем требуется ~10 000 попыток, чтобы выиграть это состояние гонки; т.е. при 10 соединениях (MaxStartups), принятых за 600 секунд (LoginGraceTime), в среднем требуется ~1 неделя, чтобы получить удалённую root-оболочку.

======================================================================== SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, от 2006)


Теория, первая попытка

root@kitploit:~
Я сплю, когда начинает восходить солнце
    -- The Interrupters, "Alien"

Обработчик SIGALRM этой версии OpenSSH больше не вызывает packet_close(); более того, glibc этой Ubuntu (2.3.6) всегда захватывает обязательную блокировку при входе в функции семейства malloc (даже если однопоточная, как sshd), что мешает нам прервать вызов одной из функций malloc и позже эксплуатировать его во время другого вызова этих функций (они всегда будут взаимоблокироваться). Мы должны найти другое решение.

CVE-2006-5051 упоминает double-free в GSSAPI, но GSSAPI (или Kerberos) не включены по умолчанию, так что это не выглядит очень привлекательным. С другой стороны, PAM включен по умолчанию, и pam_end() вызывается обработчиком SIGALRM в sshd (и, конечно, не является async-signal-safe). Поэтому мы искали функцию PAM, которая, если её прервать сигналом SIGALRM в нужный момент, оставит внутренние структуры PAM в нестабильном состоянии, пригодном для эксплуатации во время pam_end() в обработчике SIGALRM. Мы нашли pam_set_data():


33 int pam_set_data( 34 pam_handle_t *pamh, .. 37 void (*cleanup)(pam_handle_t *pamh, void *data, int error_status)) 38 { 39 struct pam_data *data_entry; .. 57 } else if ((data_entry = malloc(sizeof(*data_entry)))) { .. 65 data_entry->next = pamh->data; 66 pamh->data = data_entry; .. 74 data_entry->cleanup = cleanup; ------------------------------------------------------------------------Если эта функция будет прервана сигналом SIGALRM после строки 66, но до строки 74, то data_entry уже будет связан со структурами PAM (pamh), но его поле cleanup (указатель на функцию) ещё не будет инициализировано (так как malloc() в строке 57 не инициализирует свою память). Если мы сможем контролировать cleanup (через остатки от предыдущих выделений в куче), то сможем выполнить произвольный код, когда pam_end() (внутри обработчика SIGALRM) вызовет _pam_free_data() (в строке 118):


104 void _pam_free_data(pam_handle_t *pamh, int status) 105 { 106 struct pam_data *last; 107 struct pam_data *data; ... 112 data = pamh->data; 113 114 while (data) { 115 last = data; 116 data = data->next; 117 if (last->cleanup) { 118 last->cleanup(pamh, last->data, status);

Это был бы чрезвычайно простой эксплойт; к сожалению, мы совершенно упустили, что pam_set_data() может быть вызвана только из модулей PAM: если мы прерываем её сигналом SIGALRM, то pamh->caller_is по-прежнему остаётся _PAM_CALLED_FROM_MODULE, и в этом случае pam_end() возвращается немедленно, даже не вызывая _pam_free_data(). Возвращаемся к черновику.


Теория, вторая попытка

root@kitploit:~
Не сдаёмся — это не в наших правилах
    -- The Interrupters, "Title Holder"

Мы заметили, что в строке 601 ниже sshd передаёт указатель на свой глобальный указатель sshpam_handle напрямую в pam_start() (которая вызывается один раз на каждое соединение):


202 static pam_handle_t *sshpam_handle = NULL;

584 sshpam_init(Authctxt *authctxt) 585 { ... 600 sshpam_err = 601 pam_start(SSHD_PAM_SERVICE, user, &store_conv, &sshpam_handle);

Поэтому мы решили исследовать саму pam_start(): если её прервать сигналом SIGALRM, она может оставить структуру, на которую указывает sshpam_handle, в несогласованном состоянии, которое затем можно использовать внутри обработчика SIGALRM, когда вызывается pam_end(sshpam_handle, sshpam_err).


18 int pam_start ( .. 22 pam_handle_t **pamh) 23 { .. 32 if ((*pamh = calloc(1, sizeof(**pamh))) == NULL) { ... 110 if ( _pam_init_handlers(*pamh) != PAM_SUCCESS ) {

319 int _pam_init_handlers(pam_handle_t *pamh) 320 { ... 398 retval = _pam_parse_conf_file(pamh, f, pamh->service_name, PAM_T_ANY

66 static int _pam_parse_conf_file(pam_handle_t *pamh, FILE *f .. 73 { ... 252 res = _pam_add_handler(pamh, must_fail, other

581 int _pam_add_handler(pam_handle_t *pamh ... 585 { ... 755 the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf; ... 767 handler_p = &the_handlers->authenticate; ... 874 if ((*handler_p = malloc(sizeof(struct handler))) == NULL) { ... 886 (*handler_p)->next = NULL;

В строке 32 pam_start() немедленно устанавливает sshpam_handle в sshd на блок памяти, выделенный через calloc(); это безопасно, потому что calloc() обнуляет эту память. С другой стороны, если _pam_add_handler() (которая вызывается несколько раз из pam_start()) будет прервана сигналом SIGALRM после строки 874, но до строки 886, то структура, выделенная через malloc(), будет связана с pamh, но её поле next ещё не будет инициализировано. Если мы сможем управлять next (через остатки от предыдущих выделений в куче), то сможем передать произвольный указатель в free() во время вызова pam_end() (внутри обработчика SIGALRM) в строках 1020 (и 1017) ниже:


11 int pam_end(pam_handle_t *pamh, int pam_status) 12 { .. 31 if ((ret = _pam_free_handlers(pamh)) != PAM_SUCCESS) {

925 int _pam_free_handlers(pam_handle_t *pamh) 926 { ... 954 _pam_free_handlers_aux(&(pamh->handlers.conf.authenticate));

1009 void _pam_free_handlers_aux(struct handler **hp) 1010 { 1011 struct handler *h = *hp; 1012 struct handler last; .... 1015 while (h) { 1016 last = h; 1017 _pam_drop(h->argv); / Всё это выделено в одном блоке */ 1018 h = h->next; 1019 memset(last, 0, sizeof(*last)); 1020 free(last); 1021 }

Поскольку malloc в glibc этой Ubuntu уже защищён от старой техники unlink(), мы решили преобразовать наш произвольный free() в "House of Mind" из Malloc Maleficarum (версия для быстрого бина): мы освобождаем свой собственный блок NON_MAIN_ARENA, указываем нашу поддельную арену на .got.plt от sshd (эта Ubuntu имеет ASLR, но не PIE для sshd) и перезаписываем запись _exit() адресом нашего шеллкода в куче (куча в этой Ubuntu по-прежнему исполняема по умолчанию). Подробнее о Malloc Maleficarum:

https://seclists.org/bugtraq/2005/Oct/118


Практика

root@kitploit:~
Я всё познавал трудным путём
    -- The Interrupters, "The Hard Way"

Чтобы провести эту атаку против sshd, изначально мы столкнулись с тремя проблемами:

  • «House of Mind» требует, чтобы мы сохранили указатель на нашу поддельную арену по адресу 0x08100000 в куче; но сможем ли мы сохранить подконтрольные злоумышленнику данные по такому высокому адресу? Поскольку sshd вызывает pam_start() в самом начале аутентификации пользователя, мы не контролируем ничего, кроме самого имени пользователя; к счастью, имя пользователя длиной ~128 КБ (меньше DEFAULT_MMAP_THRESHOLD) позволяет нам сохранить свои данные по адресу 0x08100000.

  • Поле размера нашего поддельного блока NON_MAIN_ARENA не должно быть слишком большим (чтобы пройти проверки безопасности free()); то есть оно должно содержать нулевые байты. Но наше длинное имя пользователя — это строка, завершающаяся нулём, которая не может содержать нулевые байты; к счастью, мы вспомнили, что _pam_free_handlers_aux() обнуляет структуры, которые освобождает (строка 1019 выше): поэтому мы «патчим» поле размера нашего поддельного блока с помощью такого memset(0), и только потом освобождаем его.

  • Нам нужно пережить несколько вызовов free() (в строках 1017 и 1020 выше) до того, как произойдёт нашего поддельного блока . Мы превращаем эти в операции бездействия, направляя их на поддельные блоки : вызывает , который вызывает , который завершается неудачей, потому что эти поддельные блоки неправильно выровнены; по сути — бездействие, поскольку ошибки не приводят к принудительному завершению в glibc этой Ubuntu.

Наконец, наше длинное имя пользователя также позволяет нам контролировать потенциально неинициализированное поле next 20 различных структур (через остатки от временных копий нашего длинного имени пользователя), потому что pam_start() вызывает _pam_add_handler() несколько раз; то есть наше большое окно гонки содержит 20 маленьких окон гонки.


Время

root@kitploit:~
Те же трюки, что и раньше
    -- The Interrupters, "Divide Us"

Для этой атаки против Ubuntu 6.06.1 мы просто повторно использовали стратегию времени, которую применяли против Debian 3.0r6: в среднем требуется ~10 000 попыток, чтобы выиграть состояние гонки, и при 10 соединениях (MaxStartups), принимаемых за 120 секунд (LoginGraceTime), получение удалённой root-оболочки занимает в среднем ~1-2 дня.

Примечание: поскольку glibc этой Ubuntu всегда захватывает обязательную блокировку при входе в функции семейства malloc, невезучий злоумышленник может заблокировать все 10 соединений MaxStartups до получения root-оболочки; мы не пытались обойти эту проблему, потому что нашей конечной целью всё равно была эксплуатация современной версии OpenSSH.

======================================================================== SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, от 2024)


Теория

root@kitploit:~
Теперь ты готов, дай отпор демонам
    -- The Interrupters, "Be Gone"

Обработчик SIGALRM в этой версии OpenSSH не вызывает ни packet_close(), ни pam_end(); на самом деле он вызывает только одну интересную функцию: syslog():


358 grace_alarm_handler(int sig) 359 { ... 370 sigdie("Timeout before authentication for %s port %d", 371 ssh_remote_ipaddr(the_active_state), 372 ssh_remote_port(the_active_state));

96 #define sigdie(...) sshsigdie(FILE, func, LINE, 0, SYSLOG_LEVEL_ERROR, NULL, VA_ARGS)

451 sshsigdie(const char *file, const char *func, int line, int showfunc, 452 LogLevel level, const char *suffix, const char *fmt, ...) 453 { ... 457 sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL, 458 suffix, fmt, args);

464 sshlogv(const char *file, const char *func, int line, int showfunc, 465 LogLevel level, const char *suffix, const char *fmt, va_list args) 466 { ... 489 do_log(level, forced, suffix, fmt2, args);

337 do_log(LogLevel level, int force, const char *suffix, const char *fmt, 338 va_list args) 339 { ... 419 syslog(pri, "%.500s", fmtbuf);

Тогда наши два ключевых вопроса: вызывает ли syslog() из glibc этого Debian (2.36) функции, небезопасные для асинхронных сигналов, такие как malloc() и free()? И если да, то по-прежнему ли эта glibc захватывает обязательную блокировку при входе в функции семейства malloc?

  • К счастью для нас, атакующих, ответ на первый вопрос — да; если, и только если, syslog() внутри обработчика SIGALRM является самым первым вызовом syslog(), то __localtime64_r() (которая вызывается из syslog()) вызывает malloc(304) для выделения структуры FILE (в строке 166) и вызывает malloc(4096) для выделения внутреннего буфера чтения (в строке 186):

28 __localtime64_r (const __time64_t *t, struct tm *tp) 29 { 30 return __tz_convert (*t, 1, tp);

567 __tz_convert (__time64_t timer, int use_localtime, struct tm *tp) 568 { ... 577 tzset_internal (tp == &_tmbuf && use_localtime);

367 tzset_internal (int always) 368 { ... 405 __tzfile_read (tz, 0, NULL);

105 __tzfile_read (const char *file, size_t extra, char **extrap) 106 { ... 109 FILE *f; ... 166 f = fopen (file, "rce"); ... 186 if (__builtin_expect (__fread_unlocked ((void *) &tzhead, sizeof (tzhead), 187 1, f) != 1, 0)

Примечание: поскольку мы ничего не контролируем в этих выделениях malloc() (ни их порядок, ни их размеры, ни их содержимое), мы восприняли "rce" в строке 166 как крайне необходимое доброе предзнаменование.

  • И, к счастью для нас, ответ на второй вопрос — нет; начиная с октября 2017 года функции malloc в glibc больше не захватывают никакую блокировку, если программа однопоточная (как sshd):

    https://sourceware.org/git?p=glibc.git;a=commit;h=a15d53e2de4c7d83bda251469d92a3c7b49a90db https://sourceware.org/git?p=glibc.git;a=commit;h=3f6bb8a32e5f5efd78ac08c41e623651cc242a89 https://sourceware.org/git?p=glibc.git;a=commit;h=905a7725e9157ea522d8ab97b4c8b96aeb23df54

Более того, эта версия Debian страдает от уязвимости ASLR, описанной в следующих отличных постах в блогах (соответственно, Джастина Миллера и Матиаса Краузе):

https://zolutal.github.io/aslrnt/ https://grsecurity.net/toolchain_necromancy_past_mistakes_haunting_aslr

Конкретно, в случае с sshd на i386 каждое отображение памяти рандомизируется нормально (PIE sshd, куча, большинство библиотек, стек), но сама glibc всегда отображается либо по адресу 0xb7200000, либо по адресу 0xb7400000; иными словами, мы можем правильно угадать адрес glibc в половине случаев (небольшая цена за обход ASLR). В нашем эксплойте мы предполагаем, что glibc отображена по адресу 0xb7400000, потому что это немного более распространённый вариант, чем 0xb7200000.

Наш следующий вопрос: какие пути кода внутри функций malloc в glibc, если прервать их сигналом SIGALRM в нужный момент, оставляют кучу в несогласованном состоянии, которое можно использовать во время одного из вызовов malloc() внутри обработчика SIGALRM?

Мы нашли несколько интересных (и удивительных!) путей кода, но выбранный нами путь использует только относительные размеры, а не абсолютные адреса (в отличие, например, от различных путей кода внутри unlink_chunk()); это различие может оказаться решающим для будущего эксплойта под amd64. Этот путь кода внутри malloc() разделяет большой свободный блок (жертву) на два меньших блока; первый блок возвращается вызывающему malloc() (в строке 4345), а второй блок (остаток) связывается в несортированный список свободных блоков (в строках 4324-4327):


1449 #define set_head(p, s) ((p)->mchunk_size = (s))

3765 _int_malloc (mstate av, size_t bytes) 3766 { .... 3798 nb = checked_request2size (bytes); .... 4295 size = chunksize (victim); .... 4300 remainder_size = size - nb; .... 4316 remainder = chunk_at_offset (victim, nb); .... 4320 bck = unsorted_chunks (av); 4321 fwd = bck->fd; .... 4324 remainder->bk = bck; 4325 remainder->fd = fwd; 4326 bck->fd = remainder; 4327 fwd->bk = remainder; .... 4337 set_head (victim, nb | PREV_INUSE | 4338 (av != &main_arena ? NON_MAIN_ARENA : 0)); 4339 set_head (remainder, remainder_size | PREV_INUSE); .... 4343 void *p = chunk2mem (victim); .... 4345 return p;

  • Если этот путь кода будет прерван сигналом SIGALRM после строки 4327, но до строки 4339, то блок-остаток этого разделения уже будет связан в несортированный список свободных блоков (строки 4324-4327), но его поле размера (mchunk_size) ещё не будет инициализировано (строка 4339).

  • Если мы сможем контролировать его поле размера (через остатки от предыдущих выделений в куче), то сможем сделать этот блок-остаток больше, чтобы он перекрывался с другими блоками кучи, и, следовательно, повредить память кучи, когда этот увеличенный, перекрывающийся блок-остаток в итоге будет выделен через malloc() и в него будут производиться записи (внутри обработчика SIGALRM).

Тогда наш последний вопрос: учитывая, что мы ничего не контролируем в вызовах malloc() внутри обработчика SIGALRM, что мы можем перезаписать в куче, чтобы добиться выполнения произвольного кода до того, как sshd вызовет _exit() (в sshsigdie())?

Поскольку __tzfile_read() (внутри обработчика SIGALRM) выделяет структуру FILE в куче через malloc() (в строке 166 выше), и поскольку структуры FILE имеют долгую историю злоупотреблений для выполнения произвольного кода, мы решили направить наше повреждение кучи именно на эту структуру FILE. Однако это легче сказать, чем сделать: наше повреждение кучи очень ограничено, а структуры FILE за эти годы были значительно укреплены (например, с помощью IO_validate_vtable() и PTR_DEMANGLE()).

В итоге мы разработали следующую технику (которая, по-видимому, специфична для glibc на i386 — в glibc на amd64 _vtable_offset вообще не используется):

  • с помощью нашего ограниченного повреждения кучи мы перезаписываем поле _vtable_offset (один знаковый байт) структуры FILE из __tzfile_read();

  • функции libio в glibc будут поэтому искать указатель на vtable этой структуры FILE (указатель на массив указателей на функции) по ненулевому смещению (наше перезаписанное значение _vtable_offset) вместо стандартного нулевого смещения;

  • мы (атакующие) можем легко контролировать этот поддельный указатель на vtable (через остатки от предыдущих выделений в куче), потому что поле структуры FILE вокруг этого смещения не инициализируется явно вызовом fopen();

  • чтобы пройти проверки безопасности glibc, наш поддельный указатель на vtable должен указывать куда-то в секцию __libc_IO_vtables; мы решили указать его на vtable для широко-символьных потоков, _IO_wfile_jumps (то есть на 0xb761b740, поскольку мы предполагаем, что glibc отображена по адресу 0xb7400000);

Вкратце: перезаписывая один байт (_vtable_offset) структуры FILE, выделенной через malloc() вызовом fopen(), мы можем вызвать свой собственный указатель на функцию __fct и выполнить произвольный код во время __fread_unlocked().


Практика

root@kitploit:~
Я хотел, чтобы было идеально, без изъянов
    -- The Interrupters, "In the Mirror"

Чтобы провести эту атаку против привилегированного дочернего процесса sshd, давайте сначала представим следующую компоновку кучи («XXX» — это «барьерные» блоки, которые позволяют нам делать дырки в куче; например, небольшие утечки памяти):

---|----------------------------------------------|---|------------|---

XXXбольшая дыраXXXмалая дыраXXX
~8KB320Б
  • незадолго до того, как sshd получит SIGALRM, мы выделяем через malloc() блок размером ~4KB, который разделяет большую дыру ~8KB на два меньших блока:

---|-----------------------|----------------------|---|------------|--- XXX| большой выделенный | свободный блок- |XXX| малая дыра |XXX | блок | остаток | | | ---|-----------------------|----------------------|---|------------|--- | ~4KB | ~4KB | | 320Б |

  • но если этот malloc() будет прерван сигналом SIGALRM после строки 4327, но до строки 4339, то блок-остаток этого разделения уже будет связан в несортированный список свободных блоков, но его поле размера будет под нашим контролем (через остатки от предыдущих выделений в куче), и этот искусственно увеличенный блок-остаток будет перекрываться со следующей малой дырой:---|-----------------------|----------------------|---|------------|--- XXX| большой выделенный кусок | реальный остаточный кусок |XXX| маленькая дырка |XXX ---|-----------------------|----------------------|---|------------|--- | ~4KB |<------------------------------------->| искусственно увеличенный остаточный кусок

  • когда обработчик SIGALRM вызывает syslog() и, следовательно, __tzfile_read(), fopen() malloc()атит маленькую дырку для своей структуры FILE, а __fread_unlocked() malloc()атит 4KB буфер чтения, тем самым разделяя увеличенный остаточный кусок на два (4KB буфер чтения и маленький остаточный кусок):

---|-----------------------|----------------------|---|------------|--- XXX| большой выделенный кусок | |XXX| FILE |XXX ---|-----------------------|----------------------|---|--|---------|--- | ~4KB |<--------------------------->|<------->| 4KB буфер чтения остаток

  • таким образом мы перезаписываем части структуры FILE внутренним заголовком этого маленького остаточного куска: точнее, мы перезаписываем _vtable_offset структуры FILE третьим байтом поля bk этого заголовка, который является указателем на несортированный список свободных кусков, 0xb761d7f8 (т.е. мы перезаписываем _vtable_offset значением 0x61);

  • затем, как объяснено в подразделе "Теория", __fread_unlocked() вызывает _IO_wfile_underflow() (вместо _IO_file_underflow()), который вызывает наш собственный указатель функции __fct (через наш собственный указатель _codecvt) и выполняет наш произвольный код.

    Примечание: мы ещё не объяснили, как надёжно перейти от контролируемого указателя _codecvt к контролируемому указателю функции __fct; мы сделаем это, но сначала должны решить более насущную проблему.

Действительно, мы узнали из нашей работы над старыми версиями OpenSSH, что мы никогда не выиграем эту гонку в обработчике сигнала, если наше большое окно гонки содержит только одно маленькое окно гонки. Следовательно, мы реализовали следующую стратегию, основанную на следующей раскладке кучи:

---|------------|---|------------|---|------------|---|------------|---

XXXбольшая дырка 1XXXмаленькая дырка 1XXXбольшая дырка 2XXXмаленькая дырка 2...

Последний пакет, который мы отправляем sshd (незадолго до доставки SIGALRM), заставляет sshd выполнить следующую последовательность вызовов malloc(): malloc(~4KB), malloc(304), malloc(~4KB), malloc(304) и т.д.

1/ Наш первый malloc(~4KB) разделяет большую дырку 1 на две:

  • если это первое разделение прерывается SIGALRM в нужный момент, то fopen() внутри обработчика SIGALRM malloc()атит маленькую дырку 1 для своей структуры FILE, и мы достигаем произвольного выполнения кода, как объяснено выше;

  • если нет, то мы сами malloc()атим маленькую дырку 1 нашим первым malloc(304), и:

2/ Наш второй malloc(~4KB) разделяет большую дырку 2 на две:

  • если это второе разделение прерывается SIGALRM в нужный момент, то fopen() внутри обработчика SIGALRM malloc()атит маленькую дырку 2 для своей структуры FILE, и мы достигаем произвольного выполнения кода, как объяснено выше;

  • если нет, то мы сами malloc()атим маленькую дырку 2 нашим вторым malloc(304) и т.д.

Нам удалось создать 27 пар таких больших и маленьких дырок в куче sshd (28 превысило бы PACKET_MAX_SIZE, 256KB): наше большое окно гонки теперь содержит 27 маленьких окон гонки! Достижение такой сложной раскладки кучи было чрезвычайно болезненным и затратным по времени, но два основных момента:

  • Мы злоупотребляем кодом sshd для разбора открытых ключей, чтобы выполнять произвольные последовательности вызовов malloc() и free() (в строках 1805 и 573):

1754 cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf) 1755 { .... 1797 while (sshbuf_len(principals) > 0) { .... 1805 if ((ret = sshbuf_get_cstring(principals, &principal, .... 1820 key->cert->principals[key->cert->nprincipals++] = principal; 1821 }

562 cert_free(struct sshkey_cert *cert) 563 { ... 572 for (i = 0; i < cert->nprincipals; i++) 573 free(cert->principals[i]);

  • Нам не удалось найти утечку памяти для наших маленьких "барьерных" кусков; вместо этого мы используем tcache-куски (которые никогда по-настоящему не освобождаются, потому что их бит inuse никогда не сбрасывается) как импровизированные "барьерные" куски.

Чтобы надёжно достичь этой раскладки кучи, мы отправляем sshd пять различных пакетов открытых ключей (пакеты a/ - d/ можно отправить задолго до SIGALRM; большую часть пакета e/ также можно отправить задолго до SIGALRM, но его самый последний байт должен быть отправлен в самый последний момент):

a/ Мы malloc()атим и free()им множество tcache-кусков, чтобы гарантировать, что выделения кучи, которые мы не контролируем, попадут в эти tcache-куски и не помешают нашей тщательной раскладке кучи.

b/ Мы malloc()атим и free()им куски различных размеров, чтобы создать наши 27 пар больших и маленьких дырок (и соответствующие "барьерные" куски).

c/ Мы malloc()атим и free()им куски размером ~4KB и 320B, чтобы:

  • записать фальшивый заголовок (большое поле размера) нашего потенциально увеличенного остаточного куска в середину наших больших дырок;

  • записать фальшивый нижний колонтитул нашего потенциально увеличенного остаточного куска в конец наших маленьких дырок (чтобы пройти проверки безопасности glibc);

  • записать наши фальшивые указатели vtable и _codecvt в наши маленькие дырки (которые являются потенциальными структурами FILE).

d/ Мы malloc()атим и free()им одну очень большую строку (почти 256KB), чтобы гарантировать, что наши большие и маленькие дырки будут удалены из несортированного списка свободных кусков и помещены в соответствующие бины malloc.

e/ Мы заставляем sshd выполнить нашу финальную последовательность вызовов malloc (malloc(~4KB), malloc(304), malloc(~4KB), malloc(304) и т.д.), чтобы открыть наши 27 маленьких окон гонки.

Внимательные читатели могли заметить, что мы всё ещё не решили (буквально и фигурально) проблему _codecvt. На самом деле, _codecvt - это указатель на структуру (_IO_codecvt), которая содержит указатель на структуру (__gconv_step), которая содержит указатель функции __fct, который позволяет нам выполнять произвольный код. Чтобы надёжно контролировать __fct через _codecvt, мы просто указываем _codecvt на один из бинов malloc glibc, который удобно содержит указатель на один из наших свободных кусков в куче, который содержит наш собственный указатель функции __fct на произвольный код glibc (все эти адреса glibc нам известны, потому что мы предполагаем, что glibc отображён по адресу 0xb7400000).


Время

root@kitploit:~
Время на исходе
    -- The Interrupters, "As We Live"

Когда мы реализовывали этот третий эксплойт, стало ясно, что мы не можем просто повторно использовать стратегию времени, которую мы использовали против двух старых версий OpenSSH: мы никогда не выигрывали это новое состояние гонки. В конце концов мы поняли, почему:

  • Требуется много времени (~10ms) для sshd на разбор нашего пятого и последнего открытого ключа (пакет e/ выше); другими словами, наше большое окно гонки слишком велико (наши 27 маленьких окон гонки - как иголки в стоге сена).

  • user_specific_delay(), которая была введена недавно (OpenSSH 7.8p1), задерживает ответ sshd на наш последний пакет открытого ключа до ~9ms и, следовательно, разрушает нашу стратегию времени, основанную на обратной связи.

В результате мы разработали совершенно другую стратегию времени:

  • время от времени мы отправляем наш последний пакет открытого ключа с небольшой ошибкой, которая приводит к ответу об ошибке (строки 138-142 ниже), прямо перед вызовом sshkey_from_blob(), который разбирает наш открытый ключ;

  • время от времени мы отправляем наш последний пакет открытого ключа с другой небольшой ошибкой, которая приводит к ответу об ошибке (строки 151-155 ниже), сразу после вызова sshkey_from_blob(), который разбирает наш открытый ключ;

  • разница между этими двумя временами ответа - это время, которое требуется sshd на разбор нашего последнего открытого ключа, и это позволяет нам точно рассчитать время передачи наших последних пакетов (чтобы гарантировать, что sshd успеет разобрать наш открытый ключ в непривилегированном дочернем процессе, отправить его привилегированному дочернему процессу и начать разбор там до доставки SIGALRM).


88 userauth_pubkey(struct ssh *ssh, const char method) 89 { ... 138 if (pktype == KEY_UNSPEC) { 139 / это совершенно законно */ 140 verbose_f("неподдерживаемый алгоритм открытого ключа: %s", pkalg); 141 goto done; 142 } 143 if ((r = sshkey_from_blob(pkblob, blen, &key)) != 0) { 144 error_fr(r, "разбор ключа"); 145 goto done; 146 } ... 151 if (key->type != pktype) { 152 error_f("несовпадение типа для декодированного ключа " 153 "(получен %d, ожидался %d)", key->type, pktype); 154 goto done; 155 }

С этим изменением стратегии в среднем требуется ~10 000 попыток, чтобы выиграть гонку; т.е. при 100 соединениях (MaxStartups), принятых за 120 секунд (LoginGraceTime), в среднем требуется ~3-4 часа, чтобы выиграть гонку, и ~6-8 часов, чтобы получить удалённую root-оболочку (из-за ASLR).

======================================================================== На пути к amd64 эксплойту

root@kitploit:~
Какие у тебя планы на завтра?
    -- The Interrupters, "Take Back the Power"

Мы решили нацелиться на Rocky Linux 9 (производный от Red Hat Enterprise Linux 9), из "Rocky-9.4-x86_64-minimal.iso", по двум причинам:

  • его версия OpenSSH (8.7p1) уязвима для этой гонки в обработчике сигнала, а его glibc всегда отображена по адресу, кратному 2MB (из-за слабости ASLR, обсуждавшейся в предыдущем подразделе "Теория"), что делает частичные перезаписи указателей гораздо более мощными;

  • функция syslog() (которая небезопасна для async-signal, но вызывается обработчиком SIGALRM в sshd) в этой версии glibc (2.34) внутренне вызывает __open_memstream(), который malloc()атит структуру FILE в куче, а также вызывает calloc(), realloc() и free() (что даёт нам некоторую столь необходимую свободу).

Имея повреждение кучи в качестве примитива, две структуры FILE, выделенные malloc() в куче, и 21 фиксированный бит в адресах glibc, мы полагаем, что эта гонка в обработчике сигнала эксплуатируема на amd64 (вероятно, не за ~6-8 часов, но, надеюсь, менее чем за неделю). Время покажет.

Побочное замечание: мы обнаружили, что Ubuntu 24.04 не перерандомизирует ASLR своих дочерних процессов sshd (рандомизируется только один раз, при загрузке); мы отследили это до патча ниже, который отключает флаг rexec_flag у sshd. В целом это плохая идея, но в конкретном случае этой гонки в обработчике сигнала это предотвращает эксплуатацию sshd: syslog() внутри обработчика SIGALRM не вызывает никаких функций malloc, потому что это никогда не самый первый вызов syslog().

https://git.launchpad.net/ubuntu/+source/openssh/tree/debian/patches/systemd-socket-activation.patch

======================================================================== Патчи и смягчение последствий

root@kitploit:~
Шторм пришёл и ушёл
    -- The Interrupters, "Good Things"

6 июня 2024 года эта гонка в обработчике сигнала была исправлена коммитом 81c1099 ("Add a facility to sshd(8) to penalise particular problematic client behaviours"), который переместил небезопасный для async-signal код из обработчика SIGALRM в sshd в процесс-слушатель sshd, где его можно обрабатывать синхронно:

https://github.com/openssh/openssh-portable/commit/81c1099d22b81ebfd20a334ce986c4f753b0db29

Поскольку это исправление является частью большого коммита (81c1099), поверх ещё большего коммита защиты в глубину (03e3de4, "Start the process of splitting sshd into separate binaries"), его может быть трудно портировать обратно. В этом случае саму гонку в обработчике сигнала можно исправить, удалив или закомментировав небезопасный для async-signal код из функции sshsigdie(); например:


sshsigdie(const char *file, const char *func, int line, int showfunc, LogLevel level, const char *suffix, const char *fmt, ...) { #if 0 va_list args;

root@kitploit:~
    va_start(args, fmt);
    sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL,
        suffix, fmt, args);
    va_end(args);

#endif _exit(1); }

Наконец, если sshd не может быть обновлён или перекомпилирован, эту гонку в обработчике сигнала можно исправить, просто установив LoginGraceTime в 0 в файле конфигурации. Это делает sshd уязвимым для отказа в обслуживании (истощение всех соединений MaxStartups), но делает его безопасным от удалённого выполнения кода, представленного в этом уведомлении.

======================================================================== Благодарности

Мы благодарим разработчиков OpenSSH за их выдающуюся работу и тесное сотрудничество над этим выпуском. Мы также благодарим distros@openwall. Наконец, мы посвящаем это уведомление Софии д'Антуан.

======================================================================== Хронология

2024-05-19: Мы связались с разработчиками OpenSSH. Последовали итерации патчей и их рецензий.

2024-06-20: Мы связались с distros@openwall.

2024-07-01: Дата скоординированного выпуска.

Скачать инструмент
free()
NON_MAIN_ARENA
free()
IS_MMAPPED
free()
munmap_chunk()
munmap()
IS_MMAPPED
assert()
  • в результате __fread_unlocked() (в строке 186 выше) вызывает _IO_wfile_underflow() (вместо _IO_file_underflow()), которая вызывает указатель на функцию (__fct), который в основном берётся из структуры, указатель на которую (_codecvt) является ещё одним полем структуры FILE;

  • мы (атакующие) можем легко контролировать этот указатель _codecvt (через остатки от предыдущих выделений в куче, поскольку это поле структуры FILE не инициализируется явно вызовом fopen()), что также позволяет нам контролировать указатель на функцию __fct.

  • ~8KB320B~8KB320B