Bypassing PatchGuard

  • windows
  • patchguard

Kernel Patch Protection (KPP), informally known as PatchGuard, is a feature of 64-bit (x64) editions of Microsoft Windows that prevents patching the kernel. It was first introduced in 2005 with the x64 editions of Windows XP and Windows Server 2003 Service Pack 1.

序章

KPP - Kernel Patch ProtectionはWindows XPより搭載された、NTカーネルのセキュリティへの取り組みの一環です。
XP以前の”レガシー”なOSでは、良くも悪くも、OSカーネルのオブジェクトや関数を自由にマニピュレーションできました。

”カーネルを自由にマニピュレーションできる”ことは、セキュリティの観点から見て危険ではないでしょうか?
そこで生まれたのが、Kernel Patch Protection(カーネル パッチ プロテクション)です。別名、Patch Guard (パッチガード)とも呼ばれます。短く簡潔なので、僕はPatchGuardと呼んでいます。

PatchGuardが誕生するまで、アンチウイルス等のセキュリティ製品のカーネルコンポーネントでも、OSカーネルの関数に対してアグレッシブにインラインフックを仕掛けることで検出を強化する例も珍しくありませんでした。
一方で、これはマルウェアにとっても同じことなのです。

マルウェアも同じく、カーネルのマニピュレーションにより大きなアドバンテージを得ることができると言えるでしょう。
KPPは、そういった意味でもモダンなOSのセキュリティを支える大きな柱のひとつとなっています。

NoPatchGuardCallback

NoPatchGuardCallback

This project is to bypass PatchGuard protection against PsSetCreateProcessNotifyRoutine by DKOM - self register arbitrary callback routine by directly manipulating kernel objects.

NoPatchGuardCallbackPsSetCreateProcessNotifyRoutineに対するPatchGuardをバイパスします。

https://github.com/kkent030315/NoPatchGuardCallback

PsSetCreateProcessNotifyRoutine

以前に、Analysis of PsSetCreateProcessNotifyRoutine And PatchGuard - NT Kernelにて、こちらの関数の動作を詳細に追っていますので、ご興味が御座いましたらご覧ください。

簡単に言えば、当関数はプロセスの生成および削除時に呼び出されるコールバックを登録することができる強力なNTカーネルAPIです。
その性質上、悪用された場合に大きなアドバンテージとなってしまうことから、当関数はPatchGuardの監視対象となっています。

PsSetCreateProcessNotifyRoutineExPsSetCreateThreadNotifyRoutinePsSetLoadImageNotifyRoutineも例外ではなく、PatchGuardは主要なカーネルオブジェクトを保護しています。

PatchGuardは当関数が非署名のカーネルアドレス空間から呼び出された場合、即座にソフトウェア割り込みであるKiRaiseSecurityCheckFailureと呼ばれるISRが割り込み、バグチェック0x139を発生させます。

(親の顔よりみた画面)

bugcheck 0x139

コンセプト

NTカーネルには、登録されたコールバックを保持する静的配列が存在します。
PspCreateProcessNotifyRoutineです。

PspCreateProcessNotifyRoutine

配列サイズはMSDNに記載されている通り64です。

For Windows Vista and later versions of Windows, the system can register up to 64 process-creation callback routines.

PspCreateProcessNotifyRoutineMax

実際に、PoCではこちらの配列をDKOM(Direct Kernel Object Maniplation)することで、コールバックをシステムコールを通さず、直接配列に挿入することで、PatchGuardの目をかいくぐっています。
ntoskrnlのベースアドレスからのRVA(Relative Virtual Address)を計算することで、この配列へのポインタを取得できます。

//
// this array (sizeof 64) contains all callback blocks
//
PVOID* PspCreateProcessNotifyRoutine = &*( PVOID* )
    ( ( UINT_PTR )NtosKrnlImageBase + RVA_PSP_CREATE_PROCESS_NOTIFY_ROUTINE );

配列には、Callback Block(コールバックブロック)と呼ばれるブロックが存在します。
ブロックはExAllocateCallBackにより生成されます。ExAllocateCallBackはUndocumentなNTカーネル内部関数です。

本来のシステムコールの動作では、Functionには実際にPspSetCreateProcessNotifyRoutineに渡ったコールバック関数へのポインタが格納され、ContextにはRemoveとして渡された値が格納されます。

typedef struct _CALLBACK_ROUTINE_BLOCK
{
    EX_RUNDOWN_REF RundownProtect;
    PEX_CALLBACK_FUNCTION Function;
    PVOID Context;
} CALLBACK_ROUTINE_BLOCK, * PCALLBACK_ROUTINE_BLOCK;

Callback Block

ExCompareExchangeCallBackでコールバックブロックを配列に挿入します。こちらも同じく、UndocumentなNTカーネル内部関数です。

配列に挿入できた場合、配列と同じく静的なPspCreateProcessNotifyRoutineCountと呼ばれるカウンタの値をインクリメントします。以下、PoC内でntoskrnlからのRVAからアドレスを算出し、インクリメントしている様子です。なかなかひどい式ですが、ご容赦下さい。

//
// increment count
//
InterlockedIncrement( ( volatile LONG* )
    &*( UINT32* )
        ( ( UINT_PTR )NtosKrnlImageBase + RVA_PSP_CREATE_PROCESS_NOTIFY_ROUTINE_COUNT ) );

画像では、実際にシステムコール関数で削除オペレーション時に値をデクリメントしている様子が伺えます。

PspCreateProcessNotifyRoutineCount

PoC - Proof of Concept

PoC

フルコードは下記Githubリポジトリよりご覧いただけます。

https://github.com/kkent030315/NoPatchGuardCallback

※注)当PoCではコールバックの削除を実装していません。システムクラッシュを発生させる可能性が高いので、実践される際は専用のVMをご用意下さい。

Reference