
CVE-2022-42864 IOHIDFamily 競合状態の概念実証
これは、CVE-2022-42864(IOHIDFamily の time-of-check-time-of-use 脆弱性で、iOS 16.2 / macOS Ventura 13.1 で修正されました)に対する、私の(不完全な)概念実証エクスプロイトです。
現在のエクスプロイトは、multicast_bytecopy エクスプロイトで使用されているものと同じ「任意の kfree」プリミティブを実現しています。ただし、multicast_bytecopy のその後のエクスプロイトフローは大幅に緩和されているため、これは完全なエクスプロイトではなく、問題の深刻度を示すだけのものです。
もしそう聞かなければならないなら、実行すべきではありません。これは何も有用なことをせず、単にカーネルパニックを引き起こすだけです。このコードが引き起こすデータ損失や不安定性について、私は一切責任を負いません。
この問題が修正された際のAppleのコメントが、その概要を簡潔に説明しています:
// Find the number of cookies in the data. The data from elementData is shared with user space and may change at any time.
パッチ適用前の関数を見てみましょう(関連する行にラベルを付けました):
IOReturn IOHIDDevice::postElementTransaction(const void* elementData, UInt32 dataSize, UInt32 completionTimeout, IOHIDCompletion * completion)
{
IOReturn ret = kIOReturnError;
uint32_t cookies_[kMaxLocalCookieArrayLength];
uint32_t *cookies = cookies_;
uint32_t cookieCount = 0;
uint32_t cookieSize = 0;
uint32_t dataOffset = 0;
uint8_t *data = (uint8_t*)elementData;
IOMemoryDescriptor *elementDesc = getMemoryWithCurrentElementValues();
require(_elementArray && elementDesc, fail);
WORKLOOP_LOCK;
// Find the number of cookies in the data. Check that all cookies are valid elements. [1]
while (dataOffset < dataSize) {
const IOHIDElementValueHeader *headerPtr = (const IOHIDElementValueHeader *)(data + dataOffset);
IOHIDElementPrivate *element = GetElement(headerPtr->cookie);
if (!element) {
HIDDeviceLogError("Could not find element for cookie: %d", headerPtr->cookie);
ret = kIOReturnAborted;
goto fail;
}
cookieCount++;
require_noerr_action(os_add3_overflow(dataOffset, headerPtr->length, sizeof(IOHIDElementValueHeader), &dataOffset), fail, HIDDeviceLogError("Overflow iterating cookie data buffer %u %u", dataOffset, headerPtr->length));
}
// Data isn't as large as expected, don't overrun, just abort
if (dataOffset != dataSize) { // [2]
HIDDeviceLogError("Cookie data buffer is smaller than expected. %u vs. %u",
(unsigned int)dataSize, (unsigned int)dataOffset);
ret = kIOReturnAborted;
goto fail;
}
dataOffset = 0;
require_noerr_action(os_mul_overflow(cookieCount, sizeof(uint32_t), &cookieSize),
fail,
HIDDeviceLogError("Overflow calculating cookieSize"));
cookies = (cookieCount <= kMaxLocalCookieArrayLength) ? cookies : (uint32_t*)IOMallocData(cookieSize); // [3]
if (cookies == NULL) {
ret = kIOReturnNoMemory;
goto fail;
}
// Update the elements, this replaced the shared kernel-user shared memory.
for (size_t index = 0; dataOffset < dataSize; ++index) { // [4]
const IOHIDElementValueHeader *headerPtr;
IOHIDElementPrivate *element;
OSData *elementVal;
headerPtr = (const IOHIDElementValueHeader *)(data + dataOffset);
element = GetElement(headerPtr->cookie);
dataOffset += headerPtr->length + sizeof(IOHIDElementValueHeader);
elementVal = OSData::withBytesNoCopy((void*)headerPtr->value,
headerPtr->length); // [5]
require_action(elementVal, fail, ret = kIOReturnNoMemory);
element->setDataBits(elementVal);
elementVal->release();
cookies[index] = headerPtr->cookie; // [6]
}
// Actually post elements
ret = postElementValues((IOHIDElementCookie *)cookies, (UInt32)cookieCount, 0, completionTimeout, completion);
fail:
WORKLOOP_UNLOCK;
if (cookies != &cookies_[0]) {
IOFreeData(cookies, cookieSize);
}
return ret;
}
[1] のループは、バッファ内の IOHIDElementValue の数をカウントし、その数を cookieCount に格納します。[2] のチェック(while ループの条件と組み合わせて)により、各ヘッダの length フィールドが elementData バッファの境界を越えない(また、バッファの終端に満たないこともありませんが、これはあまり重要ではありません)ことが保証されます。[3] で cookies バッファが cookieCount * 4 のサイズでヒープに割り当てられます(または cookieCount が十分に小さい場合はスタックバッファが使用されます)。[4] の 2 番目のループで、バッファを再度パースし、IOHIDElementValue を再解析します。length フィールドを使用して、[5] で各要素の値を保持する OSData オブジェクトが作成されます。[6] で、各要素の cookie が [3] で割り当てられた cookies 配列に書き込まれます。では、問題は何でしょうか? この関数は elementData が不揮発性である場合には完全に正しく動作しますが、問題はメソッドが共有メモリで呼び出された場合に発生します。IOHIDInterface::SetElementValues_Impl DriverKit メソッドを見てみましょう:
kern_return_t
IMPL(IOHIDInterface, SetElementValues)
{
IOReturn ret = kIOReturnError;
UInt8 *values = NULL;
IOBufferMemoryDescriptor *md = NULL;
md = OSDynamicCast(IOBufferMemoryDescriptor, elementValues);
require_action(md && count, exit, ret = kIOReturnBadArgument);
values = (UInt8 *)md->getBytesNoCopy();
// Post the data to the device
ret = _owner->postElementTransaction(values, (UInt32)md->getLength());
require_noerr_action(ret, exit, HIDServiceLogError("postElementValues failed: 0x%x", ret));
exit:
return ret;
}
ここで、postElementTransaction が md->getBytesNoCopy() で呼び出されており、そのメモリはユーザ空間と共有されています。これにより、elementData が不揮発性であるという前提が破られます。[1] のループの後、[4] のループの前に elementData バッファの内容が変化する可能性があるため、攻撃者にとってこれは何を意味するのでしょうか?
攻撃者がこれを悪用できる方法は 2 つあります:
IOHIDElementValueHeader の length を非常に大きな値にすり替えることです。これにより、[5] で OSData が作成されるとき、それが elementData バッファの境界をはるかに超えて広がり、攻撃者は IOHIDInterface::GetElementValues_Impl を使用して範囲外のデータを読み取ることが可能になります。IOHIDElementValueHeader の length を非常に小さな値にすり替えることです。これにより、[4] のループは [1] のループで最初にカウントされたよりもはるかに多くのヘッダを解析するため、[6] で cookies が cookies 配列に書き込まれるとき、index が cookieCount に対して検証されないため、配列からオーバーフローします。実際には、これにより攻撃者は任意のサイズのカーネルヒープデータを範囲外読み取りでき、任意のサイズの任意のデータを範囲外にカーネルヒープに書き込むことができます。これらは 2 つの強力なプリミティブです。
レースコンディションでは、レースに勝ったかどうかを判断する方法を常に探します。それにより、成功するまで試行を続け、トリガーを決定論的にすることができます。幸いなことに、このレースコンディションの場合、まさにそれが可能です。
OOB 読み取りバリアントでは、length を切り替えるヘッダの後に、value を認識可能な定数 0xD1AB011CAC1DF00D に設定した IOHIDElementValueHeader をもう 1 つ配置します。そして、要素の値を読み戻すときに、返されたデータの先頭にその 0xD1AB011CAC1DF00D ヘッダが表示されれば、レースに勝ったことがわかります。
OOB 書き込みバリアントでは、length を切り替えるヘッダの後で、cookie がオーバーフローするヘッダの前に、今度は認識可能な value として 0xD15EA5ED を持つ IOHIDElementValueHeader を 1 つ配置します。このヘッダは、レースに負けた場合には大きな要素の value の中にカプセル化されるため、レースに勝った場合にのみヘッダが解析され、要素の値が 0xD15EA5ED に設定されます。要素の値を読み戻すことで、成功したかどうかがわかります。
この問題を修正するために、Apple はループ [1] とループ [4] の間に 3 つ目のループを追加し、各 length フィールドを検証して、新しい dataLengths 配列にキャッシュし、要素の数が変更されていないことを確認するようにしました。最終的なループでは、キャッシュされた長さを使用して計算を行い、バッファからの再度の読み取りを回避します。
この問題を悪用する際の主な障害は、オーバーフローするバッファが KHEAP_DATA_BUFFERS に属しているため、エクスプロイトの対象が限られることです。この概念実証では、kmsg ヘッダをターゲットに選択しました。KHEAP_DATA_BUFFERS の中でカーネルポインタを含む数少ない構造体の 1 つだからです。このアプローチで得られた「任意の kfree」プリミティブは、multicast_bytecopy エクスプロイトで使用されているものと同じですが、IOSurfaceClient 配列は現在 PAC 化されており、偽造されたクライアントはそれを作成した IOSurfaceRootUserClient への有効なポインタを持つ必要があるため、もはや望ましいカーネル r/w ターゲットではなくなりました。
Apple はカスタム DriverKit 拡張機能のビルドとインストールを容易にしておらず、特に有料の Apple Developer アカウントがない場合には困難ですが、不可能ではありません。
開始する前に、以下をお勧めします:
amfi_get_out_of_my_way=1 cs_enforcement_disable=1 を設定するsystemextensionsctl developer on を実行するその後、Xcode プロジェクト設定で開発チームを選択でき、プロジェクトは正常にビルドされます。次に HIDDriverLoader を実行し、「Install Dext」を使用して DriverKit 拡張機能をインストールし、「Trigger Exploit」で、お察しの通り、エクスプロイトをトリガーします。
これが失敗した場合は、署名なしで以下のようにビルドを試すこともできます:
xcodebuild build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
その後、手動で署名します:
codesign -fs self-sign-cert --entitlements HIDDriverLoader/HIDDriverLoader.entitlements build/Release/HIDDriverLoader.app/Contents/MacOS/HIDDriverLoader
codesign -fs self-sign-cert --entitlements HIDDriver/HIDDriver.entitlements build/Release/HIDDriverLoader.app/Contents/Library/SystemExtensions/*.dext/*.driver
self-sign-cert は、キーチェーン内の自己署名証明書の名前に置き換えてください。