
Эксплойт ядра Android 14 для Pixel7/8 Pro
Эта статья предоставляет углубленный анализ двух уязвимостей ядра в Mali GPU, доступных из стандартной песочницы приложений, которые я самостоятельно выявил и сообщил в Google. Она включает эксплойт ядра, достигающий произвольного чтения/записи в ядро. Впоследствии он отключает SELinux и повышает привилегии до root на Google Pixel 7 и 8 Pro с указанными версиями Android 14:
google/husky/husky:14/UD1A.231105.004/11010374:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keysgoogle/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (от m4b4 (Marcel))Этот эксплойт использует две уязвимости: целочисленное переполнение из-за неполного исправления в ioctl-команде gpu_pixel_handle_buffer_liveness_update_ioctl, и утечку информации в буферах сообщений потока временной шкалы.
Google устранил целочисленное переполнение в ioctl-команде gpu_pixel_handle_buffer_liveness_update_ioctl в этом коммите. Сначала, когда я сообщил об этой проблеме, я думал, что ошибка вызвана проблемой в упомянутом патче. После анализа отчета я осознал, что мой анализ уязвимости был неточным. Несмотря на мое первоначальное предположение о неполноте патча, он эффективно устраняет и предотвращает недостаток в вычислениях. Это заставило меня заподозрить, что изменение не было применено в производственных сборках. Однако, хотя я могу вызвать недостаток в вычислениях, переполнение вызвать невозможно. Это предполагает, что ioctl-команда была частично исправлена, хотя и не с помощью указанного выше патча. Анализ в IDA показал, что в производственных релизах поставляется другой неполный патч, отсутствующий в любой ветке git модуля ядра Mali GPU.
Эта уязвимость была впервые обнаружена в последней версии Android и сообщена 19 ноября 2023 года. Google позже уведомил меня, что они уже внутренне идентифицировали её и присвоили CVE-2023-48409 в декабрьском бюллетене безопасности Android, пометив как дубликат. Хотя я смог проверить, что ошибка была внутренне идентифицирована за месяцы до моего отчета (исходя из даты коммита около 30 августа), остается неясность. В частности, странно, что уровни исправлений безопасности (SPL) за октябрь и ноябрь самых последних устройств все еще были подвержены этой уязвимости — я не исследовал версии, предшествующие этим. Поэтому я не могу окончательно определить, была ли это действительно дублирующаяся проблема и было ли соответствующее исправление запланировано на декабрь до моей подачи, или же было упущение в устранении этой уязвимости.
В любом случае, что делает эту ошибку мощной, так это следующее:
info.live_ranges полностью контролируется пользователем.info.live_ranges может находиться на произвольном смещении до начала адреса ядра buff.Эта уязвимость схожа с уязвимостью недостатка буфера DeCxt::RasterizeScaleBiasData(), которую я нашел и эксплуатировал в ядре iOS 15 в 2022 году.
GPU Mali реализует пользовательский поток временной шкалы, предназначенный для сбора информации, её сериализации и последующей записи в кольцевой буфер в определенном формате. Пользователи могут вызвать ioctl-команду kbase_api_tlstream_acquire, чтобы получить файловый дескриптор, позволяющий читать из этого кольцевого буфера. Формат сообщений следующий:
Сериализованный буфер сообщения, конкретное содержимое которого зависит от идентификатора сообщения.
Например, функция __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait сериализует указатели ядра kbase_kcpu_command_queue и dma_fence в буфер сообщения, что приводит к утечке указателей ядра в пользовательский процесс.```c
void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait(
struct kbase_tlstream *stream,
const void *kcpu_queue,
const void *fence
)
{
const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT;
const size_t msg_size = sizeof(msg_id) + sizeof(u64)
+ sizeof(kcpu_queue)
+ sizeof(fence)
;
char *buffer;
unsigned long acq_flags;
size_t pos = 0;
buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);
pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id)); pos = kbasep_serialize_timestamp(buffer, pos); pos = kbasep_serialize_bytes(buffer, pos, &kcpu_queue, sizeof(kcpu_queue)); pos = kbasep_serialize_bytes(buffer, pos, &fence, sizeof(fence));
kbase_tlstream_msgbuf_release(stream, acq_flags); }
Доказательство концепции эксплуатации утечки адреса объекта `kbase_kcpu_command_queue` путем мониторинга идентификатора сообщения `KBASE_TL_KBASE_NEW_KCPUQUEUE`, который отправляется функцией `kbasep_kcpu_queue_new` при каждом выделении нового объекта очереди kcpu.
Google сообщил мне, что уязвимость была зарегистрирована в марте 2023 года и получила идентификатор [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) в их бюллетене безопасности. Тем не менее, мне удалось воспроизвести проблему на новейших устройствах Pixel, поставляемых с уровнями исправлений безопасности (SPL) за октябрь и ноябрь, что указывает на то, что исправление не было применено правильно или вообще не было применено. Впоследствии Google быстро устранил проблему в декабрьском бюллетене обновлений безопасности, не указав автора, а позже сообщил мне, что проблема считается дубликатом. Однако обоснование, почему эту проблему пометили как дубликат, остается спорным.
## Эксплуатация
---
Итак, у меня есть две интересные уязвимости. Первая предоставляет мощную возможность изменять содержимое любого 16-байтового выровненного адреса ядра, который находится перед выделенным ~buff~ адресом. Вторая уязвимость дает подсказки о возможных местоположениях объектов в памяти ядра.
### Примечания к значениям buffer_count и live_ranges_count
Имея полный контроль над полями `buffer_count` и `live_ranges_count`, я могу гибко выбирать целевой slab и точное смещение, которое собираюсь записать. Однако выбор значений для `buffer_count` и `live_ranges_count` требует тщательного рассмотрения из-за нескольких ограничений и факторов:
- Оба значения связаны, и переполнение произойдет только в том случае, если все вновь введенные проверки будут обойдены.
- Требование, чтобы отрицательное смещение было выровнено по 16 байтам, ограничивает возможность записи в произвольное место. Однако это обычно не является существенным препятствием.
- Выбор большего смещения приводит к записи большого объема данных в области памяти, которые могут не быть целевыми. Например, если размер выделения переполняется до `0x3004`, указатель `live_ranges` будет установлен на `-0x4000` байт от выделенного пространства объекта `buff`. Функция `copy_from_user` затем запишет `0x7004` байт, исходя из расчета `update->live_ranges_count` умноженного на 4. Следовательно, эта операция приведет к перезаписи пользовательскими данными области памяти между указателем `live_ranges` и выделением `buff`. Поэтому необходимо тщательно убедиться, что никакие критически важные системные объекты в этом диапазоне не будут случайно перезаписаны. Учитывая, что операция включает вызов `copy_from_user`, можно подумать о том, чтобы вызвать `EFAULT` путем намеренного снятия отображения нежелательной области памяти после исходного буфера пользователя, чтобы предотвратить запись данных в чувствительные места. Однако этот подход неэффективен, потому что если функция `raw_copy_from_user` завершится ошибкой, она обнулит оставшиеся байты в целевом буфере ядра. Такое поведение реализовано для того, чтобы в случае частичного копирования из-за ошибки остальная часть буфера ядра не содержала неинициализированных данных.```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
Учитывая это, нам нужно тщательно выбрать объект для перезаписи и данные для записи.
Поскольку я застрял с этой неудачной проверкой, моя стратегия — найти объект, который при обнулении не приведет к нежелательным последствиям. Но прежде чем перейти к этому, есть еще одна проблема, с которой нужно разобраться. Помните, в прошлой части я сказал, что могу выбрать любой размер выделения и, следовательно, любой аллокатор общего назначения slab cache для обслуживания моего буфера выделения? Это неверно, потому что опять из-за copy_from_user! Это связано с механизмом защиты CONFIG_HARDENED_USERCOPY. Он запрещает указывать размер, который не соответствует размеру соответствующего slab cache, к которому относится буфер назначения ядра (в данном случае объекта кучи). Он определяет, является ли страница буфера slab-страницей, и если да, то получает соответствующий kmem_cache->size и проверяет, не превысит ли предоставленный пользователем размер его; в противном случае ядро просто падает из-за несоответствия размера. Иными словами, я не могу нацеливаться на объекты, принадлежащие аллокатору общего назначения, НО я все еще могу нацеливаться на объекты больших размеров (т. е. те, которые обслуживаются непосредственно аллокатором страниц).
Первая мысль, которая пришла в голову, — использовать технику pipe_buffer, которая является очень элегантным способом получения примитивов произвольного чтения/записи. Я не буду вдаваться в подробности этой техники, но читателям рекомендуется прочитать этот замечательный блог от Interrupt Labs. При создании объекта pipe объект pipe_buffer изначально создается в массиве из 16 элементов; однако размер массива можно изменить с помощью fcntl(F_SETPIPE_SZ). Таким образом, выделение массива pipe_buffer может быть настроено так, чтобы он обслуживался аллокатором страниц, что делает его идеальным целевым объектом для атаки.
После выбора объекта pipe_buffer в качестве цели, следующий шаг к достижению чтения/записи ядра — перезаписать его содержимое с помощью уязвимости underflow, что позволит мне читать/записывать из/в любую область памяти, страница которой перезаписывает поле pipe_buffer->page. Поскольку уязвимость позволяет мне записывать произвольные данные, я могу контролировать все содержимое pipe_buffer, включая его поле page, и для этого мне нужно выделить массив pipe_buffer до уязвимого объекта kbuff, и они должны находиться рядом друг с другом.
Я засорил память ядра множеством объектов kbase_kcpu_command_queue, а затем последовал за ними массивом pipe_buffer. Я не могу использовать только массивы pipe_buffer в качестве основного источника для распыления из-за ограничения, налагаемого pipe_max_size. Поэтому я решил начать распыление с объекта kbase_kcpu_command_queue. Выбор объекта kbase_kcpu_command_queue был обусловлен двумя причинами: его размер выделения равен 0x38C8, поэтому он обрабатывается аллокатором страниц, и я могу детерминированно получить его адрес в ядре, используя информационную уязвимость утечки ядра, что делает его хорошим объектом для распыления, а также хорошей целью (как мы увидим в следующем разделе).
Как упоминалось ранее, я использовал fcntl(F_SETPIPE_SZ) для увеличения размера выделения массива pipe_buffer, чтобы он мог обслуживаться аллокатором страниц. Если быть точнее, я выбрал размер выделения равным ==0x4000 bytes (4 * PAGE_SIZE)==, чтобы соответствовать выделениям kbase_kcpu_command_queue.
Для правильного использования pipe_buffer требуется адрес страницы. Возможность идентифицировать адрес ядра объекта kbase_kcpu_command_queue, который я могу намеренно создавать и уничтожать, делает его хорошим кандидатом для использования, а найти соответствующий struct page можно с помощью virt_to_page.
Итак, объект pipe_buffer выглядит следующим образом:```c
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
Как уже упоминалось, поле `page` должно содержать корректный адрес страницы. Поля `offset` и `len` не должны превышать `PAGE_SIZE`, иначе канал (pipe) увеличит счётчики head/tail, что приведёт к использованию нового объекта `pipe_buffer` и потере контроля над поддельным буфером канала.
Кроме того, `flags` должно быть `PIPE_BUF_FLAG_CAN_MERGE`, чтобы при последующих вызовах `pipe_write` вместо слепого увеличения счётчика head и использования следующего `pipe_buffer` сначала проверялось, есть ли место в текущем `pipe_buffer`, которое вместит запрос на запись, и если да, то данные просто дописывались в тот же буфер, начиная со значения, хранящегося в поле `len`.
Чтобы избежать краха устройства в `pipe_buf_confirm`, который вызывается `pipe_write` и `pipe_read`, указатель `ops` также должен быть корректным адресом ядра с полем `ops->confirm`, установленным в _NULL_. Я могу просто использовать смещение внутри утёкшего объекта `kbase_kcpu_command_queue`, которое равно NULL и не изменится ни при каких обстоятельствах.
### Выбор оптимального значения смещения для переполнения вниз (underflow)
Хотя размеры выделения для `buff`, `kbase_kcpu_command_queue` и `pipe_buffer` составляют ~0x4000~ байт, я решил выполнить переполнение вниз буфера на **0x8000** байт. Почему?
Давайте кратко рассмотрим, как обновляются `pipe_buffers` во время операций чтения и записи. Предположим, мы можем сформировать `pipe_buffer` следующим образом:```c
struct pipe_buffer {
.page = virt_to_page(addr),
.offset = 0,
.len = 0x40,
.ops = kcpu_addr + 0x50,
.flags = PIPE_BUF_FLAG_CAN_MERGE,
unsigned long private = 0
};
Хотя ошибка даёт возможность произвольно управлять содержимым этого объекта, она делает это только один раз, потому что объект с переполнением вниз освобождается сразу после завершения вызова ioctl. Это на самом деле создаёт проблему, так как мне нужно вручную обновлять объект pipe_buffer, чтобы сделать его снова пригодным для использования, поскольку каждая операция чтения/записи в канале:
.page не обновляется; оно остаётся тем же, и когда буфер пуст, он освобождается, чего я не хочу, потому что поле .ops установлено некорректно.pipe_buffer обновляет поле .offset при операции чтения, я не могу прочитать ту же область памяти снова.pipe_buffer, будут добавлены в буфер, начиная со значения .len (при условии, что установлен флаг PIPE_BUF_FLAG_CAN_MERGE), и .len обновляется соответственно. То есть мы не можем записать данные в точный адрес дважды.В результате, если я не обновлю pipe_buffer после каждой операции чтения или записи, я не смогу одновременно читать и записывать из/в один и тот же канал. Вот почему переполнение вниз на 0x8000 байт гораздо практичнее, потому что вместо перезаписи одного pipe_buffer, я перезапишу два различных экземпляра pipe_buffer двух различных объектов каналов: один будет предназначен для чтения, а другой — для записи.```c
#define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */
pipe_read = (struct pipe_buffer *)( ptr); pipe_read->page = virt_to_page(ta->kcpu_kaddr); pipe_read->offset = 0; pipe_read->len = 0xfff; pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_read->private = 0;
pipe_write = (struct pipe_buffer )( ptr + 0x4000); pipe_write->page = virt_to_page(ta->kcpu_kaddr); pipe_write->offset = 0; pipe_write->len = 0; / This is the starting position of the pipe_write */ pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_write->private = 0;
`pipe_read` — это фиктивный буфер канала, который будет использоваться для чтения данных с целевой страницы, начиная с `.offset = 0` и до `0xfff` байт, тогда как `pipe_write` — это фиктивный `pipe_buffer`, который будет использоваться для записи данных, начиная с `.len = 0` и до `0xfff` байт.
Также очень важно еще раз отметить, что запись более `PAGE_SIZE` байт приведет к увеличению счетчика head в канале, что приведет к использованию свежего вновь выделенного `pipe_buffer` и потере контроля над нашим фиктивным `pipe_write`. С другой стороны, опустошение (чтение 0xfff данных из) буфера `fake_read` заставляет ядро освободить фактическую страницу вызовом `ops→release`, что вызывает крах ядра, так как у меня пока нет адреса текста ядра.
Хотя мне удалось разделить операции чтения и записи в канале так, что выполнение записи в одном конце канала не мешает другому буферу канала и наоборот, я все еще не решил основную проблему: как надежно обновлять буфер канала? Очевидным ответом было просто повторять процесс заливки снова и снова после каждого вызова чтения или записи в канал. И это не имеет смысла, потому что это существенно повлияло бы на надежность эксплойта. В следующем разделе я разделю цель на две подцели: для начала я сосредоточусь только на поле `.page`, а затем на полях `.len/.offset`.
### Изменение поля pipe_buffer→page
К моему удивлению, мне не нужно и не требуется обновлять поле `.page` вообще, потому что я могу перезаписать `pipe_buffer→page`, чтобы он указывал на адрес страницы утекшего `kbase_kcpu_command_queue`. Следовательно, **все, что мне нужно сделать, это освободить объект `kbase_kcpu_command_queue` и перекрыть его новым объектом `pipe_buffer`. Ага! Теперь у меня есть `pipe_buffer→page`, указывающий на легитимный объект `pipe_buffer`!**
Замена `kbase_kcpu_command_queue` на `pipe_buffer` дает нам возможность манипулировать легитимным буфером канала без необходимости регулярно обновлять поле `.page`. Однако мне все еще нужно разобраться с полями `.len` и `.offset`.
### Изменение полей pipe_buffer→len/offset
Как я упоминал ранее, выполнение чтения/записи в канале обновляет поля `.len` и `.offset`, делая последующие операции чтения/записи на той же странице непригодными, даже если они выполняются через два разных канала. Вот еще один трюк: **существует техника чтения/записи данных без касания полей `.len/.offset`!**. И это возможно путем вызывания ошибки (`fault`) в вызовах `copy_page_from_iter` и `copy_page_to_iter` в `pipe_read/write`! Да, так же, как и с `copy_to/from_user`, `copy_page_to/from_iter` копирует данные из/в пользовательское пространство, переданное через структуру `iov_iter`, и в ней можно вызвать ошибку.
Продолжая предыдущий пример, если мы хотим записать 8 байт данных по некоторому адресу, размер предоставленного буфера пользовательского пространства должен быть 8, за которым следует неотображенная или недоступная для чтения область памяти, а затем передать `9` в качестве аргумента размера системному вызову `write`, указывая количество данных, которые мы хотим записать. Эта операция запишет 8 байт и завершится ошибкой на _девятом_, так как столкнется с неотображенной/нечитаемой областью памяти. В результате данные фактически записаны в целевой буфер ядра, а поле `.len` не было изменено. Функция ядра `pipe_write` просто вернется без обновления поля `buf->len`.```c
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
ret = pipe_buf_confirm(pipe, buf);
if (ret)
goto out;
ret = copy_page_from_iter(buf->page, offset, chars, from);
if (unlikely(ret < chars)) {
ret = -EFAULT;
goto out;
}
buf->len += ret;
if (!iov_iter_count(from))
goto out;
}
То же самое верно и для операций чтения; если мы хотим прочитать 8 байт, сделаем девятый байт буфера недоступным для чтения и просто заявим, что хотим прочитать 9 байт — данные будут скопированы в пользовательский буфер без изменения поля .offset.
В результате мы можем выполнять неограниченные операции чтения/записи по любому адресу памяти ядра, без необходимости повторно проходить процесс распыления.
Теперь, имея мощный примитив произвольного чтения/записи, я просто просмотрел все struct page в массиве VMEMMAP_START, чтобы определить начальный адрес текста ядра, используя метод, описанный в блоге Interrupt Labs. Затем я обнаружил, что init_task обнуляется в Android November Security Updates, поэтому вместо него я использовал kthreadd_task. Имея адрес ядра kthreadd_task, я смог пройти по списку task->tasks и получить адрес ядра своей собственной задачи current, после чего обнулить структуру cred для получения root-привилегий.
Позже я понял, что сканировать все адреса страниц необязательно, поскольку у меня уже был адрес текста ядра anon_pipe_buf_ops из объекта pipe_buffer. С этой информацией я мог вычислить базовый адрес текста ядра, фактически обойдя KASLR.
Эксплойт также отключает SELinux: с базовым адресом текста ядра мне нужно лишь найти местоположение глобальной структуры selinux_state, а затем обнулить поле .enforcing.
Эксплойт PoC, приложенный к отчёту, был протестирован на устройствах Pixel 7 и 8 Pro под управлением Android 14 с ASB октября и ноября, достигнув почти 100% успеха. Также важно отметить, что эксплойт не будет работать «из коробки» на других устройствах из-за использования некоторых жёстко закодированных смещений. Чтобы добавить поддержку нового устройства, необходимо указать следующее:
kthreadd_task от базового адреса ядра.selinux_state от базового адреса ядра.task_struct->cred, task_struct->pid и task_struct->tasks.anon_pipe_buf_ops от базового адреса ядра.Чтобы скомпилировать эксплойт как отдельный бинарный файл, используйте следующую команду, затем используйте adb shell для его запуска:```sh
$ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog
$ adb push poc /data/local/tmp/
$ adb shell /data/local/tmp/poc
Вы также можете запустить эксплойт через приложение Android Studio, встроив этот каталог вместе с ним, и убедитесь, что отключили бесполезные предупреждения C++, добавив `-w -Wno-c++11-narrowing` в файл cmake.
### Демо```shell
$ adb logcat |grep -i EXPLOIT
11-28 16:04:12.500 7989 7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563 7989 7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000 from context (0x0)
11-28 16:04:18.441 7989 7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000 from context (0xff)
11-28 16:04:18.442 7989 7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442 7989 7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444 7989 7989 E EXPLOIT : 10 00 39 01 89 FF FF FF 10 00 39 01 89 FF FF FF | ..9.......9.....
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.445 7989 7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446 7989 7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462 7989 7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463 7989 7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF 00 00 00 00 30 00 00 00 | @..&........0...
11-28 16:04:18.463 7989 7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF 10 00 00 00 00 00 00 00 | p7..............
11-28 16:04:18.463 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 | ........
11-28 16:04:18.463 7989 7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463 7989 7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102 7989 7989 E EXPLOIT : [+] Cleanup ... OK