独自のサンドボックスの作成方法:シンプルなサンドボックスの例。 パートII

画像



記事の最初の部分では 、特権モードのドライバーについて簡単に説明しました。 サンドボックスを掘り下げましょう。



サンドボックスの例:ドライバーのビルドとインストール



サンドボックスの中核は、ミニフィルタードライバーです。 ソースコードは、src \ FSSDK \ Kernel \ miniltにあります。 WDK 7.xキットを使用してドライバーをビルドしていると思います。 これを行うには、適切な環境、たとえばWin 7 x86チェックを実行し、ソースディレクトリに移動する必要があります。 開発環境で起動するときにコマンドラインで「build / c」と書くだけで、ビルドされたドライバーを取得できます。 ドライバーをインストールするには、* .infファイルを* .sysファイルを含むフォルダーにコピーし、Windowsエクスプローラーを使用してこのディレクトリに移動し、* .infファイルのコンテキストメニューを使用して、[インストール]オプションを選択すると、ドライバーがインストールされます。 仮想マシン内ですべての実験を行うことをお勧めします; VMwareは良い選択です。 64ビットバージョンのWindowsでは、署名されていないドライバーは読み込まれません。 VMwareでドライバーを実行できるようにするには、ゲストOSで特権モードデバッガーを有効にする必要があります。 これは、管理者として実行される次のcmdコマンドを実行することで実行できます。



1.bcdedit /デバッグオン

2.bcdedit / bootdebug on



ここで、名前付きパイプをVMwareのシリアルポートとして指定し、マシンにインストールされているWinDBGを使用して構成する必要があります。 その後、デバッガーを使用してVMwareに接続し、ドライバーをデバッグできます。



この記事では、ドライバーのデバッグ用にVMwareを構成する方法について詳しく説明しています。



サンドボックスの例:アーキテクチャの概要



シンプルなサンドボックスは、3つのモジュールで構成されています。



•仮想化プリミティブを実装する特権モードドライバー。

•ユーザーモードサービス。ドライバーからメッセージを受信し、ドライバーから受信した通知の設定を変更することでファイルシステムの動作を変更できます。

•fsproxyミドルウェアライブラリ。サービスがドライバーと通信するのに役立ちます。



特権モードのドライバーを使用した最も単純なサンドボックスの探索を始めましょう。



サンドボックスの例:ドライバーの作成



通常のアプリケーションはWinMain()で起動する傾向がありますが、ドライバーはDriverEntry()関数を使用してこれを行います。 この機能でドライバーの学習を始めましょう。



