OOBE:レジストリ内の平文パスワードの話 (CVE-2023-21726)2023/04/26
概要
CVE-2023-21726 [1]はCredUI (CredPackAuthenticationBufferW [2])のWindows API 関数に発見された脆弱性であり、平文パスワードの漏洩につながる可能性があります。本記事では、考えられる攻撃の一例のみを紹介しますが、理論上は、CredPackAuthenticationBufferW (CRED_PACK_PROTECTED_CREDENTIALS, ...)のWindows API関数を使用するすべてのアプリケーションが端末のどこかにプレーンテキストのパスワードを保存している可能性があります。 CredUI以外にも、OOBE(Out-of-Box Experience [3])が奇妙な動作をしており、新しく作成したユーザーアカウントの認証情報をレジストリ(非特権Medium integrityユーザーがアクセスできる)にキャッシュしようとしていることが確認されています。この動作がこの問題を発見するきっかけとなりました。
パスワードの発見について
アプリケーションの設定データがディスク上に保存されることは一般的です。通常、このデータは暗号化された形で保存されます。方式は、DPAPIのCryptProtectDataデータブロブから独自の暗号化アルゴリズムまでさまざまです。
しかし、時々、セキュリティモデルの設計不足や開発者のミスで、重要なデータが平文の状態で保存されてしまうことがあります。
そのため、ディスク(Rawモード)やメモリをスキャンして、平文のパスワードを含む重要な文字列を探すことが魅力的になることがあります。ほとんどの場合、ランダムなデータの中で誤検知が発生しますが、たまに思いがけないものも見つかることがあります。
そこで、我々が行ったのは、新しいWindowsをインストールし、OOBEを通じて最初のパスワード保護されたアカウントを作成しました。次に、仮想マシンのディスクドライブをスキャンしたところ、「C:¥Windows¥System32¥Config¥DEFAULT」ファイルに作成したパスワードが平文で保存されていることを確認しました。
このファイルに対応するレジストリキーは(特権を持たないユーザーもアクセス可能)以下になります。
HKEY_USERS\.DEFAULT\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker\LocalSystemAuthBuffer
技術的詳細(CredUI側)
主要な問題は、credui.dll
ライブラリファイルにありました。ユーザーがCRED_PACK_PROTECTED_CREDENTIALSフラグを指定しているにもかかわらず、パスワードの暗号化が全く行われていないことが判明しました。
以下の簡単なプログラムによって、この問題を実証することができます。
#include <windows.h>
#include <stdio.h>
#include <wincred.h>
#pragma comment(lib,"Credui.lib")
void HexTable(pCred, dwCredSize) {
unsigned char *ptr = (unsigned char *)pCred;
for (size_t i = 0; i < dwCredSize; i++) {
if (i % 16 == 0) {
printf("%08zx: ", i);
}
printf("%02x ", ptr[i]);
if (i % 16 == 15) {
printf(" |");
for (size_t j = i - 15; j <= i; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
printf("|\n");
}
}
}
int main() {
DWORD dwCredSize = 0x1000;
PBYTE pCred = (PBYTE)malloc(0x1000);
if (CredPackAuthenticationBufferW(CRED_PACK_PROTECTED_CREDENTIALS, (LPWSTR) L"UserName", (LPWSTR)L"P@ssw0rd123", pCred, &dwCredSize)) {
HexTable(pCred, dwCredSize);
} else {
printf("CredUnPackAuthenticationBufferW LastError=%08x", GetLastError());
}
}
以下に、修正前と修正後で実行した結果を示します。
結果の通り、修正後にパスワードは暗号化されているようです。これは、CryptProtectMemory(CRYPTPROTECTMEMORY_SAME_LOGON)[4]とほぼ同様なWindows API関数の内部呼び出しによって行われており、暗号化時に使用されたLUIDと等しくないLUIDを持つユーザーがパスワードを復号することができないようになっています。また、上記に加えて、再起動後に同様なLUIDを持つアカウントでも復号できなくなってしまいます。これは、暗号化に使用されるグローバルキー値が、コンピュータ起動時にcng.sys
ライブラリファイルによってランダムに生成されるためです。
現在のコールフロー
CredPackAuthenticationBufferW (cryptui.dll) | - CredPackKerbBufferFromStrings (cryptui.dll) | -- CredProtectEx (sechost.dll) | --- CredpEncryptAndMarshalBinaryBlobEx (sechost.dll) | ---- CredpEncodeSecretEx (sechost.dll) | ------ SystemFunction040 / RtlEncryptMemory(RTL_ENCRYPT_OPTION_SAME_LOGON) (cryptbase.dll) | ------- NtDeviceIoControlFile ("\\??\KSecDD", 0x39001E, ...) | .... | -------- CngEncryptMemoryEx (cng.sys)
CngEncryptMemoryExのWindows API関数の動作については、[5](flare-on, KeePass writeup)で詳しく説明されています。
以下では、cryptui.dll
ライブラリファイルの修正箇所を確認できます。
修正後:
その結果、最初に設定されるローカル管理者のパスワードは、端末の初回起動時にOOBEによって作成され、安全な形で管理されると考えられます。
技術的詳細(OOBE側)
以下は、我々がUserOOBE(Windows 11)と呼ぶ例です。
この画面はWindowsインストール直後に表示され、ユーザーがアカウントを設定したり、他の設定を適用したりすることができます。
これはexplorer.exeプロセス内でホストされ、UserOOBE.dll
ライブラリによって実装されています。すべてがdefaultuser0アカウントによって行われます。
注:本調査ではユーザーがローカルアカウントを作成する方式のみを検証しています。Microsoft 365アカウントによって作成する方式は対象外でした。
ユーザーがパスワードとセキュリティ質問を入力すると、RuntimeBroker.exeプロセス内に読み込まれているCloudExperienceHostBroker.dll
のCloudExperienceHostBroker::Account::LocalAccountManager::CreateLocalAccountWithRecoveryKindAsync関数にWinRTコールが送信されます。
この関数は、管理者アカウントを作成すると同時に、明確な理由は不明ですが(関連するインターフェイスの1つがIOOBEOneDriveOptinと呼ばれるため、OneDriveアカウント登録と何らかの関係があると推測)、平文パスワードをキャッシュしようとします。
キャッシング処理は、msoobeplugins.dllに実装されたin-proc COMサーバーを呼び出すことで行われます。
PackAuthBufferは、実質的にCRED_PACK_PROTECTED_CREDENTIALSを使用してCredPackAuthentificationBufferW関数を呼び出すだけの関数です。MSRCに報告時点では、この関数は提供された認証情報を暗号化せず、シリアライズされた形式に変換するだけでした。
最後に、CacheAuthBuffer関数を呼び出すことで、結果がレジストリに保存されます。
HKEY_USERS\.DEFAULT\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker\LocalSystemAuthBuffer
ただし、上記で保存されたパスワードの使用箇所を特定できませんでした。
公式の緩和策
すでに一般的なCredUIの修正を示しましたが、これにより今後の認証情報の漏洩が防止されます。しかし、すでにレジストリキーに平文パスワードが保存されているステーションはどうなるのかという疑問が残ります。
ここで新しいOOBE-Maintenance.exeバイナリが役立ちます。
2023年1月のセキュリティアップデートパッケージでは、「\Microsoft\Windows\Registry\OOBE-Maintenance」というスケジュールされたタスクが作成され、このバイナリが1回実行されるようです。
バイナリのロジックはシンプルで、以下のようです。
1. LocalSystemAuthBuffer内のキャッシュされたパスワードをすべて削除
2. DeviceMigitationStatus 1 を保存
3. スケジュールされたタスクOOBE-Maintenanceを削除
パッチが適用されたインストールでは、パスワードはまだそのレジストリキーに保存されていますが、今度は暗号化されており、初めからDeviceMigitationStatus=1の値が付随しています。これにより、暗号化されたパスワードが安全に保管され、漏洩のリスクが軽減されます。
確認方法
- HEX文字列で値を取得する場合:
[string]::join(' ',((Get-Item -Path Registry::HKEY_USERS\.Default\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker).GetValue('LocalSystemAuthBuffer')| ForEach{'{0:x2}' -f $_}))
- ASCII文字列で値を取得する場合:
[System.Text.Encoding]::ASCII.GetString((Get-Item -Path Registry::HKEY_USERS\.Default\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE\Broker).GetValue('LocalSystemAuthBuffer'))
タイムライン
• 2021/12/24 - MSRCに報告
• 2022/3/23 - MSRCから修正が2023年1月11日以降に予定されているとの連絡
• 2023/1/23 - CVE-2023-21726とともに修正のリリース
備考リンク
[1] https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-21726
[2] https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credpackauthenticationbufferw
[3] https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/customize-oobe-in-windows-11
[4] https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectmemory
[5] https://github.com/eleemosynator/writeups/blob/master/flare-on-6/12%20-%20help/readme.md#7-the-shortening-of-the-way