CVE-2021-22555 - это уязвимость записи за пределами кучи в Linux Netfilter, возникшая 15 лет назад, которая достаточно мощная, чтобы обойти все современные меры безопасности и добиться выполнения кода ядра. Она была использована для того, чтобы сломать изоляцию kubernetes под кластера kCTF и выиграть 10000 долларов на благотворительность.
Введение
После BleedingTooth (https://google.github.io/security-research/pocs/linux/bleedingtooth/writeup.html), когда я впервые заглянул в Linux, я также захотел найти уязвимость повышения привилегий. Я начал с изучения старых уязвимостей, таких как CVE-2016-3134 и CVE-2016-4997, которые вдохновили меня на использование grep для memcpy() и memset() в коде Netfilter. Это привело меня к некоторому бажному коду.
Уязвимость
Когда IPT_SO_SET_REPLACE или IP6T_SO_SET_REPLACE вызывается в режиме совместимости, который требует возможности CAP_NET_ADMIN, которая, однако, может быть получена в пространстве имен user + network, структуры должны быть преобразованы из пользовательского в ядро, а также из 32-битного в 64-битный, чтобы они обрабатывались нативными функцииями. Естественно, это неизбежно приведет к ошибкам. Наша уязвимость находится в xt_compat_target_from_user(), где memset() вызывается со смещением target->targetize, которое не учитывается во время выделения, что приводит к записи нескольких байтов за пределы памяти:
Targetsize не контролируется пользователем, но можно выбрать разные цели с разными размерами структуры по имени (например, TCPMSS, TTL или NFQUEUE). Чем больше размер цели, тем больше мы можем варьировать смещение. Тем не менее, целевой размер не должен быть выровнен на 8 байтов, чтобы заполнить pad > 0. Самый большой из возможных, который я нашел, - это NFLOG, для которого мы можем выбрать смещение до 0x4C байтов за пределами границ (на смещение можно повлиять, добавив отступ между struct xt_entry_match и struct xt_entry_target):
Обратите внимание, что место назначения буфера выделяется с помощью GFP_KERNEL_ACCOUNT и также может варьироваться по размеру:
Хотя минимальный размер > 0x100, что означает, что наименьший слаб, в которой может быть размещен этот объект, - kmalloc-512. Другими словами, мы должны найти жертв, которые распределены между kmalloc-512 и kmalloc-8192 для использования.
Эксплуатация
Наш примитив ограничен записью четырех байтов от нуля до 0x4C байтов за пределы памяти. При таком примитиве обычными целями являются:
-Счетчик ссылок
К сожалению, мне не удалось найти подходящих объектов со счетчиком ссылок в первых байтах 0x4C.
-Свободный указатель на список
CVE-2016-6187: Поочередное использование кучи ядра Linux - хороший пример того, как использовать указатель на список свободных мест. Однако это было уже 5 лет назад, а между тем в ядрах включена опция CONFIG_SLAB_FREELIST_HARDENED, которая, помимо прочего, защищает указатели свободных списков.
-Указатель в структуре
Это наиболее многообещающий подход, однако четыре нулевых байта - это слишком много для записи. Например, указатель 0xffff91a49cb7f000 может быть преобразован только в 0xffff91a400000000 или 0x9cb7f000, где оба они, вероятно, будут недопустимыми указателями. С другой стороны, если бы мы использовали примитив для записи в самом начале соседнего блока, мы могли бы записать меньше байтов, например 2 байта, и, например, повернув указатель с 0xffff91a49cb7f000 на 0xffff91a49cb70000.
Играя с некоторыми объектами-жертвами, я заметил, что никогда не смогу надежно разместить их вокруг struct xt_table_info в ядре 5.4. Я понял, что это как-то связано с флагом GFP_KERNEL_ACCOUNT, поскольку другие объекты, выделенные с помощью GFP_KERNEL_ACCOUNT, не имели этой проблемы. Янн Хорн подтвердил, что до версии 5.9 для ведения аккаунтинга использовались отдельные слабы. Следовательно, каждый примитив кучи, который мы используем в цепочке эксплойтов, также должен использовать GFP_KERNEL_ACCOUNT.
Системный вызов msgsnd() - это хорошо известный примитив для распыления кучи (который использует GFP_KERNEL_ACCOUNT) и уже использовался для нескольких общедоступных эксплойтов. Хотя удивительно, что его структура msg_msg никогда не подвергалась злоупотреблениям. В этой статье мы продемонстрируем, как можно злоупотребить этой структурой данных, чтобы получить примитив использования после освобождения, который, в свою очередь, можно использовать для утечки адресов и подделки других объектов. По совпадению, параллельно с моим исследованием в марте 2021 года Александр Попов также исследовал ту же самую структуру в Four Bytes of Power: CVE-2021-26708 в ядре Linux.
Изучение структуры msg_msg
При отправке данных с помощью msgsnd () полезная нагрузка разделяется на несколько сегментов:
где заголовки для структуры msg_msg и структуры msg_msgseg:
Первый член в msg_msg - это указатель mlist.next, который указывает на другое сообщение в очереди (которое отличается от следующего, поскольку это указатель на следующий сегмент). Как вы узнаете дальше, это идеальный кандидат на повреждение.
Получение use-after-free
Сначала мы инициализируем множество очередей сообщений (в нашем случае 4096) с помощью msgget(). Затем мы отправляем одно сообщение размером 4096 (включая заголовок msg_msg) для каждой очереди сообщений с помощью msgsnd(), которую мы будем называть основным сообщением. В конце концов, после большого количества сообщений у нас есть несколько последовательных:
Затем мы отправляем вторичное сообщение размером 1024 для каждой очереди сообщений с помощью msgsnd ():
Наконец, мы создаем несколько дыр (в нашем случае каждые 1024-е) в первичных сообщениях и запускаем уязвимую опцию setsockopt (IPT_SO_SET_REPLACE), которая в лучшем случае разместит объект xt_table_info в одной из дыр:
Мы решили перезаписать два байта соседнего объекта нулями. Предположим, что мы примыкаем к другому первичному сообщению, эти байты, которые мы перезаписываем, являются частью указателя на вторичное сообщение. Поскольку мы выделяем им размер 1024 байта, у нас есть шанс 1 -(1024/65536) перенаправить указатель (единственный случай, когда мы терпим неудачу, - это когда два младших байта указателя уже равны нулю).
Теперь лучший сценарий, на который мы можем надеяться - это тот, что управляемый указатель также указывает на вторичное сообщение, поскольку следствием этого будут два разных первичных сообщения, указывающих на одно и то же вторичное сообщение, и это может привести к использованию после освобождения:
Однако как узнать, какие два основных сообщения указывают на одно и то же вторичное сообщение? Чтобы ответить на этот вопрос, мы помечаем каждое (первичное и вторичное) сообщение индексом очереди сообщений, который находится в диапазоне [0, 4096). Затем, после запуска повреждения, мы перебираем все очереди сообщений, просматриваем все сообщения с помощью msgrcv() с MSG_COPY и проверяем, совпадают ли они. Если тег основного сообщения отличается от вторичного сообщения, это означает, что оно было перенаправлено. В этом случае тег основного сообщения представляет собой индекс очереди поддельных сообщений, т. е. тот, который содержит неправильное вторичное сообщение, а тег неправильного вторичного сообщения представляет индекс реальной очереди сообщений. Зная эти два индекса, достижение use-after-free теперь тривиально - мы извлекаем вторичное сообщение из реальной очереди сообщений с помощью msgrcv() и, как таковое, освобождаем его:
Обратите внимание, что у нас все еще есть ссылка на освобожденное сообщение в очереди поддельных сообщений.
Обход SMAP
Используя сокеты unix (которые можно легко настроить с помощью socketpair()), мы теперь распыляем множество сообщений размером 1024 и имитируем заголовок struct msg_msg. В идеале мы можем вернуть адрес ранее освобожденного сообщения:
Обратите внимание, что mlist.next - 41414141, поскольку мы еще не знаем никаких адресов ядра (когда SMAP включен, мы не можем указать адрес пользователя). Отсутствие адреса ядра имеет решающее значение, поскольку фактически мешает нам снова освободить блок (позже вы узнаете, почему это необходимо). Причина в том, что во время msgrcv() сообщение отключается от очереди сообщений, которая является циклическим списком. К счастью, у нас есть хорошие возможности для утечки информации, так как в msg_msg есть несколько интересных полей. А именно, поле m_ts используется для определения, сколько данных вернуть в пользовательское пространство:
Исходный размер сообщения составляет всего 1024 байта sizeof(struct msg_msg), который теперь мы можем искусственно увеличить до DATALEN_MSG = 4096-sizeof (struct msg_msg). Как следствие, теперь мы сможем прочитать сообщение, превышающее предполагаемый размер, и пропустить заголовок struct msg_msg соседнего сообщения. Как было сказано ранее, очередь сообщений реализована как круговой список, таким образом, mlist.next указывает на основное сообщение.
Зная адрес основного сообщения, мы можем переработать фальшивую структуру msg_msg с этим адресом как следующим (что означает, что это следующий сегмент). Затем может произойти утечка содержимого основного сообщения при чтении байтов, превышающих DATALEN_MSG. Утечка указателя mlist.next из первичного сообщения показывает адрес вторичного сообщения, которое находится рядом с нашей фальшивой структурой msg_msg. Вычитая 1024 из этого адреса, мы наконец получаем адрес фальшивого сообщения.
Получение UAF
Теперь мы можем перестроить фальшивый объект struct msg_msg с просочившимся адресом как mlist.next и mlist.prev (что означает, что он указывает на себя), сделав фальшивое сообщение свободным от очереди фальшивых сообщений.
Обратите внимание, что при распылении с использованием сокетов unix у нас фактически есть объект struct sk_buff, который указывает на поддельное сообщение. Очевидно, это означает, что когда мы освобождаем фальшивое сообщение, у нас все еще остается устаревшая ссылка:
Этот устаревший буфер данных struct sk_buff является лучшим сценарием использования после освобождения, поскольку он не содержит информации заголовка, а это означает, что теперь мы можем использовать его для освобождения любого типа объекта на слэбе. Для сравнения, освобождение объекта struct msg_msg возможно только в том случае, если первые два члена являются записываемыми указателями (необходимыми для разрыва связи с сообщением).
Поиск жертвы
Лучшая жертва атаки - это жертва, в структуре которой есть указатель на функцию. Помните, что жертве также должен быть назначен GFP_KERNEL_ACCOUNT.
В беседе с Янном Хорном он предложил объект struct pipe_buffer, который размещен в kmalloc-1024 (поэтому вторичное сообщение имеет размер 1024 байта). Структуру pipe_buffer можно легко выделить с помощью pipe(), которая имеет alloc_pipe_info() в качестве подпрограммы:
Хотя он не содержит напрямую указателя на функцию, он содержит указатель на struct pipe_buf_operations, который, с другой стороны, имеет указатели на функции:
Обход KASLR/SMEP
Когда кто-то пишет в каналы, заполняется struct pipe_buffer. Что наиболее важно, ops будет указывать на статическую структуру anon_pipe_buf_ops, которая находится в сегменте .data:
Поскольку разница между сегментом .data и сегментом .text всегда одинакова, наличие anon_pipe_buf_ops в основном позволяет нам вычислить базовый адрес ядра.
Мы распыляем множество объектов struct pipe_buffer и восстанавливаем местоположение устаревшего буфера данных sk_buff:
Поскольку у нас все еще есть ссылка из struct sk_buff, мы можем прочитать его буфер данных, пропустить содержимое struct pipe_buffer и раскрыть адрес anon_pipe_buf_ops:
[+] anon_pipe_buf_ops: ffffffffa1e78380
[+] kbase_addr: ffffffffa0e00000
С этой информацией теперь мы можем найти гаджеты JOP/ROP. Обратите внимание, что при чтении из сокета unix мы фактически также освобождаем его буфер:
Повышение привилегий
Мы заменяем устаревшую структуру pipe_buffer фальшивой, где ops указывает на фальшивую структуру pipe_buf_operations. Эта фальшивая структура устанавливается в том же месте, поскольку мы знаем ее адрес, и, очевидно, эта структура должна содержать указатель на вредоносную функцию в качестве релиза.
Заключительный этап эксплойта - закрыть все каналы, чтобы запустить релиз, который, в свою очередь, запустит цепочку JOP. Найти гаджеты JOP сложно, поэтому цель состоит в том, чтобы как можно скорее выполнить разворот стека ядра, чтобы выполнить цепочку ROP ядра.
ROP-цепочка ядра
Мы сохраняем значение RBP на некотором адресе блокнота в ядре, чтобы мы могли позже возобновить выполнение, затем мы вызываем commit_creds(prepare_kernel_cred (NULL)) для установки учетных данных ядра и, наконец, мы вызываем switch_task_namespaces(find_task_by_vpid (1), init_nsproxy) для переключения пространство имен процесса 1 в пространство имен процесса инициализации. После этого мы восстанавливаем значение RBP и возвращаемся, чтобы возобновить выполнение (что немедленно приведет к возврату free_pipe_info()).
Выход из контейнера и получение корневой оболочки
Вернувшись в пользовательскую среду, у нас теперь есть права root на изменение пространств имен mnt, pid и net, чтобы выйти из контейнера и выйти из модуля kubernetes. В конце концов, мы открываем корневую оболочку.
Доказательство концепции
Proof-Of-Concept доступен по адресу https://github.com/google/security-research/tree/master/pocs/linux/cve-2021-22555.
Выполнение его на уязвимой машине предоставит вам root:
theflow@theflow:~$ gcc -m32 -static -o exploit exploit.c
theflow@theflow:~$ ./exploit
[+] Linux Privilege Escalation by theflow@ - 2021
[+] STAGE 0: Initialization
[*] Setting up namespace sandbox...
[*] Initializing sockets and message queues...
[+] STAGE 1: Memory corruption
[*] Spraying primary messages...
[*] Spraying secondary messages...
[*] Creating holes in primary messages...
[*] Triggering out-of-bounds write...
[*] Searching for corrupted primary message...
[+] fake_idx: ffc
[+] real_idx: fc4
[+] STAGE 2: SMAP bypass
[*] Freeing real secondary message...
[*] Spraying fake secondary messages...
[*] Leaking adjacent secondary message...
[+] kheap_addr: ffff91a49cb7f000
[*] Freeing fake secondary messages...
[*] Spraying fake secondary messages...
[*] Leaking primary message...
[+] kheap_addr: ffff91a49c7a0000
[+] STAGE 3: KASLR bypass
[*] Freeing fake secondary messages...
[*] Spraying fake secondary messages...
[*] Freeing sk_buff data buffer...
[*] Spraying pipe_buffer objects...
[*] Leaking and freeing pipe_buffer object...
[+] anon_pipe_buf_ops: ffffffffa1e78380
[+] kbase_addr: ffffffffa0e00000
[+] STAGE 4: Kernel code execution
[*] Spraying fake pipe_buffer objects...
[*] Releasing pipe_buffer objects...
[*] Checking for root...
[+] Root privileges gained.
[+] STAGE 5: Post-exploitation
[*] Escaping container...
[*] Cleaning up...
[*] Popping root shell...
root@theflow:/# id
uid=0(root) gid=0(root) groups=0(root)
root@theflow:/#
Тайм-лайн
2021-04-06 - Сообщение об уязвимости отправлено по адресу security@kernel.org.
2021-04-13 - Патч объединен в апстриме.
2021-07-07 - Публичное раскрытие.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
Введение
После BleedingTooth (https://google.github.io/security-research/pocs/linux/bleedingtooth/writeup.html), когда я впервые заглянул в Linux, я также захотел найти уязвимость повышения привилегий. Я начал с изучения старых уязвимостей, таких как CVE-2016-3134 и CVE-2016-4997, которые вдохновили меня на использование grep для memcpy() и memset() в коде Netfilter. Это привело меня к некоторому бажному коду.
Уязвимость
Когда IPT_SO_SET_REPLACE или IP6T_SO_SET_REPLACE вызывается в режиме совместимости, который требует возможности CAP_NET_ADMIN, которая, однако, может быть получена в пространстве имен user + network, структуры должны быть преобразованы из пользовательского в ядро, а также из 32-битного в 64-битный, чтобы они обрабатывались нативными функцииями. Естественно, это неизбежно приведет к ошибкам. Наша уязвимость находится в xt_compat_target_from_user(), где memset() вызывается со смещением target->targetize, которое не учитывается во время выделения, что приводит к записи нескольких байтов за пределы памяти:
C:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/x_tables.c
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
unsigned int *size)
{
const struct xt_target *target = t->u.kernel.target;
struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t;
int pad, off = xt_compat_target_offset(target);
u_int16_t tsize = ct->u.user.target_size;
char name[sizeof(t->u.user.name)];
t = *dstptr;
memcpy(t, ct, sizeof(*ct));
if (target->compat_from_user)
target->compat_from_user(t->data, ct->data);
else
memcpy(t->data, ct->data, tsize - sizeof(*ct));
pad = XT_ALIGN(target->targetsize) - target->targetsize;
if (pad > 0)
memset(t->data + target->targetsize, 0, pad);
tsize += off;
t->u.user.target_size = tsize;
strlcpy(name, target->name, sizeof(name));
module_put(target->me);
strncpy(t->u.user.name, name, sizeof(t->u.user.name));
*size += off;
*dstptr += tsize;
}
Targetsize не контролируется пользователем, но можно выбрать разные цели с разными размерами структуры по имени (например, TCPMSS, TTL или NFQUEUE). Чем больше размер цели, тем больше мы можем варьировать смещение. Тем не менее, целевой размер не должен быть выровнен на 8 байтов, чтобы заполнить pad > 0. Самый большой из возможных, который я нашел, - это NFLOG, для которого мы можем выбрать смещение до 0x4C байтов за пределами границ (на смещение можно повлиять, добавив отступ между struct xt_entry_match и struct xt_entry_target):
C:
struct xt_nflog_info {
/* 'len' will be used iff you set XT_NFLOG_F_COPY_LEN in flags */
__u32 len;
__u16 group;
__u16 threshold;
__u16 flags;
__u16 pad;
char prefix[64];
};
Обратите внимание, что место назначения буфера выделяется с помощью GFP_KERNEL_ACCOUNT и также может варьироваться по размеру:
C:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/x_tables.c
struct xt_table_info *xt_alloc_table_info(unsigned int size)
{
struct xt_table_info *info = NULL;
size_t sz = sizeof(*info) + size;
if (sz < sizeof(*info) || sz >= XT_MAX_TABLE_SIZE)
return NULL;
info = kvmalloc(sz, GFP_KERNEL_ACCOUNT);
if (!info)
return NULL;
memset(info, 0, sizeof(*info));
info->size = size;
return info;
}
Хотя минимальный размер > 0x100, что означает, что наименьший слаб, в которой может быть размещен этот объект, - kmalloc-512. Другими словами, мы должны найти жертв, которые распределены между kmalloc-512 и kmalloc-8192 для использования.
Эксплуатация
Наш примитив ограничен записью четырех байтов от нуля до 0x4C байтов за пределы памяти. При таком примитиве обычными целями являются:
-Счетчик ссылок
К сожалению, мне не удалось найти подходящих объектов со счетчиком ссылок в первых байтах 0x4C.
-Свободный указатель на список
CVE-2016-6187: Поочередное использование кучи ядра Linux - хороший пример того, как использовать указатель на список свободных мест. Однако это было уже 5 лет назад, а между тем в ядрах включена опция CONFIG_SLAB_FREELIST_HARDENED, которая, помимо прочего, защищает указатели свободных списков.
-Указатель в структуре
Это наиболее многообещающий подход, однако четыре нулевых байта - это слишком много для записи. Например, указатель 0xffff91a49cb7f000 может быть преобразован только в 0xffff91a400000000 или 0x9cb7f000, где оба они, вероятно, будут недопустимыми указателями. С другой стороны, если бы мы использовали примитив для записи в самом начале соседнего блока, мы могли бы записать меньше байтов, например 2 байта, и, например, повернув указатель с 0xffff91a49cb7f000 на 0xffff91a49cb70000.
Играя с некоторыми объектами-жертвами, я заметил, что никогда не смогу надежно разместить их вокруг struct xt_table_info в ядре 5.4. Я понял, что это как-то связано с флагом GFP_KERNEL_ACCOUNT, поскольку другие объекты, выделенные с помощью GFP_KERNEL_ACCOUNT, не имели этой проблемы. Янн Хорн подтвердил, что до версии 5.9 для ведения аккаунтинга использовались отдельные слабы. Следовательно, каждый примитив кучи, который мы используем в цепочке эксплойтов, также должен использовать GFP_KERNEL_ACCOUNT.
Системный вызов msgsnd() - это хорошо известный примитив для распыления кучи (который использует GFP_KERNEL_ACCOUNT) и уже использовался для нескольких общедоступных эксплойтов. Хотя удивительно, что его структура msg_msg никогда не подвергалась злоупотреблениям. В этой статье мы продемонстрируем, как можно злоупотребить этой структурой данных, чтобы получить примитив использования после освобождения, который, в свою очередь, можно использовать для утечки адресов и подделки других объектов. По совпадению, параллельно с моим исследованием в марте 2021 года Александр Попов также исследовал ту же самую структуру в Four Bytes of Power: CVE-2021-26708 в ядре Linux.
Изучение структуры msg_msg
При отправке данных с помощью msgsnd () полезная нагрузка разделяется на несколько сегментов:
C:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ipc/msgutil.c
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;
msg->next = NULL;
msg->security = NULL;
len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;
cond_resched();
alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
return msg;
out_err:
free_msg(msg);
return NULL;
}
где заголовки для структуры msg_msg и структуры msg_msgseg:
C:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/msg.h
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/types.h
struct list_head {
struct list_head *next, *prev;
};
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ipc/msgutil.c
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};
Первый член в msg_msg - это указатель mlist.next, который указывает на другое сообщение в очереди (которое отличается от следующего, поскольку это указатель на следующий сегмент). Как вы узнаете дальше, это идеальный кандидат на повреждение.
Получение use-after-free
Сначала мы инициализируем множество очередей сообщений (в нашем случае 4096) с помощью msgget(). Затем мы отправляем одно сообщение размером 4096 (включая заголовок msg_msg) для каждой очереди сообщений с помощью msgsnd(), которую мы будем называть основным сообщением. В конце концов, после большого количества сообщений у нас есть несколько последовательных:
Затем мы отправляем вторичное сообщение размером 1024 для каждой очереди сообщений с помощью msgsnd ():
Наконец, мы создаем несколько дыр (в нашем случае каждые 1024-е) в первичных сообщениях и запускаем уязвимую опцию setsockopt (IPT_SO_SET_REPLACE), которая в лучшем случае разместит объект xt_table_info в одной из дыр:
Мы решили перезаписать два байта соседнего объекта нулями. Предположим, что мы примыкаем к другому первичному сообщению, эти байты, которые мы перезаписываем, являются частью указателя на вторичное сообщение. Поскольку мы выделяем им размер 1024 байта, у нас есть шанс 1 -(1024/65536) перенаправить указатель (единственный случай, когда мы терпим неудачу, - это когда два младших байта указателя уже равны нулю).
Теперь лучший сценарий, на который мы можем надеяться - это тот, что управляемый указатель также указывает на вторичное сообщение, поскольку следствием этого будут два разных первичных сообщения, указывающих на одно и то же вторичное сообщение, и это может привести к использованию после освобождения:
Однако как узнать, какие два основных сообщения указывают на одно и то же вторичное сообщение? Чтобы ответить на этот вопрос, мы помечаем каждое (первичное и вторичное) сообщение индексом очереди сообщений, который находится в диапазоне [0, 4096). Затем, после запуска повреждения, мы перебираем все очереди сообщений, просматриваем все сообщения с помощью msgrcv() с MSG_COPY и проверяем, совпадают ли они. Если тег основного сообщения отличается от вторичного сообщения, это означает, что оно было перенаправлено. В этом случае тег основного сообщения представляет собой индекс очереди поддельных сообщений, т. е. тот, который содержит неправильное вторичное сообщение, а тег неправильного вторичного сообщения представляет индекс реальной очереди сообщений. Зная эти два индекса, достижение use-after-free теперь тривиально - мы извлекаем вторичное сообщение из реальной очереди сообщений с помощью msgrcv() и, как таковое, освобождаем его:
Обратите внимание, что у нас все еще есть ссылка на освобожденное сообщение в очереди поддельных сообщений.
Обход SMAP
Используя сокеты unix (которые можно легко настроить с помощью socketpair()), мы теперь распыляем множество сообщений размером 1024 и имитируем заголовок struct msg_msg. В идеале мы можем вернуть адрес ранее освобожденного сообщения:
Обратите внимание, что mlist.next - 41414141, поскольку мы еще не знаем никаких адресов ядра (когда SMAP включен, мы не можем указать адрес пользователя). Отсутствие адреса ядра имеет решающее значение, поскольку фактически мешает нам снова освободить блок (позже вы узнаете, почему это необходимо). Причина в том, что во время msgrcv() сообщение отключается от очереди сообщений, которая является циклическим списком. К счастью, у нас есть хорошие возможности для утечки информации, так как в msg_msg есть несколько интересных полей. А именно, поле m_ts используется для определения, сколько данных вернуть в пользовательское пространство:
Исходный размер сообщения составляет всего 1024 байта sizeof(struct msg_msg), который теперь мы можем искусственно увеличить до DATALEN_MSG = 4096-sizeof (struct msg_msg). Как следствие, теперь мы сможем прочитать сообщение, превышающее предполагаемый размер, и пропустить заголовок struct msg_msg соседнего сообщения. Как было сказано ранее, очередь сообщений реализована как круговой список, таким образом, mlist.next указывает на основное сообщение.
Зная адрес основного сообщения, мы можем переработать фальшивую структуру msg_msg с этим адресом как следующим (что означает, что это следующий сегмент). Затем может произойти утечка содержимого основного сообщения при чтении байтов, превышающих DATALEN_MSG. Утечка указателя mlist.next из первичного сообщения показывает адрес вторичного сообщения, которое находится рядом с нашей фальшивой структурой msg_msg. Вычитая 1024 из этого адреса, мы наконец получаем адрес фальшивого сообщения.
Получение UAF
Теперь мы можем перестроить фальшивый объект struct msg_msg с просочившимся адресом как mlist.next и mlist.prev (что означает, что он указывает на себя), сделав фальшивое сообщение свободным от очереди фальшивых сообщений.
Обратите внимание, что при распылении с использованием сокетов unix у нас фактически есть объект struct sk_buff, который указывает на поддельное сообщение. Очевидно, это означает, что когда мы освобождаем фальшивое сообщение, у нас все еще остается устаревшая ссылка:
Этот устаревший буфер данных struct sk_buff является лучшим сценарием использования после освобождения, поскольку он не содержит информации заголовка, а это означает, что теперь мы можем использовать его для освобождения любого типа объекта на слэбе. Для сравнения, освобождение объекта struct msg_msg возможно только в том случае, если первые два члена являются записываемыми указателями (необходимыми для разрыва связи с сообщением).
Поиск жертвы
Лучшая жертва атаки - это жертва, в структуре которой есть указатель на функцию. Помните, что жертве также должен быть назначен GFP_KERNEL_ACCOUNT.
В беседе с Янном Хорном он предложил объект struct pipe_buffer, который размещен в kmalloc-1024 (поэтому вторичное сообщение имеет размер 1024 байта). Структуру pipe_buffer можно легко выделить с помощью pipe(), которая имеет alloc_pipe_info() в качестве подпрограммы:
C:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/pipe.c
struct pipe_inode_info *alloc_pipe_info(void)
{
...
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
...
pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
if (pipe == NULL)
goto out_free_uid;
...
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
...
}
Хотя он не содержит напрямую указателя на функцию, он содержит указатель на struct pipe_buf_operations, который, с другой стороны, имеет указатели на функции:
C:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
struct pipe_buf_operations {
...
/*
* When the contents of this pipe buffer has been completely
* consumed by a reader, ->release() is called.
*/
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
...
};
Обход KASLR/SMEP
Когда кто-то пишет в каналы, заполняется struct pipe_buffer. Что наиболее важно, ops будет указывать на статическую структуру anon_pipe_buf_ops, которая находится в сегменте .data:
C:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/pipe.c
static const struct pipe_buf_operations anon_pipe_buf_ops = {
.release = anon_pipe_buf_release,
.try_steal = anon_pipe_buf_try_steal,
.get = generic_pipe_buf_get,
};
Поскольку разница между сегментом .data и сегментом .text всегда одинакова, наличие anon_pipe_buf_ops в основном позволяет нам вычислить базовый адрес ядра.
Мы распыляем множество объектов struct pipe_buffer и восстанавливаем местоположение устаревшего буфера данных sk_buff:
Поскольку у нас все еще есть ссылка из struct sk_buff, мы можем прочитать его буфер данных, пропустить содержимое struct pipe_buffer и раскрыть адрес anon_pipe_buf_ops:
[+] anon_pipe_buf_ops: ffffffffa1e78380
[+] kbase_addr: ffffffffa0e00000
С этой информацией теперь мы можем найти гаджеты JOP/ROP. Обратите внимание, что при чтении из сокета unix мы фактически также освобождаем его буфер:
Повышение привилегий
Мы заменяем устаревшую структуру pipe_buffer фальшивой, где ops указывает на фальшивую структуру pipe_buf_operations. Эта фальшивая структура устанавливается в том же месте, поскольку мы знаем ее адрес, и, очевидно, эта структура должна содержать указатель на вредоносную функцию в качестве релиза.
Заключительный этап эксплойта - закрыть все каналы, чтобы запустить релиз, который, в свою очередь, запустит цепочку JOP. Найти гаджеты JOP сложно, поэтому цель состоит в том, чтобы как можно скорее выполнить разворот стека ядра, чтобы выполнить цепочку ROP ядра.
ROP-цепочка ядра
Мы сохраняем значение RBP на некотором адресе блокнота в ядре, чтобы мы могли позже возобновить выполнение, затем мы вызываем commit_creds(prepare_kernel_cred (NULL)) для установки учетных данных ядра и, наконец, мы вызываем switch_task_namespaces(find_task_by_vpid (1), init_nsproxy) для переключения пространство имен процесса 1 в пространство имен процесса инициализации. После этого мы восстанавливаем значение RBP и возвращаемся, чтобы возобновить выполнение (что немедленно приведет к возврату free_pipe_info()).
Выход из контейнера и получение корневой оболочки
Вернувшись в пользовательскую среду, у нас теперь есть права root на изменение пространств имен mnt, pid и net, чтобы выйти из контейнера и выйти из модуля kubernetes. В конце концов, мы открываем корневую оболочку.
C:
setns(open("/proc/1/ns/mnt", O_RDONLY), 0);
setns(open("/proc/1/ns/pid", O_RDONLY), 0);
setns(open("/proc/1/ns/net", O_RDONLY), 0);
char *args[] = {"/bin/bash", "-i", NULL};
execve(args[0], args, NULL);
Доказательство концепции
Proof-Of-Concept доступен по адресу https://github.com/google/security-research/tree/master/pocs/linux/cve-2021-22555.
Выполнение его на уязвимой машине предоставит вам root:
theflow@theflow:~$ gcc -m32 -static -o exploit exploit.c
theflow@theflow:~$ ./exploit
[+] Linux Privilege Escalation by theflow@ - 2021
[+] STAGE 0: Initialization
[*] Setting up namespace sandbox...
[*] Initializing sockets and message queues...
[+] STAGE 1: Memory corruption
[*] Spraying primary messages...
[*] Spraying secondary messages...
[*] Creating holes in primary messages...
[*] Triggering out-of-bounds write...
[*] Searching for corrupted primary message...
[+] fake_idx: ffc
[+] real_idx: fc4
[+] STAGE 2: SMAP bypass
[*] Freeing real secondary message...
[*] Spraying fake secondary messages...
[*] Leaking adjacent secondary message...
[+] kheap_addr: ffff91a49cb7f000
[*] Freeing fake secondary messages...
[*] Spraying fake secondary messages...
[*] Leaking primary message...
[+] kheap_addr: ffff91a49c7a0000
[+] STAGE 3: KASLR bypass
[*] Freeing fake secondary messages...
[*] Spraying fake secondary messages...
[*] Freeing sk_buff data buffer...
[*] Spraying pipe_buffer objects...
[*] Leaking and freeing pipe_buffer object...
[+] anon_pipe_buf_ops: ffffffffa1e78380
[+] kbase_addr: ffffffffa0e00000
[+] STAGE 4: Kernel code execution
[*] Spraying fake pipe_buffer objects...
[*] Releasing pipe_buffer objects...
[*] Checking for root...
[+] Root privileges gained.
[+] STAGE 5: Post-exploitation
[*] Escaping container...
[*] Cleaning up...
[*] Popping root shell...
root@theflow:/# id
uid=0(root) gid=0(root) groups=0(root)
root@theflow:/#
Тайм-лайн
2021-04-06 - Сообщение об уязвимости отправлено по адресу security@kernel.org.
2021-04-13 - Патч объединен в апстриме.
2021-07-07 - Публичное раскрытие.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
Последнее редактирование: