
EDRSandBlast は、C で書かれたツールであり、脆弱性のある署名済みドライバを悪用して、EDR の検出(Notify Routine コールバック、Object Callbacks、ETW TI プロバイダー)や LSASS の保護をバイパスします。また、ユーザーランド監視を回避するための複数のユーザーランドアンフック技術も実装されています。
リリース時点では、ユーザーランド(--usermode)とカーネルランド(--kernelmode)の技術を組み合わせて、EDR の監視下で LSASS メモリをダンプし、ブロックされることなく、また製品(クラウド)コンソールに「OS Credential Dumping」関連のイベントを生成することもありませんでした。テストは 3 種類の異なる EDR 製品に対して実施され、それぞれ成功しました。
EDR 製品は、Windows のカーネル「Notify Routines」コールバックを使用して、プロセスやスレッドの作成、イメージ(exe / DLL)のロードなど、システムアクティビティの通知をカーネルから受け取ります。
これらのカーネルコールバックは、カーネルランドから定義され、通常はコールバックを実装するドライバが、文書化されたいくつかの API(nt!PsSetCreateProcessNotifyRoutine、nt!PsSetCreateThreadNotifyRoutine など)を使用して定義します。これらの API は、ドライバが提供するコールバックルーチンを、カーネル空間内の文書化されていないルーチンの配列に追加します:
PspCreateProcessNotifyRoutine(プロセス作成用)PspCreateThreadNotifyRoutine(スレッド作成用)PspLoadImageNotifyRoutine(イメージロード用)EDRSandBlast は、これらの配列で定義されたルーチンを列挙し、事前定義された EDR ドライバのリスト(1000 以上のセキュリティ製品のドライバに対応、EDR ドライバ検出セクション を参照)に関連するコールバックルーチンをすべて削除します。この列挙と削除は、脆弱性のあるドライバの悪用によって提供される任意のカーネルメモリ読み取り/書き込みプリミティブの悪用を通じて可能になります(脆弱性のあるドライバセクション を参照)。
前述の配列のオフセットは、複数の技術を使用して復元されます。詳細は オフセットセクション を参照してください。
EDR(および EPP)製品は、多くの場合、nt!ObRegisterCallbacks カーネル API を使用して「オブジェクトコールバック」を登録します。これらのコールバックにより、セキュリティ製品は、特定のオブジェクトタイプ(Windows ではプロセス、スレッド、デスクトップに関連するオブジェクトコールバックがサポートされています)に対するハンドル生成のたびに通知を受け取ることができます。ハンドル生成は、オブジェクトのオープン(OpenProcess、OpenThread などの呼び出し)だけでなく、ハンドルの複製(DuplicateHandle などの呼び出し)でも発生する可能性があります。
これらの各操作についてカーネルから通知を受けることで、セキュリティ製品はハンドル作成の正当性を分析し((例:未知のプロセスが LSASS を開こうとしている))、脅威が検出された場合はブロックすることもできます。
ObRegisterCallbacks を使用したコールバック登録のたびに、コールバックの影響を受けるオブジェクトのタイプ(プロセス、スレッド、デスクトップのいずれか)を記述する _OBJECT_TYPE オブジェクト内に存在する二重リンクリスト CallbackList に新しい項目が追加されます。残念ながら、これらの項目は Microsoft によって文書化されておらず、シンボルファイルにも公開されていない構造体で記述されています。しかし、さまざまな ntoskrnl.exe バージョンから調査したところ、この構造体は(少なくとも)Windows 10 ビルド 10240 から 22000(2015 年から 2022 年)の間で変更されていないようです。
言及された、オブジェクトコールバック登録を表す構造体は次のとおりです:```C typedef struct OB_CALLBACK_ENTRY_t { LIST_ENTRY CallbackList; // linked element tied to _OBJECT_TYPE.CallbackList OB_OPERATION Operations; // bitfield : 1 for Creations, 2 for Duplications BOOL Enabled; // self-explanatory OB_CALLBACK* Entry; // points to the structure in which it is included POBJECT_TYPE ObjectType; // points to the object type affected by the callback POB_PRE_OPERATION_CALLBACK PreOperation; // callback function called before each handle operation POB_POST_OPERATION_CALLBACK PostOperation; // callback function called after each handle operation KSPIN_LOCK Lock; // lock object used for synchronization } OB_CALLBACK_ENTRY;
上記の`OB_CALLBACK`構造体も未文書化であり、以下のように定義されています:```C
typedef struct OB_CALLBACK_t {
USHORT Version; // usually 0x100
USHORT OperationRegistrationCount; // number of registered callbacks
PVOID RegistrationContext; // arbitrary data passed at registration time
UNICODE_STRING AltitudeString; // used to determine callbacks order
struct OB_CALLBACK_ENTRY_t EntryItems[1]; // array of OperationRegistrationCount items
WCHAR AltitudeBuffer[1]; // is AltitudeString.MaximumLength bytes long, and pointed by AltitudeString.Buffer
} OB_CALLBACK;
EDRに登録されたオブジェクトコールバックを無効にするために、EDRSandblastには3つの手法が実装されていますが、現時点では1つだけが有効になっています。
OB_CALLBACK_ENTRY の Enabled フィールドを使用するこれはEDRSandblastでデフォルトで有効になっている手法です。EDR関連のオブジェクトコールバックを検出して無効にするために、ProcessおよびThreadタイプに関連付けられた_OBJECT_TYPEオブジェクト内にあるCallbackListリストを参照します。両方の_OBJECT_TYPEは、カーネル内の公開グローバルシンボルPsProcessTypeとPsThreadTypeによって指されています。
リストの各項目は、上記で説明したOB_CALLBACK_ENTRY構造体に適合すると想定されています(少なくとも執筆時点のすべてのWindows 10ビルドで成立すると思われる仮定)。PreOperationフィールドとPostOperationフィールドで定義された関数が、EDRドライバに属するかどうかをチェックするために特定され、該当する場合は、Enabledフラグを切り替えるだけでコールバックが無効になります。
かなり安全な手法ですが、文書化されていない構造に依存するという不便さがあります。この構造の安全でない操作のリスクを軽減するために、いくつかのフィールドが期待される値を持っていることを検証する基本的なチェックが実行されます:
Enabled は TRUE または FALSE のいずれかである(笑わないでください。BOOL は int なので、1 や 0 以外の値になる可能性があります)。Operations は OB_OPERATION_HANDLE_CREATE、OB_OPERATION_HANDLE_DUPLICATE、またはその両方である。ObjectType は PsProcessType または PsThreadType を指す。CallbackListをアンリンクする文書化されていない構造に依存しない別の戦略(したがって理論的にはNTカーネルの変更に対してより堅牢)は、プロセスとスレッドの両方に対するCallbackList全体をアンリンクすることです。_OBJECT_TYPEオブジェクトは以下の通りです:```C
struct _OBJECT_TYPE {
LIST_ENTRY TypeList;
UNICODE_STRING Name;
[...]
_OBJECT_TYPE_INITIALIZER TypeInfo;
[...]
LIST_ENTRY CallbackList;
}
Making the `Flink` and `Blink` pointers of the `CallbackList` `LIST_ENTRY` point to
the `LIST_ENTRY` itself effectively make the list empty. Since the `_OBJECT_TYPE` structure
is published in the kernel' symbols, the technique does not rely on hardcoded offsets/structures.
However, it has some drawbacks.
The first being not able to only disable callbacks from EDR; indeed, the technique affects
all object callbacks that could have been registered by "legitimate" software. It should
nevertheless be noted that object callbacks are not used by any pre-installed component
on Windows 10 (at the time of writing) so disabling them should not affect the machine
stability (even more so if the disabling is only temporary).
The second drawback is that process or thread handle operation are really frequent (nearly
continuous) in the normal functioning of the OS. As such, if the kernel write primitive used
cannot perform a `QWORD` write "atomically", there is a good chance that the
`_OBJECT_TYPE.CallbackList.Flink` pointer will be accessed by the kernel in the middle
of its overwriting. For instance, the MSI vulnerable driver `RTCore64.sys` can only perform
a `DWORD` write at a time, so 2 distinct IOCTLs will be needed to overwrite the pointer, between
which the kernel has a high probability of using it (resulting in a crash). On the other hand,
the vulnerable DELL driver `DBUtil_2_3.sys` can perform writes of arbitrary sizes in one
IOCTL, so using this method with it does not risk causing a crash.
#### Disabling object callbacks altogether
One last technique we found was to disable entirely the object callbacks support for thread
and processes. Inside the `_OBJECT_TYPE` structure corresponding to the process and
thread types resides a `TypeInfo` field, following the documented `_OBJECT_TYPE_INITIALIZER`
structure. The latter contains a `ObjectTypeFlags` bit field, whose `SupportsObjectCallbacks`
flag determines if the described object type (Process, Thread, Desktop, Token, File, etc.)
supports object callback registering or not. As previously stated, only Process, Thread and
Desktop object types supports these callbacks on a Windows installation at the time of writing.
Since the `SupportsObjectCallbacks` bit is checked by `ObpCreateHandle` or
`ObDuplicateObject` before even reading the `CallbackList` (and before executing
callbacks, of course), flipping the bit at kernel runtime effectively disable all object callbacks
execution.
The main drawback of the method is simply that *KPP* ("*PatchGuard*") monitors the integrity
of some (all ?) `_OBJECT_TYPE` structures, and triggers a [`0x109 Bug Check`](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-0x109---critical-structure-corruption)
with parameter 4 being equal to `0x8`, meaning an object type structure has been altered.
However, performing the disabling / re-enabling (and "malicious" action in-between) quickly
enough should be enough to "race" *PatchGuard* (unless you are unlucky and a periodic
check is performed just at the wrong moment).