
empty_list — эксплойт для p0 issue 1564 (CVE-2018-4243): чтение/запись ядра iOS 11.0 - 11.3.1
empty_list - эксплойт для p0 issue 1564 (CVE-2018-4243) iOS 11.0 - 11.3.1, r/w ядра @i41nbeer
ОШИБКА: getvolattrlist принимает управляемый пользователем аргумент bufferSize через системный вызов fgetattrlist.
При выделении буфера ядра для сериализации списка атрибутов имеется следующий комментарий:
/*
Проблема в том, что код затем некорректно обрабатывает случай, когда предоставленный пользователем размер буфера меньше запрошенного размера заголовка. Если передать ATTR_CMN_RETURNED_ATTRS, мы попадём на следующий код:
/* Return attribute set output if requested. / if (return_valid) { ab.actual.commonattr |= ATTR_CMN_RETURNED_ATTRS; if (pack_invalid) { / Only report the attributes that are valid */ ab.actual.commonattr &= ab.valid.commonattr; ab.actual.volattr &= ab.valid.volattr; } bcopy(&ab.actual, ab.base + sizeof(uint32_t), sizeof (ab.actual)); }
Нет проверки того, что выделенного буфера достаточно хотя бы для этого.
Эксплуатация: Надеюсь опубликовать более развёрнутый отчёт об этой уязвимости; ниже — краткие заметки о том, как работает эксплойт:
Эта ошибка даёт возможность записать 8 нулевых байтов за конец выделения в kalloc.16. Хотя похоже, что вы можете контролировать несколько битов в этих байтах, я не уверен, что это действительно так, поэтому я сосредоточился на эксплуатации так, как если бы это была запись NULL-указателя за конец.
Это довольно ограниченный примитив, поэтому первый шаг — перечислить возможные действия:
В итоге я выбрал первый вариант. Тогда появляются два дополнительных требования:
Я решил целиться в struct ipc_port, у которого счётчик ссылок находится во втором dword, что удовлетворяет первому требованию. Однако он выделяется не в kalloc.16, а в собственной зоне (ipc_ports).
Это означает, что нам нужно выровнять блок зоны kalloc.16 непосредственно перед блоком ipc_ports, затем переполниться из последнего выделения kalloc.16 в блоке kalloc.16 в первое выделение в блоке ipc_ports.
Есть два приёма, которые облегчают задачу:
Реверсирование списка свободных блоков: Выделения из зоны в первую очередь берутся из промежуточных (частично заполненных) страниц. Это значит, что если мы начнём освобождать и выделять объекты k.16 где-то в середине груминга, они не будут переиспользованы, пока текущая промежуточная страница не станет полностью заполненной или пустой.
Это создаёт сложность, потому что списки свободных блоков свежих страниц заполняются полу-случайным образом, так что их выделения идут изнутри наружу:
| 9 8 6 5 2 1 3 4 7 10 | <-- example "randomized" allocation order from a fresh all-free page
Это означает, что наши итоговые промежуточные страницы k.16 и ports будут выглядеть примерно так:
| - - - 5 2 1 3 4 - - | - - - 4 1 2 3 5 - - | kalloc.16 ipc_ports
Если использовать переполнение для повреждения записи в списке свободных блоков, при её выделении произойдёт паника, поэтому нам нужно этого избегать.
Трюк в том, что, контролируя порядок выделения и освобождения, мы можем развернуть списки свободных блоков так, что итоговые промежуточные страницы будут выглядеть скорее так:
| 1 4 - - - - - 5 3 2 | 2 5 - - - - - 4 3 1 | kalloc.16 ipc_ports
На этом этапе гораздо вероятнее, что мы сможем освободить kalloc.16 и перевыделить его для переполнения так, чтобы попасть в первое qword объекта ipc_port.
Безопасные для переполнения выделения: поскольку, скорее всего, будет много кандидатов на переполнение, прежде чем мы попадём в целевую (которая находится в самом конце, непосредственно перед ipc_port), нужно убедиться, что выделенные объекты на странице kalloc.16 безопасно повреждать NULL-указателем.
Для этого я использую дескрипторы ool_port в mach message, поскольку NULL — допустимое значение.
Ход эксплуатации: Мы выполняем груминг, чтобы развернуть списки свободных блоков kalloc.16, и начинаем пытаться переполниться в ipc_port.
Мы знаем приблизительный диапазон имён mach-портов, содержащих порт, который будет повреждён; после каждой попытки переполнения мы проверяем каждый из этих портов, чтобы узнать, был ли порт повреждён. Побочный эффект успешного повреждения — флаг io_active порта становится равным нулю. Мы можем обнаружить это без побочных эффектов с помощью метода MIG mach_port_kobject.
Как только мы находим повреждённый порт, нам нужно вызвать взятие и сброс ссылки на него; и, что более важно, нам нужно, чтобы путь выполнения, который это делает, не проверял флаг io_active. Это сделает за нас mach_port_set_attributes.
Теперь мы превратили запись NULL-указателя за конец kalloc.16 в висячий mach-порт :)
Мы запускаем сборку мусора зоны (zone gc), стремясь получить повторное использование памяти порта как страницы kalloc.4096. Сначала добиваемся её повторного использования в качестве дескриптора ool_ports, где поле ip_context перекрывается с send-right, который мы отправляем себе на канареечный порт. Это позволяет нам узнать приблизительный адрес наших объектов в ядре. Затем мы заменяем ool_desc на pipe-буфер, и, немного повозившись, можем вычислить, где в памяти находится висячий mach-порт.
Мы создаём там поддельный порт задачи ядра (kernel task port) и затем прибираем за собой.
Надёжность: Эксплойт действительно работает, что и было моей целью :) Надёжность — где-то порядка 30%, всё зависит от того, насколько быстро вы можете выполнить начальное переполнение и цикл проверки. Если что-то ещё вмешается и начнёт выделять или освобождать память в kalloc.16, вы увеличиваете вероятность того, что повредите запись в списке свободных блоков или что-то ещё, и произойдёт паника.
Я уверен, что эксплойт можно сделать более надёжным; я довёл его лишь до точки, где продемонстрировал, что эта ошибка эксплуатируема. Если вы хотите взять это за отправную точку и показать, как повысить надёжность, я с удовольствием прочитаю пост в блоге! Думаю, для этого нужно реально отслеживать выделения в kalloc.16 и понимать, каковы сценарии отказа и как их предотвратить.
Процент успеха, похоже, максимален, когда устройство перезагружено и некоторое время простаивает.
Очистка: Если эксплойт сработает, он должен убрать за собой и не вызвать панику устройства. Поддельный порт задачи ядра останется живым.
Используйте функции из kmem.h для чтения и записи памяти ядра. Сохраните там send-right на tfp0, если хотите сохранить доступ к памяти ядра после завершения этого процесса.
Я тестировал на: iPod Touch 6G, iPhone 6S, iPhone SE, iPhone 7, iPhone 8 Должно работать на iOS 11 и до iOS 11.3.1 включительно.