NTSTATUS DriverEntry ( __in PDRIVER_OBJECT DriverObject, __in PUNICODE_STRING RegistryPath ) { OBJECT_ATTRIBUTES oa; UNICODE_STRING uniString; PSECURITY_DESCRIPTOR sd; NTSTATUS status; UNREFERENCED_PARAMETER( RegistryPath ); ProcessNameOffset = GetProcessNameOffset(); DbgPrint("Loading driver"); // //     // status = FltRegisterFilter( DriverObject, &FilterRegistration, &MfltData.Filter ); if (!NT_SUCCESS( status )) { DbgPrint("RegisterFilter failure 0x%x \n",status); return status; } // //   . // RtlInitUnicodeString( &uniString, ScannerPortName ); // //   ,          . // status = FltBuildDefaultSecurityDescriptor( &sd, FLT_PORT_ALL_ACCESS ); if (NT_SUCCESS( status )) { InitializeObjectAttributes( &oa, &uniString, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, sd ); status = FltCreateCommunicationPort( MfltData.Filter, &MfltData.ServerPort, &oa, NULL, FSPortConnect, FSPortDisconnect, NULL, 1 ); // //      .    //  ,   FltCreateCommunicationPort() // FltFreeSecurityDescriptor( sd ); regCookie.QuadPart = 0; if (NT_SUCCESS( status )) { // //    -. // DbgPrint(" Starting Filtering \n"); status = FltStartFiltering( MfltData.Filter ); if (NT_SUCCESS(status)) { status = PsSetCreateProcessNotifyRoutine(CreateProcessNotify,FALSE); if (NT_SUCCESS(status)) { DbgPrint(" All done! \n"); return STATUS_SUCCESS; } } DbgPrint(" Something went wrong \n"); FltCloseCommunicationPort( MfltData.ServerPort ); } } FltUnregisterFilter( MfltData.Filter ); return status; }
      
      





DriverEntryにはいくつかの重要な機能があります。 まず、この関数はFltRegisterFilter()関数を使用してドライバーをミニフラーとして登録します。



 status = FltRegisterFilter( DriverObject, &FilterRegistration, &MfltData.Filter );
      
      





FilterRegistrationでインターセプトする特定の操作のハンドラーへのポインターの配列であり、登録が成功した場合はMfltData.Filterでフィルターインスタンスを受け取ります。 FilterRegistrationは次のように宣言されます。



 const FLT_REGISTRATION FilterRegistration = { sizeof( FLT_REGISTRATION ), //  FLT_REGISTRATION_VERSION, //  0, //  NULL, //   Callbacks, //  DriverUnload, //   FSInstanceSetup, //    FSQueryTeardown, //     NULL, NULL, FSGenerateFileNameCallback, //    FSNormalizeNameComponentCallback, //    NULL, //    #if FLT_MGR_LONGHORN NULL, //    FSNormalizeNameComponentExCallback, //     #endif // FLT_MGR_LONGHORN };
      
      





ご覧のとおり、構造体にはイベントハンドラー(コールバック)の配列へのポインターが含まれています。 これは、「古い」ドライバーでのディスパッチ手順に類似しています。 さらに、この構造には、他のいくつかの補助関数へのポインターが含まれています-後で説明します。 ここで、Callbacks配列で説明されているハンドラーについて説明します。 それらは次のように定義されます。



 const FLT_OPERATION_REGISTRATION Callbacks[] = { { IRP_MJ_CREATE, 0, FSPreCreate, NULL }, { IRP_MJ_CLEANUP, 0, FSPreCleanup, NULL}, { IRP_MJ_OPERATION_END} };
      
      





MSDNでFLT_OPERATION_REGISTRATION構造の詳細な説明を確認できます。 ドライバーは、IRP_MJ_CREATE要求を受け取るたびに呼び出されるFSPreCreateと、IRP_MJ_CLEANUPを受け取るたびに呼び出されるFSPreCleanupの2つのハンドラーのみを登録します。 この要求は、最後のファイル記述子が閉じるときに届きます。 入力パラメーターを変更し、変更された要求をスタックに送信することができます(そして、そうします)。これにより、下位フィルターとファイルシステムドライバーが変更された要求を受け取ります。 操作の終了時に到着するいわゆる通知後を登録できます。 これを行うには、FSPreCreateへのポインターに続くNULLポインターを、対応するポストハンドラーへのポインターに置き換えることができます。 要素IRP_MJ_OPERATION_ENDで配列を完成させる必要があります。 これは、イベントハンドラーの配列の終わりをマークする「偽の」操作です。 「従来の」フィルタードライバーに対して行う必要があるため、各IRP_MJ_XXX操作に対してハンドラーを提供しないでください。



DriverEntry()が行う2番目の重要なことは、ミニフィルターポートを作成することです。 ユーザーレベルのサービスから通知を送信し、そこから応答を受信するために使用されます。 これは、FltCreateCommunicationPort()操作を使用して行われます。



 status = FltCreateCommunicationPort( MfltData.Filter, &MfltData.ServerPort, &oa, NULL, FSPortConnect, FSPortDisconnect, NULL, 1 );
      
      





ユーザーモードサービスをドライバーに接続および切断するときに、FSPortConnect()およびFSPortDisconnect()関数へのポインターが発生します。



最後に行うことは、フィルタリングを開始することです。



 status = FltStartFiltering( MfltData.Filter );
      
      





FltRegisterFilter()によって返されるフィルターインスタンスへのポインターがこのプロシージャに渡されることに注意してください。 今後、IRP_MJ_CREATEおよびIRP_MJ_CLEANUPリクエストの通知の受信を開始します。 ファイルフィルタリング通知とともに、この関数を使用して新しいプロセスがロードおよびアンロードされるタイミングをOSに通知するよう要求します。



 PsSetCreateProcessNotifyRoutine(CreateProcessNotify,FALSE);
      
      





CreateProcessNotifyは、プロセスの作成と終了の通知のためのハンドラです。



サンドボックスの例:FSPreCreateハンドラー



ここで本当の魔法が生まれます。 この機能の本質は、どのファイルが開かれ、どのプロセスによって開かれたかを報告することです。 このデータは、ユーザーモードサービスに送信されます。 サービス(サービス)は、ファイルへのアクセスを拒否するか、要求を別のファイルにリダイレクトする(これがサンドボックスの実際の動作です)か、操作を実行する許可についてのコマンドの形式で応答を提供します。 この場合に最初に発生するのは、DriverEntry()で作成した通信ポート(通信ポート)を介してユーザーモードサービスとの接続を確認することです。接続がない場合、それ以上のアクションは発生しません。 また、サービスが要求のソース(イニシエーター)であるかどうかを確認します。これは、グローバルに割り当てられたMfltData構造のUserProcessフィールドを確認することで行います。 このフィールドは、ユーザーモードサービスがポートに接続するときに呼び出されるPortConnect()ルーチンに入力されます。 また、ページングに関連するリクエストを処理する必要はありません。 これらのすべての場合において、リターンコードFLT_PREOP_SUCCESS_NO_CALLBACKを返します。これは、リクエストの処理を完了しており、操作後ハンドラがないことを意味します。 それ以外の場合、FLT_PREOP_SUCCESS_WITH_CALLBACKを返します。 これが「従来の」ドライバーフィルターである場合、前述のスタックフレーム、IoCallDriverプロシージャなどを処理する必要があります。 ミニフィルターの場合、リクエストの送信は非常に簡単です。



要求を処理する場合、最初に行う必要があるのは、ユーザーモードに転送する構造体MINFILTER_NOTIFICATIONを入力することです。 彼女は完全にカスタマイズ可能です。 操作タイプ(CREATE)、リクエストが実行されたファイルの名前、プロセス識別番号(PID)、およびソースプロセスの名前を渡します。 プロセスの名前を見つける方法に注意を払う価値があります。 実際、これは商用ソフトウェアでの使用が推奨されていないプロセスの名前を取得するための文書化されていない方法です。 さらに、これはWindowsのx64バージョンでは機能しません。 商用ソフトウェアでは、プロセスID(ユーザーID)のみをユーザーモードに渡します。実行可能ファイル名が必要な場合は、ユーザーモードAPIを使用して取得できます。 たとえば、OpenProcess()APIを使用してそのIDでプロセスを開き、GetProcessImageFileName()APIを呼び出して実行可能ファイルの名前を取得できます。 しかし、サンドボックスを簡素化するために、PEPROCESS構造体のドキュメント化されていないフィールドからプロセス名を取得します。 名前のオフセット(相対アドレス)を見つけるために、システムに「SYSTEM」というプロセスがあることを考慮します。 PEPROCESS構造に指定された名前を含むプロセスをスキャンし、他のプロセスを分析するときに検出された名前オフセットを使用します。 詳細については、SetProcessName()関数を参照してください。



FltGetFileNameInformation()およびFltParseFileNameInformation()の2つの関数を使用して、リクエストを受信した「ターゲット」ファイル(ファイルを開くリクエストなど)からファイル名を取得します。



MINFILTER_NOTIFICATION構造体を埋めた後、ユーザーモードに送信します。



 Status = FltSendMessage( MfltData.Filter, &MfltData.ClientPort, notification, sizeof(MINFILTER_NOTIFICATION), &reply, &replyLength, NULL );
      
      





そして、変数replyで答えを取得します。 操作をキャンセルするように求められた場合、アクションは簡単です:



 if (!reply.bAllow) { Data->IoStatus.Status = STATUS_ACCESS_DENIED; Data->IoStatus.Information = 0; return FLT_PREOP_COMPLETE; }
      
      





ここでのキーポイントは次のとおりです。最初に、FLT_PREOP_COMPLETEを返すことによって戻りコードを変更します。 これは、たとえば、IoCallDriver()を呼び出さずに「従来の」ドライバーからIoCompleteRequest()を呼び出す場合のように、ドライバースタックに要求を渡さないことを意味します。 次に、リクエスト構造のIoStatusフィールドに入力します。 エラーコードSTATUS_ACCESS_DENIEDおよび情報フィールドを「ゼロ」に設定します。 原則として、情報フィールドには、操作中に転送されたバイト数が含まれます。たとえば、コピーされたバイト数はコピー操作中に記録されます。



操作をリダイレクトしたい場合、見た目が異なります:



 if (reply.bSupersedeFile) { //    //   : \Device\HardDiskVolume1\Windows\File, //  \DosDevices\C:\Windows\File OR \??\C:\Windows\File  C:\Windows\File RtlZeroMemory(wszTemp,MAX_STRING*sizeof(WCHAR)); // \Device\HardDiskvol\file  \DosDevice\C:\file int endIndex = 0; int nSlash = 0; //    int len = wcslen(reply.wsFileName); while (nSlash < 3 ) { if (endIndex == len ) break; if (reply.wsFileName[endIndex]==L'\\') nSlash++; endIndex++; } endIndex--; if (nSlash != 3) return FLT_PREOP_SUCCESS_NO_CALLBACK; //     WCHAR savedch = reply.wsFileName[endIndex]; reply.wsFileName[endIndex] = UNICODE_NULL; RtlInitUnicodeString(&uniFileName,reply.wsFileName); HANDLE h; PFILE_OBJECT pFileObject; reply.wsFileName[endIndex] = savedch; NTSTATUS Status = RtlStringCchCopyW(wszTemp,MAX_STRING,reply.wsFileName + endIndex ); RtlInitUnicodeString(&uniFileName,wszTemp); Status = IoReplaceFileObjectName(Data->Iopb->TargetFileObject, reply.wsFileName, wcslen(reply.wsFileName)*sizeof(wchar_t)); Data->IoStatus.Status = STATUS_REPARSE; Data->IoStatus.Information = IO_REPARSE; FltSetCallbackDataDirty(Data); return FLT_PREOP_COMPLETE; }
      
      





ここでのキーは、IoReplaceFileObjectNameを呼び出すことです



  Status = IoReplaceFileObjectName(Data->Iopb->TargetFileObject, reply.wsFileName, wcslen(reply.wsFileName)*sizeof(wchar_t));
      
      





この関数は、転送されたファイルオブジェクト(FILE_OBJECT)のファイル名を変更します-オープンファイルであるI / Oマネージャーのオブジェクト。 マニュアル名は次のように置き換えられます。名前を含むフィールドでメモリを解放し、バッファを割り当ててそこに新しい名前をコピーします。 ただし、Windows 7でIoReplaceFileObjectName関数が登場したため、バッファーの代わりに使用することを強くお勧めします。 著者の個人プロジェクト(Cyber​​genic Shade Sandbox製品)は、XPからWindows 10までのすべてのオペレーティングシステムと互換性があり、ドライバーが古いOS(Win 7より前)で実行されている場合、手動でバッファーを使用します。 ファイル名を変更した後、データに特別なステータスSTATUS_REPARSEを入力し、情報フィールドに値IO_REPARSEを入力します。 さらに、ステータスFLT_PREOP_COMPLETEを返します。 REPARSEは、アプリケーション(要求のイニシエーター)が最初に新しい名前でファイルを開くことを要求する場合のように、I / Oマネージャーに元の要求を(新しいパラメーターで)再起動することを意味します。 FltSetCallbackDataDirty()も呼び出す必要があります。このAPI関数は、IoStatusも変更する場合を除き、データ構造を変更するたびに必要です。 実際、ここではIoStatusを実際に変更しているので、この関数を呼び出して、これらの変更をI / Oマネージャーに通知したことを確認します。



サンドボックスの例:名前プロバイダー



ファイル名を変更するため、ドライバーには、ファイル名が要求されたとき、またはファイル名が「正規化」されたときに呼び出される名前プロバイダーハンドラーの実装が含まれている必要があります。 これらのハンドラーは、FSGenerateFileNameCallbackおよびFSNormalizeNameComponentCallback(Ex)です。



私たちの仮想化方法は、IRP_MJ_CREATEリクエストの「再起動」に基づいており(仮想化された名前はREPARSE_POINTSであると想定しています)、これらのハンドラーの実装は非常に簡単です



ユーザーモードサービス



ユーザーモードはファイルウォールプロジェクトにあり(記事のソースコードを参照)、ドライバーと通信します。 主な機能は、次の機能によって表されます。



 bool CService::FS_Emulate( MINFILTER_NOTIFICATION* pNotification, MINFILTER_REPLY* pReply, const CRule& rule) { using namespace std; //    // ,   ,   - / if(IsSandboxedFile(ToDos(pNotificationwsFileName).c_str(),rule.SandBoxRoot)) { pReply->bSupersedeFile = FALSE; pReply->bAllow = TRUE; return true; } wchar_t* originalPath = pNotification->wsFileName; //   int iLen = GetNativeDeviceNameLen(originalPath); wstring relativePath; for (int i = iLen ; i < wcslen(originalPath); i++) relativePath += originalPath[i]; wstring substitutedPath = ToNative(rule.SandBoxRoot) + relativePath; if (PathFileExists(ToDos(originalPath).c_str())) { if (PathIsDirectory(ToDos(originalPath).c_str()) ) { //   –     CreateComplexDirectory(ToDos(substitutedPath).c_str() ); } else { //    –      (sandbox),     wstring path = ToDos(substitutedPath); wchar_t* pFileName = PathFindFileName(path.c_str()); int iFilePos = pFileName - path.c_str(); wstring Dir; for (int i = 0; i< iFilePos-1; i++) Dir = Dir + path[i]; CreateComplexDirectory(ToDos(Dir).c_str()); CopyFile(ToDos(originalPath).c_str(),path.c_str(),TRUE); } } else { //   ,     ,    wstring path = ToDos(substitutedPath); wchar_t* pFileName = PathFindFileName(path.c_str()); int iFilePos = pFileName - path.c_str(); wstring Dir; for (int i = 0; i< iFilePos-1; i++) Dir = Dir + path[i]; CreateComplexDirectory(ToDos(Dir).c_str()); } wcscpy(pReply->wsFileName,substitutedPath.c_str()); pReply->bSupersedeFile = TRUE; pReply->bAllow = TRUE; return true; }
      
      





ドライバーがファイル名のリダイレクトを決定したときに呼び出されます。 ここでのアルゴリズムは非常に単純です。サンドボックスに配置されたファイルが既に存在する場合、リクエストは単にリダイレクトされ、pReply変数に新しいファイル名(サンドボックスフォルダー内の名前)が入力されます。 そのようなファイルが存在しない場合、元のファイルがコピーされ、その後、元のリクエストが変更されて、新しいコピーされたファイルを示します。 サービスは、特定のプロセスにリクエストをリダイレクトする必要があることをどのように知るのですか? これはルールを使用して行われます-CRuleクラスの実装を参照してください。 ルール(通常、デモサービスの唯一のルール)はLoadRules()関数でロードされます。



 bool CService::LoadRules() { CRule rule; ZeroMemory(&rule, sizeof(rule)); rule.dwAction = emulate; wcscpy(rule.ImageName,L"cmd.exe"); rule.GenericNotification.iComponent = COM_FILE; rule.GenericNotification.Operation = CREATE; wcscpy(rule.GenericNotification.wsFileName,L"\\Device\\Harddisk*\\*.txt"); wcscpy(rule.SandBoxRoot,L"C:\\Sandbox"); GetRuleManager()->AddRule(rule); return true; }
      
      





この関数は、「cmd.exe」と呼ばれるプロセスのルールを作成し、* .txtファイルを含むすべての操作をサンドボックスにリダイレクトします。 サービスが実行されているPCでcmd.exeを実行すると、サンドボックス内の操作が分離されます。 たとえば、cmd.exeからtxtファイルを作成できます。たとえば、「dir> files.txt」コマンドを実行すると、files.txtはC:/sandbox//files.txtに作成されます。cmd.exeの現在のディレクトリです。 cmd.exeから既存のファイルを追加すると、2つのコピーが取得されます。元のファイルシステムの変更されていないバージョンと、C:/ Sandboxの変更されたバージョンです。



おわりに



この記事では、サンドボックスを作成する主な側面に注目しました。 ただし、一部の詳細と問題は影響を受けていません。



たとえば、ユーザーモードからルールを管理することはできません。これにより、PCの速度が著しく低下するためです。 このアプローチは、実装の点では非常に単純であり、教育目的で使用できますが、商用ソフトウェアで使用することはできません。



もう1つの制限は、ファイル名用に事前定義されたバッファを持つ通知/応答構造です。 これらのバッファには2つの欠点があります。まず、サイズが制限され、ファイルシステムの奥深くにある一部のファイルが正しく処理されません。 第二に、ファイル名で割り当てられたカーネルモードメモリの大部分は、ほとんどの場合使用されません。 したがって、商用ソフトウェアでは、より合理的なメモリ割り当て戦略を使用する必要があります。



もう1つの欠点は、FltSendMessage()関数が広く使用されていることです。これはかなり遅いです。 ユーザーモードアプリケーションがユーザーに要求を表示し、ユーザーが操作を許可または拒否する必要がある場合にのみ使用してください。 この場合、人との対話はコードの実行よりもはるかに遅いため、この関数を使用できます。 ただし、プログラムが自動的に応答する場合は、ユーザーモードコードとのやり取りを避けてください。



ソースへの参照



» オリジナル記事

» 問題のサンドボックスのソースコード



All Articles