Автор статьи Michael S, Vitaly Nikolenko
В этой статье мы обсудим изменения в реализации slab allocator в ядре Linux и проблемы эксплуатации уязвимостей, связанных с кучей ядра. В этой статье мы сосредоточимся на реализации SLUB (unqueued slab allocator), поскольку это наиболее распространенный allocator включенный по умолчанию в большинстве дистрибутивов Linux и устройств Android.
Мы обсудим некоторые основные изменения в современных ядрах, которые влияют на возможность эксплуатации уязвимостей, связанных с кучей. Некоторые из этих изменений/функций были сделаны намеренно, в то время как другие были побочным эффектом, затрудняющим или облегчающим процесс эксплуатации.
Для справки:
Двумя распространенными механизмами динамического распределения объектов в ядре являются:
Выделение общего назначения, выполняемое с помощью kmalloc/kzalloc/... API
Выделение специального назначения с помощью kmem_cache_create/kmem_cache_alloc
Кэши специального назначения обычно создаются для часто выделяемых/используемых объектов, таких как task_struct, cred, inode, sock и т.д.
Стандартное распределение ядра общего назначения может выглядеть следующим образом:
kmalloc(sizeof(struct some_struct), GFP_KERNEL).
Существуют и другие флаги, кроме GFP_KERNEL, которые могут сделать выделение атомарным, запросить большой объем памяти и т.д., но GFP_KERNEL указывает на обычное выделение ядра и представляет собой наиболее распространенный случай выделения уязвимого объекта в уязвимостях, связанных с UAF / heapovf. В стандартной системе Linux кэши общего назначения SLUB называются kmalloc-* и начинаются с небольших объектных кэшей размером 8 байт и доходят до 8k, с увеличением в два раза, за исключением 96- и 192-байтных кэшей:
Когда запрашивается новое выделение, например, kmalloc(24, GFP_KERNEL), это выделение округляется до следующего ближайшего размера кэша. В данном случае запрошенные 24 байта будут обслуживаться из кэша общего назначения kmalloc-32. Объекты одинакового размера (или при округлении до ближайшего размера кэша общего назначения), выделенные с помощью API k*alloc, выделяются в том же кэше, предполагая, что они обслуживаются одним и тем же ядром (если SMP). Это делает пополнение / распыление кучи тривиальным, если как уязвимые, так и целевые / пополняемые объекты выделяются через k*alloc. Для сравнения, в Android/arm64 наименьший кэш общего назначения - kmalloc-128. С точки зрения эксплуатации это может быть как преимуществом, так и недостатком. Если кэш общего назначения меньше, то и кандидатов на пополнение больше. Например, в Android все распределения k*alloc размером менее или равным 128 байт обслуживаются из кэша kmalloc-128. Это может быть и недостатком. Если все распределения ≤ 128 байт обслуживаются kmalloc-128, кэш становится 'hot' и пополнение становится менее надежным, т.е. возрастает вероятность того, что какое-то другое непреднамеренное распределение займет место освобожденного уязвимого объекта до фактического пополнения.
Наконец, кэши специального назначения распределяются/создаются с помощью API kmem_cache_create/kmem_cache_alloc. Например, создаем специализированный кэш test_cache для объектов размером 86 байт и запрашиваем одно выделение:
На размеры кэша специального назначения нет никаких ограничений - они могут быть произвольными без какого-либо определенного выравнивания. SLAB_HWCACHE_ALIGN указывает, что кэш должен быть выровнен по размеру строки кэша (который составляет 64 байта как в x86_64, так и в aarch64), и почти все кэши специального назначения будут использовать этот флаг. Этот флаг также важен с точки зрения эксплуатации, поскольку он может влиять на слияние кэша с одним из кэшей общего назначения (в зависимости от версии ядра, см. ниже).
Слияние кэшей / возможность слияния
С точки зрения эксплуатации важно определить, распределены ли уязвимый объект и его пополнение (UAF) или уязвимый и целевой объекты (heapovf) в одном и том же кэше/слабе. Как говорилось выше, если уязвимый и целевой объекты выделяются через k*alloc, то выделения обслуживаются из одного и того же кэша общего назначения (с учетом требований к размеру). А если уязвимый или целевой объект выделяется в кэше специального назначения? Например, уязвимый объект A размером 86 байт выделен в кэше специального назначения, а его пополнение размером 128 байт B выделено через k*alloc (очень распространенный сценарий эксплуатации). Для этого конкретного примера успешная эксплуатация зависит от того, сливается или сглаживается кэш A с кэшем общего назначения B. Слияние кэшей SLUB используется для уменьшения фрагментации памяти ядра путем объединения кэшей с похожими характеристиками. Например, когда создается кэш специального назначения без каких-либо специфических флагов, таких как SLAB_ACCOUNT (подробнее об этом ниже), этот кэш может быть объединен с одним из кэшей общего назначения или, возможно, с несколькими другими кэшами специального назначения. Следующая функция определяет возможность слияния кэшей при создании кэша специального назначения:
Выравнивание и расчет размера объектов выполняются в [1] и [2]. Затем цикл в [3] перебирает все доступные кэши, проверяя несколько условий, таких как размер, выравнивание и определенные флаги для определения возможности слияния кэшей. Функция calculate_alignment() показана ниже:
Как упоминалось выше, почти все специализированные кэши выравниваются по размеру кэш-линии (64 байта). Выравнивание, возвращаемое функцией calculate_alignment(), затем используется для вычисления размера объекта кэша. В нашем примере размер test_obj, выровненный по размеру строки кэша, составляет 128 байт. Цикл перебирает все доступные кэши, проверяя несколько условий, таких как размер, выравнивание и определенные флаги для определения возможности слияния кэшей. На ядрах до 4.16 специализированный кэш test_cache будет объединен с кэшем общего назначения kmalloc-128, так как их размеры совпадают после выравнивания и нет специальных флагов, предотвращающих объединение кэшей. С другой стороны, если размер объекта test_cache был 286 байт, то этот кэш не будет выравниваться ни с одним кэшем общего назначения, так как его размер после выравнивания по размеру строки кэша становится 320 байт.
Информация о выравнивании кэшей доступна в /sys/kernel/slab/. Например, следующие кэши объединены вместе:
Все кэши специального назначения выше (aio_kiocb, btree_node и т.д.) объединяются/сглаживаются с помощью kmalloc-128. Существует также инструмент slabinfo, поставляемый с исходным кодом ядра, который может вывести информацию о сглаживании в более приятном формате (т.е. slabinfo -a), разобрав /sys/kernel/slabinfo.
SLAB_ACCOUNT
Этот флаг используется для учета всех объектов определенного кэша kmem. Когда учет kmem включен (или отключен, но скомпилирован), кэши специального назначения, созданные с SLAB_ACCOUNT, не будут сливаться с другими кэшами (специального или общего назначения), созданными без флага SLAB_ACCOUNT. Например, кэш test_cache, созданный с SLAB_ACCOUNT, на этот раз не будет объединен с kmalloc-128:
С точки зрения эксплуатации, если уязвимый объект выделен в кэше, учитываемом kmem, и не содержит указателей функций или других полезных данных для повышения привилегий, эта уязвимость может быть неэксплуатируемой. Поскольку кэш становится автономным, пополнение может быть выполнено только объектом того же типа.
Например, struct cred часто использовался для эскалации привилегий, поскольку кэш специального назначения cred_jar можно было объединить (после выравнивания строк кэша) с kmalloc-192, где sizeof(struct cred) составлял примерно 168-176 байт в зависимости от версии ядра. struct cred обычно использовался в уязвимостях переполнения кучи в 192-байтных кэшах - было тривиально вызвать выделение cred с помощью стандартных системных вызовов set*uid/set*gid, а переполнение счетчика первых ссылок (первые 4 байта) нулями не оказывало существенного влияния на стабильность.
Однако в версиях ядра после 4.4 в кэш cred_jar был добавлен SLAB_ACCOUNT:
Тем не менее, существуют способы пополнения объектов при работе с автономными кэшами. Например, рассмотрим UAF, где уязвимый объект выделен в кэше общего назначения A размером Y, а целевой объект выделен в кэше специального назначения B размером X. Размеры Y и X могут быть практически произвольными. Общая техника заключается в следующем
1. Распределение кэша A (что должно быть тривиально, учитывая, что это кэш общего назначения).
2. Срабатывает триггер освобождения уязвимого объекта, а затем освобождаются все распределенныеые объекты одновременно.
3. Распределение объектов из кэша B.
4. Запустите UAF.
Как только объекты из кэша А начнут освобождаться на шаге 2, освободятся целые слэбы (которые обычно охватывают несколько страниц). Затем мы перераспределяем эти освобожденные слэбы с новыми слэбами принадлежащими кэшу B, на шаге 3 перед запуском UAF. Именно эта техника использовалась в данном LPE, где освобожденные слэбы пополнялись слэбами, принадлежащими кэшу cred_jar, а UAF перезаписывал несколько членов структуры cred нулями. Сама техника, очевидно, менее надежна, поскольку полагается на то, что вся SLUB будет перераспределена в другой кэш, но, учитывая отсутствие ограничений на память, она может достичь высокого уровня успеха.
После 4.16 / усиленное usercopy
Как говорилось выше, до версии 4.16 кэши специального назначения, созданные с помощью kmem_cache_create/kmem_cache_alloc, могли объединяться с кэшами общего назначения, если их размер соответствовал одному из кэшей общего назначения и не использовался kmem.
Усиленное usercopy (CONFIG_HARDENED_USERCOPY) было введено в качестве средства защиты от переполнений кучи/инфоутечек при копировании данных из/в пространство пользователя. Она была введена до версии 4.16, но начиная с версии 4.16 в struct kmem_cache были добавлены два новых компонента useroffset и usersize.
Эти два компонента обеспечивают более тонкий доступ для функций передачи данных из пространства пользователя в пространство ядра, таких как copy_to_user/copy_from_user и т.д. Например, вместо того, чтобы пометить весь объект как доступный пользователю через copy_to_user(), к нему можно получить частичный доступ, чтобы предотвратить утечку информации. Более того, не все кэши должны быть доступны в пространстве пользователя - kmem_cache_create_usercopy была введена специально для создания кэшей, доступных в пространстве пользователя, а оригинальная kmem_cache_create была сохранена для всех остальных кэшей. Подписи обеих функций показаны ниже:
kmem_cache_create_usercopy принимает параметры useroffset и usersize и инициализирует соответствующий struct kmem_cache этими значениями. kmem_cache_create, с другой стороны, устанавливает значения в struct kmem_cache в 0, делая кэш недоступным для пользовательского пространства через copy_to/from_user() и т.д. Одним из побочных эффектов этих изменений API (начиная с версии 4.16) является то, что slab_unmergeable() теперь имеет проверку на usersize в:
Если usersize ненулевой (т.е. кэш доступен в пространстве пользователя), то этот кэш не объединяется ни с каким другим кэшем в системе. Это основной недостаток с точки зрения эксплуатации, поскольку все кэши общего назначения теперь помечаются как доступные в пространстве пользователя в create_boot_cache(), где useroffset устанавливается в 0, а usersize - это весь размер кэша/объекта. В результате, кэши общего назначения больше не могут объединяться с кэшами специального назначения. Это верно, даже если CONFIG_HARDENED_USERCOPY отключен!
Все еще возможно пересечь границу кэша общего/специального назначения при пополнении/распределения, используя технику, рассмотренную в предыдущем разделе (т.е. освобождение целых плит и перераспределение с SLUB, принадлежащими разным кэшам).
После 5.0 / SLAB_ACCOUNT
До выхода ядра 5.0 учет кэша kmem был реализован с помощью отдельных кэшей. Как говорилось выше, установка флага SLAB_ACCOUNT при создании специализированных кэшей делала эти кэши необъединяемыми. Однако в ядрах 5.x были внесены существенные изменения, и это ограничение было снято. Например, следующие два специализированных кэша test_cache1 и test_cache2 теперь объединяются/сглаживаются на ядрах 5.x:
В результате кэши с учетом kmem теперь объединяются с другими специализированными кэшами с установленным флагом SLAB_ACCOUNT или без него, если их размер выравнивания совпадает. Это может быть преимуществом с точки зрения эксплуатации, например, cred_jar теперь объединен с несколькими другими специализированными кэшами в системе.
Рандомизация указателей FREELIST
Рандомизация указателей freelist CONFIG_SLAB_FREELIST_RANDOM была введена в 4.8 и теперь включена по умолчанию в большинстве современных дистрибутивов.
Распространенное заблуждение заключается в том, что рандомизация указателей фрилиста является средством защиты от UAF, поскольку она влияет на порядок выделения объектов. Вторая часть этого утверждения частично верна, т.е. когда выделяется новая область (состоящая из одной или нескольких страниц), выделения из этой области больше не являются последовательными. Это было сделано в первую очередь для борьбы с уязвимостями переполнения кучи ядра, когда уязвимый и целевой объекты должны быть размещены рядом друг с другом в одной и той же плите, прежде чем спровоцировать переполнение. Распространенной техникой формирования кучи было исчерпание кэша путем выделения объектов того же размера, что и целевой кэш (с учетом выравнивания объектов/кэша). Когда все частичные/фрагментированные перекрытия заполнены, выделяются новые перекрытия, и все выделения из этих перекрытий становятся последовательными. Однако, если включена рандомизация указателей freelist, выделения из нового пустого слэба становятся случайными, что препятствует детерминированному размещению уязвимых и целевых объектов на куче.
Когда создается новый кэш, для него выделяется случайный предварительно вычисленный список/последовательность (член массива random_seq int структуры kmem_cache) и перемешивается с помощью алгоритма Фишера-Ятса.
Этот код является общим для реализаций SLAB и SLUB. Для SLUB random_seq преобразуется из массива индексов freelist в массив смещений внутри нового слэба (на основе размера объекта кэша). Когда выделяется новая SLUB, генерируется случайная начальная позиция/индекс в random_seq для первого объекта, а затем всем остальным объектам назначается последовательность выделения, основанная на этой начальной позиции.
где next_freelist_entry() просто возвращает адрес следующего объекта на основе следующего значения индекса (или смещения внутри слэба) в предварительно вычисленном списке и оборачивается, если текущая позиция достигает конца предварительно вычисленного списка:
Например, для kmalloc-8 следующая предварительно вычисленная последовательность индексов с начальным pos = 2 приведет к следующему расположению блоков:
В приведенном выше примере нет способа детерминированно разместить целевой и уязвимый объекты рядом друг с другом , просто выполнив последовательное выделение кучи. Вместо этого распространенной техникой эксплуатации переполнений кучи с включенной рандомизацией указателей freelist является следующая
1. Исчерпать кэш, выделив объекты нужного размера, чтобы заполнить все частичные слэбы, и начать выделение новых слэбов.
2. Начать заполнять новые перекрытия целевыми объектами.
3. Освободить один целевой объект и выделить уязвимый объект.
4. Выполните переполнение и проверьте, какой целевой объект был изменен.
Проверка того, какой целевой объект был переполнен на шаге 4, может быть или не быть возможной в зависимости от самого переполнения, целевого кэша и/или выбранного целевого объекта. Для повышения надежности этой техники, часто в слэбах на шаге 3 делается несколько "дыр" (вместо освобождения только одного целевого объекта) и заполняется несколькими уязвимыми объектами.
Как упоминалось выше, рандомизация указателей freelist не является средством защиты от уязвимостей UAF и не влияет на порядок пополнения - оно всегда выполняется в порядке FILO с рандомизацией указателей freelist или без нее. Это означает, что последнее освобожденное место в слэбе будет выделено первым.
Заключение
Современные ядра внесли изменения, которые препятствуют успешной эксплуатации уязвимостей, связанных с кучей ядра. Некоторые из этих изменений были сделаны намеренно (например, рандомизация указателей freelist), другие были просто побочным эффектом.
Выравнивание кэша - одна из наиболее важных особенностей с точки зрения эксплуатации. В новых ядрах общие кэши kmalloc остаются несмешиваемыми как побочный эффект реализации hardened usercopy (независимо от включения или отключения CONFIG_HARDENED_USERCOPY).
Ядра 5.x, однако, сделали кэши kmem accounted (SLAB_ACCOUNT) сливаемыми с другими кэшами специального назначения, превратив некоторые ранее неэксплуатируемые ошибки повреждения памяти в эксплуатируемые уязвимости.
В этой статье мы обсудим изменения в реализации slab allocator в ядре Linux и проблемы эксплуатации уязвимостей, связанных с кучей ядра. В этой статье мы сосредоточимся на реализации SLUB (unqueued slab allocator), поскольку это наиболее распространенный allocator включенный по умолчанию в большинстве дистрибутивов Linux и устройств Android.
Мы обсудим некоторые основные изменения в современных ядрах, которые влияют на возможность эксплуатации уязвимостей, связанных с кучей. Некоторые из этих изменений/функций были сделаны намеренно, в то время как другие были побочным эффектом, затрудняющим или облегчающим процесс эксплуатации.
Для справки:
Двумя распространенными механизмами динамического распределения объектов в ядре являются:
Выделение общего назначения, выполняемое с помощью kmalloc/kzalloc/... API
Выделение специального назначения с помощью kmem_cache_create/kmem_cache_alloc
Кэши специального назначения обычно создаются для часто выделяемых/используемых объектов, таких как task_struct, cred, inode, sock и т.д.
Стандартное распределение ядра общего назначения может выглядеть следующим образом:
kmalloc(sizeof(struct some_struct), GFP_KERNEL).
Существуют и другие флаги, кроме GFP_KERNEL, которые могут сделать выделение атомарным, запросить большой объем памяти и т.д., но GFP_KERNEL указывает на обычное выделение ядра и представляет собой наиболее распространенный случай выделения уязвимого объекта в уязвимостях, связанных с UAF / heapovf. В стандартной системе Linux кэши общего назначения SLUB называются kmalloc-* и начинаются с небольших объектных кэшей размером 8 байт и доходят до 8k, с увеличением в два раза, за исключением 96- и 192-байтных кэшей:
Код:
# cat /proc/slabinfo | grep ^kmalloc
kmalloc-8192 40 40 8192 4 8 : tunables 0 0 0 : slabdata 10 10 0
kmalloc-4096 127 136 4096 8 8 : tunables 0 0 0 : slabdata 17 17 0
kmalloc-2048 256 256 2048 8 4 : tunables 0 0 0 : slabdata 32 32 0
kmalloc-1024 896 896 1024 8 2 : tunables 0 0 0 : slabdata 112 112 0
kmalloc-512 537 608 512 8 1 : tunables 0 0 0 : slabdata 76 76 0
kmalloc-256 1613 1680 256 16 1 : tunables 0 0 0 : slabdata 105 105 0
kmalloc-192 1525 1596 192 21 1 : tunables 0 0 0 : slabdata 76 76 0
kmalloc-128 1184 1184 128 32 1 : tunables 0 0 0 : slabdata 37 37 0
kmalloc-96 1260 1260 96 42 1 : tunables 0 0 0 : slabdata 30 30 0
kmalloc-64 5760 5760 64 64 1 : tunables 0 0 0 : slabdata 90 90 0
kmalloc-32 3072 3072 32 128 1 : tunables 0 0 0 : slabdata 24 24 0
kmalloc-16 1792 1792 16 256 1 : tunables 0 0 0 : slabdata 7 7 0
kmalloc-8 2048 2048 8 512 1 : tunables 0 0 0 : slabdata 4
Когда запрашивается новое выделение, например, kmalloc(24, GFP_KERNEL), это выделение округляется до следующего ближайшего размера кэша. В данном случае запрошенные 24 байта будут обслуживаться из кэша общего назначения kmalloc-32. Объекты одинакового размера (или при округлении до ближайшего размера кэша общего назначения), выделенные с помощью API k*alloc, выделяются в том же кэше, предполагая, что они обслуживаются одним и тем же ядром (если SMP). Это делает пополнение / распыление кучи тривиальным, если как уязвимые, так и целевые / пополняемые объекты выделяются через k*alloc. Для сравнения, в Android/arm64 наименьший кэш общего назначения - kmalloc-128. С точки зрения эксплуатации это может быть как преимуществом, так и недостатком. Если кэш общего назначения меньше, то и кандидатов на пополнение больше. Например, в Android все распределения k*alloc размером менее или равным 128 байт обслуживаются из кэша kmalloc-128. Это может быть и недостатком. Если все распределения ≤ 128 байт обслуживаются kmalloc-128, кэш становится 'hot' и пополнение становится менее надежным, т.е. возрастает вероятность того, что какое-то другое непреднамеренное распределение займет место освобожденного уязвимого объекта до фактического пополнения.
Наконец, кэши специального назначения распределяются/создаются с помощью API kmem_cache_create/kmem_cache_alloc. Например, создаем специализированный кэш test_cache для объектов размером 86 байт и запрашиваем одно выделение:
Код:
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void *));
...
struct kmem_cache *s = kmem_cache_create("test_cache", 86, 0,
SLAB_HWCACHE_ALIGN, NULL);
void *test_obj = kmem_cache_alloc(s, GFP_KERNEL);
На размеры кэша специального назначения нет никаких ограничений - они могут быть произвольными без какого-либо определенного выравнивания. SLAB_HWCACHE_ALIGN указывает, что кэш должен быть выровнен по размеру строки кэша (который составляет 64 байта как в x86_64, так и в aarch64), и почти все кэши специального назначения будут использовать этот флаг. Этот флаг также важен с точки зрения эксплуатации, поскольку он может влиять на слияние кэша с одним из кэшей общего назначения (в зависимости от версии ядра, см. ниже).
Слияние кэшей / возможность слияния
С точки зрения эксплуатации важно определить, распределены ли уязвимый объект и его пополнение (UAF) или уязвимый и целевой объекты (heapovf) в одном и том же кэше/слабе. Как говорилось выше, если уязвимый и целевой объекты выделяются через k*alloc, то выделения обслуживаются из одного и того же кэша общего назначения (с учетом требований к размеру). А если уязвимый или целевой объект выделяется в кэше специального назначения? Например, уязвимый объект A размером 86 байт выделен в кэше специального назначения, а его пополнение размером 128 байт B выделено через k*alloc (очень распространенный сценарий эксплуатации). Для этого конкретного примера успешная эксплуатация зависит от того, сливается или сглаживается кэш A с кэшем общего назначения B. Слияние кэшей SLUB используется для уменьшения фрагментации памяти ядра путем объединения кэшей с похожими характеристиками. Например, когда создается кэш специального назначения без каких-либо специфических флагов, таких как SLAB_ACCOUNT (подробнее об этом ниже), этот кэш может быть объединен с одним из кэшей общего назначения или, возможно, с несколькими другими кэшами специального назначения. Следующая функция определяет возможность слияния кэшей при создании кэша специального назначения:
Код:
struct kmem_cache *find_mergeable(size_t size, size_t align,
slab_flags_t flags, const char *name, void (*ctor)(void *))
{
struct kmem_cache *s;
if (slab_nomerge)
return NULL;
if (ctor)
return NULL;
size = ALIGN(size, sizeof(void *));
align = calculate_alignment(flags, align, size); [1]
size = ALIGN(size, align); [2]
flags = kmem_cache_flags(size, flags, name, NULL);
if (flags & SLAB_NEVER_MERGE)
return NULL;
list_for_each_entry_reverse(s, &slab_root_caches, root_caches_node) { [3]
if (slab_unmergeable(s))
continue;
if (size > s->size)
continue;
if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME))
continue;
/*
* Check if alignment is compatible.
* Courtesy of Adrian Drzewiecki
*/
if ((s->size & ~(align - 1)) != s->size)
continue;
if (s->size - size >= sizeof(void *))
continue;
if (IS_ENABLED(CONFIG_SLAB) && align &&
(align > s->align || s->align % align))
continue;
return s;
}
return NULL;
}
Выравнивание и расчет размера объектов выполняются в [1] и [2]. Затем цикл в [3] перебирает все доступные кэши, проверяя несколько условий, таких как размер, выравнивание и определенные флаги для определения возможности слияния кэшей. Функция calculate_alignment() показана ниже:
Код:
unsigned long calculate_alignment(slab_flags_t flags,
unsigned long align, unsigned long size)
{
/*
* If the user wants hardware cache aligned objects then follow that
* suggestion if the object is sufficiently large.
*
* The hardware cache alignment cannot override the specified
* alignment though. If that is greater then use it.
*/
if (flags & SLAB_HWCACHE_ALIGN) {
unsigned long ralign = cache_line_size();
while (size <= ralign / 2)
ralign /= 2;
align = max(align, ralign);
}
if (align < ARCH_SLAB_MINALIGN)
align = ARCH_SLAB_MINALIGN;
return ALIGN(align, sizeof(void *));
}
Как упоминалось выше, почти все специализированные кэши выравниваются по размеру кэш-линии (64 байта). Выравнивание, возвращаемое функцией calculate_alignment(), затем используется для вычисления размера объекта кэша. В нашем примере размер test_obj, выровненный по размеру строки кэша, составляет 128 байт. Цикл перебирает все доступные кэши, проверяя несколько условий, таких как размер, выравнивание и определенные флаги для определения возможности слияния кэшей. На ядрах до 4.16 специализированный кэш test_cache будет объединен с кэшем общего назначения kmalloc-128, так как их размеры совпадают после выравнивания и нет специальных флагов, предотвращающих объединение кэшей. С другой стороны, если размер объекта test_cache был 286 байт, то этот кэш не будет выравниваться ни с одним кэшем общего назначения, так как его размер после выравнивания по размеру строки кэша становится 320 байт.
Информация о выравнивании кэшей доступна в /sys/kernel/slab/. Например, следующие кэши объединены вместе:
Код:
# ls -al /sys/kernel/slab | grep ':t-0000128'
lrwxrwxrwx 1 root root 0 May 16 21:30 aio_kiocb -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 btree_node -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 cifs_mpx_ids -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 ecryptfs_key_tfm_cache -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 eventpoll_epi -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 fib6_nodes -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 ip6_mrt_cache -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 ip_mrt_cache -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 kmalloc-128 -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 pid -> :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 scsi_sense_cache -> :t-0000128
drwxr-xr-x 3 root root 0 May 16 21:30 :t-0000128
lrwxrwxrwx 1 root root 0 May 16 21:30 uid_cache -> :t-0000128
Все кэши специального назначения выше (aio_kiocb, btree_node и т.д.) объединяются/сглаживаются с помощью kmalloc-128. Существует также инструмент slabinfo, поставляемый с исходным кодом ядра, который может вывести информацию о сглаживании в более приятном формате (т.е. slabinfo -a), разобрав /sys/kernel/slabinfo.
Код:
# ./slabinfo -a
:at-0000104 <- ext4_prealloc_space buffer_head
:at-0000256 <- dquot jbd2_transaction_s
:t-0000016 <- kmalloc-16 ecryptfs_file_cache
:t-0000024 <- numa_policy scsi_data_buffer
:t-0000032 <- dnotify_struct ecryptfs_dentry_info_cache sd_ext_cdb kmalloc-32
:t-0000040 <- khugepaged_mm_slot Acpi-Namespace ext4_system_zone
:t-0000048 <- fasync_cache shared_policy_node ip_fib_trie ftrace_event_field ksm_mm_slot jbd2_inode
:t-0000056 <- fanotify_event_info Acpi-Parse uhci_urb_priv dm_io ip_fib_alias zswap_entry file_lock_ctx nsproxy
:t-0000064 <- ecryptfs_key_sig_cache dmaengine-unmap-2 io secpath_cache anon_vma_chain task_delay_info tcp_bind_bucket ksm_stable_node kmalloc-64 ksm_rmap_item fanotify_perm_event_info fs_cache ecryptfs_global_auth_tok_cache
:t-0000072 <- eventpoll_pwq Acpi-Operand
:t-0000080 <- Acpi-State fsnotify_mark Acpi-ParseExt
:t-0000088 <- trace_event_file dnotify_mark inotify_inode_mark
:t-0000112 <- dm_rq_target_io flow_cache
:t-0000120 <- kernfs_node_cache cfq_io_cq
:t-0000128 <- uid_cache eventpoll_epi pid ecryptfs_key_tfm_cache ip6_mrt_cache kmalloc-128 scsi_sense_cache btree_node aio_kiocb ip_mrt_cache cifs_mpx_ids fib6_nodes
:t-0000192 <- bio_integrity_payload cred_jar inet_peer_cache dmaengine-unmap-16 kmalloc-192 key_jar
...
SLAB_ACCOUNT
Этот флаг используется для учета всех объектов определенного кэша kmem. Когда учет kmem включен (или отключен, но скомпилирован), кэши специального назначения, созданные с SLAB_ACCOUNT, не будут сливаться с другими кэшами (специального или общего назначения), созданными без флага SLAB_ACCOUNT. Например, кэш test_cache, созданный с SLAB_ACCOUNT, на этот раз не будет объединен с kmalloc-128:
Код:
struct kmem_cache *s = kmem_cache_create("test_cache", 86, 0,
SLAB_HWCACHE_ALIGN|SLAB_ACCOUNT, NULL);
void *test_obj = kmem_cache_alloc(s, GFP_KERNEL);
С точки зрения эксплуатации, если уязвимый объект выделен в кэше, учитываемом kmem, и не содержит указателей функций или других полезных данных для повышения привилегий, эта уязвимость может быть неэксплуатируемой. Поскольку кэш становится автономным, пополнение может быть выполнено только объектом того же типа.
Например, struct cred часто использовался для эскалации привилегий, поскольку кэш специального назначения cred_jar можно было объединить (после выравнивания строк кэша) с kmalloc-192, где sizeof(struct cred) составлял примерно 168-176 байт в зависимости от версии ядра. struct cred обычно использовался в уязвимостях переполнения кучи в 192-байтных кэшах - было тривиально вызвать выделение cred с помощью стандартных системных вызовов set*uid/set*gid, а переполнение счетчика первых ссылок (первые 4 байта) нулями не оказывало существенного влияния на стабильность.
Однако в версиях ядра после 4.4 в кэш cred_jar был добавлен SLAB_ACCOUNT:
Код:
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}
Тем не менее, существуют способы пополнения объектов при работе с автономными кэшами. Например, рассмотрим UAF, где уязвимый объект выделен в кэше общего назначения A размером Y, а целевой объект выделен в кэше специального назначения B размером X. Размеры Y и X могут быть практически произвольными. Общая техника заключается в следующем
1. Распределение кэша A (что должно быть тривиально, учитывая, что это кэш общего назначения).
2. Срабатывает триггер освобождения уязвимого объекта, а затем освобождаются все распределенныеые объекты одновременно.
3. Распределение объектов из кэша B.
4. Запустите UAF.
Как только объекты из кэша А начнут освобождаться на шаге 2, освободятся целые слэбы (которые обычно охватывают несколько страниц). Затем мы перераспределяем эти освобожденные слэбы с новыми слэбами принадлежащими кэшу B, на шаге 3 перед запуском UAF. Именно эта техника использовалась в данном LPE, где освобожденные слэбы пополнялись слэбами, принадлежащими кэшу cred_jar, а UAF перезаписывал несколько членов структуры cred нулями. Сама техника, очевидно, менее надежна, поскольку полагается на то, что вся SLUB будет перераспределена в другой кэш, но, учитывая отсутствие ограничений на память, она может достичь высокого уровня успеха.
После 4.16 / усиленное usercopy
Как говорилось выше, до версии 4.16 кэши специального назначения, созданные с помощью kmem_cache_create/kmem_cache_alloc, могли объединяться с кэшами общего назначения, если их размер соответствовал одному из кэшей общего назначения и не использовался kmem.
Усиленное usercopy (CONFIG_HARDENED_USERCOPY) было введено в качестве средства защиты от переполнений кучи/инфоутечек при копировании данных из/в пространство пользователя. Она была введена до версии 4.16, но начиная с версии 4.16 в struct kmem_cache были добавлены два новых компонента useroffset и usersize.
Эти два компонента обеспечивают более тонкий доступ для функций передачи данных из пространства пользователя в пространство ядра, таких как copy_to_user/copy_from_user и т.д. Например, вместо того, чтобы пометить весь объект как доступный пользователю через copy_to_user(), к нему можно получить частичный доступ, чтобы предотвратить утечку информации. Более того, не все кэши должны быть доступны в пространстве пользователя - kmem_cache_create_usercopy была введена специально для создания кэшей, доступных в пространстве пользователя, а оригинальная kmem_cache_create была сохранена для всех остальных кэшей. Подписи обеих функций показаны ниже:
Код:
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
unsigned int align, slab_flags_t flags,
void (*ctor)(void *));
struct kmem_cache *kmem_cache_create_usercopy(const char *name,
unsigned int size, unsigned int align,
slab_flags_t flags,
unsigned int useroffset, unsigned int usersize,
void (*ctor)(void *));
kmem_cache_create_usercopy принимает параметры useroffset и usersize и инициализирует соответствующий struct kmem_cache этими значениями. kmem_cache_create, с другой стороны, устанавливает значения в struct kmem_cache в 0, делая кэш недоступным для пользовательского пространства через copy_to/from_user() и т.д. Одним из побочных эффектов этих изменений API (начиная с версии 4.16) является то, что slab_unmergeable() теперь имеет проверку на usersize в:
Код:
int slab_unmergeable(struct kmem_cache *s)
{
if (slab_nomerge || (s->flags & SLAB_NEVER_MERGE))
return 1;
if (!is_root_cache(s))
return 1;
if (s->ctor)
return 1;
if (s->usersize)
return 1;
/*
* We may have set a slab to be unmergeable during bootstrap.
*/
if (s->refcount < 0)
return 1;
return 0;
}
Если usersize ненулевой (т.е. кэш доступен в пространстве пользователя), то этот кэш не объединяется ни с каким другим кэшем в системе. Это основной недостаток с точки зрения эксплуатации, поскольку все кэши общего назначения теперь помечаются как доступные в пространстве пользователя в create_boot_cache(), где useroffset устанавливается в 0, а usersize - это весь размер кэша/объекта. В результате, кэши общего назначения больше не могут объединяться с кэшами специального назначения. Это верно, даже если CONFIG_HARDENED_USERCOPY отключен!
Код:
# cat /sys/kernel/slab/kmalloc-128/aliases
0
Все еще возможно пересечь границу кэша общего/специального назначения при пополнении/распределения, используя технику, рассмотренную в предыдущем разделе (т.е. освобождение целых плит и перераспределение с SLUB, принадлежащими разным кэшам).
После 5.0 / SLAB_ACCOUNT
До выхода ядра 5.0 учет кэша kmem был реализован с помощью отдельных кэшей. Как говорилось выше, установка флага SLAB_ACCOUNT при создании специализированных кэшей делала эти кэши необъединяемыми. Однако в ядрах 5.x были внесены существенные изменения, и это ограничение было снято. Например, следующие два специализированных кэша test_cache1 и test_cache2 теперь объединяются/сглаживаются на ядрах 5.x:
Код:
struct kmem_cache *s1 = kmem_cache_create("test_cache1", 286, 0,
SLAB_HWCACHE_ALIGN|SLAB_ACCOUNT, NULL);
struct kmem_cache *s2 = kmem_cache_create("test_cache2", 286, 0,
SLAB_HWCACHE_ALIGN, NULL);
В результате кэши с учетом kmem теперь объединяются с другими специализированными кэшами с установленным флагом SLAB_ACCOUNT или без него, если их размер выравнивания совпадает. Это может быть преимуществом с точки зрения эксплуатации, например, cred_jar теперь объединен с несколькими другими специализированными кэшами в системе.
Рандомизация указателей FREELIST
Рандомизация указателей freelist CONFIG_SLAB_FREELIST_RANDOM была введена в 4.8 и теперь включена по умолчанию в большинстве современных дистрибутивов.
Распространенное заблуждение заключается в том, что рандомизация указателей фрилиста является средством защиты от UAF, поскольку она влияет на порядок выделения объектов. Вторая часть этого утверждения частично верна, т.е. когда выделяется новая область (состоящая из одной или нескольких страниц), выделения из этой области больше не являются последовательными. Это было сделано в первую очередь для борьбы с уязвимостями переполнения кучи ядра, когда уязвимый и целевой объекты должны быть размещены рядом друг с другом в одной и той же плите, прежде чем спровоцировать переполнение. Распространенной техникой формирования кучи было исчерпание кэша путем выделения объектов того же размера, что и целевой кэш (с учетом выравнивания объектов/кэша). Когда все частичные/фрагментированные перекрытия заполнены, выделяются новые перекрытия, и все выделения из этих перекрытий становятся последовательными. Однако, если включена рандомизация указателей freelist, выделения из нового пустого слэба становятся случайными, что препятствует детерминированному размещению уязвимых и целевых объектов на куче.
Когда создается новый кэш, для него выделяется случайный предварительно вычисленный список/последовательность (член массива random_seq int структуры kmem_cache) и перемешивается с помощью алгоритма Фишера-Ятса.
Код:
int cache_random_seq_create(struct kmem_cache *cachep, unsigned int count,
gfp_t gfp)
{
struct rnd_state state;
if (count < 2 || cachep->random_seq)
return 0;
cachep->random_seq = kcalloc(count, sizeof(unsigned int), gfp);
if (!cachep->random_seq)
return -ENOMEM;
/* Get best entropy at this stage of boot */
prandom_seed_state(&state, get_random_long());
freelist_randomize(&state, cachep->random_seq, count);
return 0;
}
Этот код является общим для реализаций SLAB и SLUB. Для SLUB random_seq преобразуется из массива индексов freelist в массив смещений внутри нового слэба (на основе размера объекта кэша). Когда выделяется новая SLUB, генерируется случайная начальная позиция/индекс в random_seq для первого объекта, а затем всем остальным объектам назначается последовательность выделения, основанная на этой начальной позиции.
Код:
static bool shuffle_freelist(struct kmem_cache *s, struct page *page)
{
void *start;
void *cur;
void *next;
unsigned long idx, pos, page_limit, freelist_count;
if (page->objects < 2 || !s->random_seq)
return false;
freelist_count = oo_objects(s->oo);
pos = get_random_int() % freelist_count;
page_limit = page->objects * s->size;
start = fixup_red_left(s, page_address(page));
/* First entry is used as the base of the freelist */
cur = next_freelist_entry(s, page, &pos, start, page_limit,
freelist_count);
page->freelist = cur;
for (idx = 1; idx < page->objects; idx++) {
setup_object(s, page, cur);
next = next_freelist_entry(s, page, &pos, start, page_limit,
freelist_count);
set_freepointer(s, cur, next);
cur = next;
}
setup_object(s, page, cur);
set_freepointer(s, cur, NULL);
return true;
}
где next_freelist_entry() просто возвращает адрес следующего объекта на основе следующего значения индекса (или смещения внутри слэба) в предварительно вычисленном списке и оборачивается, если текущая позиция достигает конца предварительно вычисленного списка:
Код:
static void *next_freelist_entry(struct kmem_cache *s, struct page *page,
unsigned long *pos, void *start,
unsigned long page_limit,
unsigned long freelist_count)
{
unsigned int idx;
...
do {
idx = s->random_seq[*pos];
*pos += 1;
if (*pos >= freelist_count)
*pos = 0;
} while (unlikely(idx >= page_limit));
return (char *)start + idx;
}
Например, для kmalloc-8 следующая предварительно вычисленная последовательность индексов с начальным pos = 2 приведет к следующему расположению блоков:
В приведенном выше примере нет способа детерминированно разместить целевой и уязвимый объекты рядом друг с другом , просто выполнив последовательное выделение кучи. Вместо этого распространенной техникой эксплуатации переполнений кучи с включенной рандомизацией указателей freelist является следующая
1. Исчерпать кэш, выделив объекты нужного размера, чтобы заполнить все частичные слэбы, и начать выделение новых слэбов.
2. Начать заполнять новые перекрытия целевыми объектами.
3. Освободить один целевой объект и выделить уязвимый объект.
4. Выполните переполнение и проверьте, какой целевой объект был изменен.
Проверка того, какой целевой объект был переполнен на шаге 4, может быть или не быть возможной в зависимости от самого переполнения, целевого кэша и/или выбранного целевого объекта. Для повышения надежности этой техники, часто в слэбах на шаге 3 делается несколько "дыр" (вместо освобождения только одного целевого объекта) и заполняется несколькими уязвимыми объектами.
Как упоминалось выше, рандомизация указателей freelist не является средством защиты от уязвимостей UAF и не влияет на порядок пополнения - оно всегда выполняется в порядке FILO с рандомизацией указателей freelist или без нее. Это означает, что последнее освобожденное место в слэбе будет выделено первым.
Заключение
Современные ядра внесли изменения, которые препятствуют успешной эксплуатации уязвимостей, связанных с кучей ядра. Некоторые из этих изменений были сделаны намеренно (например, рандомизация указателей freelist), другие были просто побочным эффектом.
Выравнивание кэша - одна из наиболее важных особенностей с точки зрения эксплуатации. В новых ядрах общие кэши kmalloc остаются несмешиваемыми как побочный эффект реализации hardened usercopy (независимо от включения или отключения CONFIG_HARDENED_USERCOPY).
Ядра 5.x, однако, сделали кэши kmem accounted (SLAB_ACCOUNT) сливаемыми с другими кэшами специального назначения, превратив некоторые ранее неэксплуатируемые ошибки повреждения памяти в эксплуатируемые уязвимости.