デバッガーからアプリケーションのメインスレッドを切断し、CreateFile()のキャプチャを回避します。

アプリケーションの調査を複雑にする標準的な方法の1つは、API関数の実行をエミュレートすることです。



特定のケースを考えてみましょう。

たとえば、アプリケーションのチェックサムをチェックするために自分自身を開くなど、アプリケーションの重要な場所を決定する必要がある場合、BPはAPIでCreateFile()関数を設定し、そこで実行可能ファイルへのパスを持つ入力パラメーターを待機し、その後デバッガーに移動しますこの関数を引き起こしたコード。分析に直接進みます。



悪名高いMark RussinovichとBruce Cogswellの著者であるProcess Monitorユーティリティを使用すると、さらに簡単に実行できます。



このユーティリティは、関心のあるリターンアドレスを含む完全なコールスタックを完全に冷静に表示します。



画像



あとは、デバッガを起動して、BPを目的のアドレスに設定するだけです。



画像



確かに、このユーティリティは直接関数呼び出しのアドレスではなく、戻りアドレスをすでに表示しています。 しかし、それについては後で。



アプリケーションの本体がすでに変更されていると想像してください。 アプリケーションのチェックサム検証をバイパスする最も簡単な解決策は、キャプチャされたCreateFile()関数のlpFileNameパラメーターを、変更されていないアプリケーション本体へのパスに置き換えることです。 この操作の後、チェックサム計算メカニズムを検討する必要さえありません。そこで適用されるもの、デジタル署名検証、MD5ハッシュ、または通常のCRC32は関係ありません。 なぜなら このアルゴリズムは、元のアプリケーションの本体で機能します-すべてのチェックに成功します。



そのため、タスクは最初の近似値を次のように調べます。呼び出された関数のパラメーターを変更することをできる限り難しくするためです。 この場合、ヒンジ付き保護を使用しても保存されません。 その結果、CreateFile()関数の呼び出しが常に行われますが、その本体は仮想マシンの保護下に置くのが難しく、穏やかに置くことができます(ここでは微妙な違いにより、一部のヒンジ付き保護はこの呼び出しを独自にエミュレートできますが、今ではそうではありません)。



CreateFile()の回避策の1つは、kernel32-> kernelbase-> ntdllをバイパスして、対応するカーネル関数を直接呼び出すことです。 そのような呼び出しの結果は写真で見ることができます:



画像



直接の呼び出しでは、BPを機能にインストールすることについてはもう話していません。 単にそのような挑戦はありません。 はい、電話後もまだ返信先住所がありますが、ニュアンスは、現代のヒンジ付きプロテクターの大部分がコードを非常に知的に汚すことであり、私たちの手に返信電話コードが存在しても、電話を変更するために電話の場所を決定できるわけではありません別のパラメーター。



このアルゴリズムを実装するために、このコード(delphi7 + Windows 7 32bit)を呼び出したときに何が起こるかを考えてみましょう。



hFile := CreateFile(PChar(ParamStr(0)), GENERIC_READ, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
      
      







1.インポートテーブルに移動します

2. kernel32.dllライブラリのCreateFileA()関数への制御の転送

3. kernel32.dllライブラリのCreateFileW()関数への制御の転送

4. KernelBase.dllライブラリのCreateFileW()関数への制御の転送

5. ntdll.dllライブラリのZwCreateFile()関数への制御権の移動

6.カーネルへの制御の移行



最初の5つのポイントから離れて、ポイント番号6を直接実行します。

このため、ntdll.dllライブラリのZwCreateFile関数の実装、関数自体のパラメーターの説明、および小さな教育プログラムが役立ちます:)



さまざまなシステムでの3番目のリング(UserMode)のZwCreateFileの実装を検討してください。

MOV命令のマシンコードに注意してください。



Windows Vista-32ビット(6.0.6002.18005)

 .text:77F343D4 public ZwCreateFile .text:77F343D4 ZwCreateFile proc near .text:77F343D4 B8 3C 00 00 00 mov eax, 3Ch .text:77F343D9 BA 00 03 FE 7F mov edx, 7FFE0300h .text:77F343DE FF 12 call dword ptr [edx] .text:77F343E0 C2 2C 00 retn 2Ch .text:77F343E0 ZwCreateFile endp
      
      







Windows 7-32ビット(6.1.7601.17725)

 .text:77F055C8 public ZwCreateFile .text:77F055C8 ZwCreateFile proc near .text:77F055C8 B8 42 00 00 00 mov eax, 42h .text:77F055CD BA 00 03 FE 7F mov edx, 7FFE0300h .text:77F055D2 FF 12 call dword ptr [edx] .text:77F055D4 C2 2C 00 retn 2Ch .text:77F055D4 ZwCreateFile endp
      
      







Windows 8-32ビット(6.2.8400.0)

 .text:6A21629C public ZwCreateFile .text:6A21629C ZwCreateFile proc near .text:6A21629C B8 64 01 00 00 mov eax, 164h .text:6A2162A1 E8 03 00 00 00 call sub_6A2162A9 .text:6A2162A6 C2 2C 00 retn 2Ch .text:6A2162A6 ZwCreateFile endp
      
      







ご覧のとおり、違いはごくわずかです。 注意する価値があるのは、特定の番号がEAXレジスタに入力され、特定の関数が呼び出されることです。 この関数はKiFastSystemCallと呼ばれ、次のようになります(OSによって異なります)。



 mov edx, esp sysenter ret
      
      







SYSENTERの代わりに、INT 0x2Eの呼び出しがあるかもしれませんが、これはもはや必須ではありません。

64ビットシステムでのこの関数の実装はわずかに異なります。



Windows 8-64ビット、32ビットntdll(6.2.8400.0)

 .text:6B2BF470 public ZwCreateFile .text:6B2BF470 ZwCreateFile proc near .text:6B2BF470 B8 53 00 00 00 mov eax, 53h .text:6B2BF475 64 FF 15 C0 00 00 00 call large dword ptr fs:0C0h .text:6B2BF47C C2 2C 00 retn 2Ch .text:6B2BF47C ZwCreateFile endp
      
      







なぜなら 32ビットコードがあり、システムは64ビットであり、FSゲートウェイはここで既に呼び出されています:0C0h。これは最終的に次のようなネイティブ64ビット関数の実行に渡されます。



Windows 8-64ビット、64ビットntdll(6.2.8400.0)

 .text:0000000180003110 public NtOpenFile .text:0000000180003110 NtOpenFile proc near .text:0000000180003110 4C 8B D1 mov r10, rcx .text:0000000180003113 B8 31 00 00 00 mov eax, 31h .text:0000000180003118 0F 05 syscall .text:000000018000311A C3 retn .text:000000018000311A NtOpenFile endp
      
      







しかし、この微妙な違いにもかかわらず、この場合でもEAXレジスタは初期化されます。



EAXに配置される番号は、KeServiceDescriptorTableテーブルからのインデックスです。このテーブルを介して、カーネルは特定の時間に呼び出す必要がある関数を決定します。 これらのインデックスはNTDLLコードに直接埋め込まれ、バージョンによって異なります(マイナーパッチの結果としてもテーブルの変更が発生する可能性があるため)。動的に取得する方法を学ぶ必要があります。



次の関数がこれに役立ちます。

 type //  STD  TSDTIndex = ( sdtNtSetInformationThread, sdtZwOpenFile, sdtNtQueryObject, WOW64ReservedAddr); var FunctionSDTIndex: array [TSDTIndex] of DWORD = (0, 0, 0, 0); procedure InitSDTTable; const //  ,      ApiNames: array [TSDTIndex] of string = ( 'NtSetInformationThread', 'ZwOpenFile', 'NtQueryObject', '' ); const KSEG0_BASE = $80000000; MM_HIGHEST_USER_ADDRESS = $7FFEFFFF; MM_USER_PROBE_ADDRESS = $7FFF0000; MM_SYSTEM_RANGE_START = KSEG0_BASE; MustWrite = PAGE_READWRITE or PAGE_WRITECOPY or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY; OBJ_CASE_INSENSITIVE = $00000040; FILE_SYNCHRONOUS_IO_NONALERT = $00000020; FILE_READ_DATA = 1; var pSectionAddr, dwLength: DWORD; lpBuffer: TMemoryBasicInformation; pNtHeaders: PImageNtHeaders; ExportAddr: TImageDataDirectory; ProcessExport: Boolean; ImageBase: DWORD; IED: PImageExportDirectory; I: Integer; FuntionAddr: Pointer; NamesCursor: PDWORD; OrdinalCursor: PWORD; Ordinal: DWORD; CurrentFuncName: string; SDT: TSDTIndex; begin //    ,    NTDLL pSectionAddr := GetModuleHandle('ntdll.dll'); ImageBase := 0; ExportAddr.VirtualAddress := 0; ExportAddr.Size := 0; dwLength := SizeOf(TMemoryBasicInformation); //  WOW ,     , //      sysenter //  32-     asm push eax mov eax, fs:[$c0] mov I, eax pop eax end; FunctionSDTIndex[WOW64ReservedAddr] := I; _Write(Format('WOW64Reserved: %d', [FunctionSDTIndex[WOW64ReservedAddr]])); //      while pSectionAddr < MM_USER_PROBE_ADDRESS do begin //     if VirtualQuery(Pointer(pSectionAddr), lpBuffer, dwLength) <> dwLength then RaiseLastOSError; try //     -   if (lpBuffer.State = MEM_FREE) or (lpBuffer.State = MEM_RESERVE) then Continue; //    -   if (lpBuffer.Protect and PAGE_GUARD) = PAGE_GUARD then Continue; if (lpBuffer.Protect and PAGE_NOACCESS) = PAGE_NOACCESS then Continue; _Write(Format(' : %x', [pSectionAddr])); //  -       ? if PWord(lpBuffer.BaseAddress)^ = IMAGE_DOS_SIGNATURE then begin //     pNtHeaders := Pointer(Integer(lpBuffer.BaseAddress) + PImageDosHeader(lpBuffer.BaseAddress)^._lfanew); ExportAddr.VirtualAddress := 0; ExportAddr.Size := 0; ImageBase := DWORD(lpBuffer.BaseAddress); if (pNtHeaders^.Signature = IMAGE_NT_SIGNATURE) and (pNtHeaders^.FileHeader.Machine = IMAGE_FILE_MACHINE_I386) then begin _Write(' PE .'); //   -      ExportAddr := pNtHeaders.OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT]; if ExportAddr.VirtualAddress <> 0 then Inc(ExportAddr.VirtualAddress, ImageBase) else ExportAddr.Size := 0; end; _Write(Format('  : %x', [ExportAddr.VirtualAddress])); _Write(Format('  : %x', [ExportAddr.Size])); end; // ,         ProcessExport := False; if ExportAddr.Size <> 0 then if ExportAddr.VirtualAddress >= DWORD(lpBuffer.BaseAddress) then ProcessExport := ExportAddr.VirtualAddress + ExportAddr.Size < DWORD(lpBuffer.BaseAddress) + lpBuffer.RegionSize; //    -   if ProcessExport then begin if (ImageBase = 0) or (ExportAddr.VirtualAddress = 0) then Exit; IED := PImageExportDirectory(ExportAddr.VirtualAddress); _Write(Format(' : %s', [string(PAnsiChar(ImageBase + IED^.Name))])); // ,     ? if LowerCase(string(PAnsiChar(ImageBase + IED^.Name))) = 'ntdll.dll' then begin _Write('  '); // ,   ,      I := 1; NamesCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNames)); OrdinalCursor := Pointer(ImageBase + DWORD(IED^.AddressOfNameOrdinals)); while I < Integer(IED^.NumberOfNames) do begin //       CurrentFuncName := string(PAnsiChar(ImageBase + PDWORD(NamesCursor)^)); for SDT := sdtNtSetInformationThread to sdtNtQueryObject do if ApiNames[SDT] = CurrentFuncName then begin //       Ordinal := OrdinalCursor^ + IED^.Base; //       FuntionAddr := Pointer(ImageBase + DWORD(IED^.AddressOfFunctions)); FuntionAddr := Pointer(ImageBase + PDWORD(DWORD(FuntionAddr) + (Ordinal - 1) * 4)^); //      MOV FuntionAddr := Pointer(DWORD(FuntionAddr) + 1); //  SDT   FunctionSDTIndex[SDT] := PDWORD(FuntionAddr)^; _Write(Format('  %s - SDT  %d', [CurrentFuncName, FunctionSDTIndex[SDT]])); end; Inc(I); Inc(NamesCursor); Inc(OrdinalCursor); end; end; ImageBase := 0; end; // ,     ? if FunctionSDTIndex[sdtNtSetInformationThread] <> 0 then if FunctionSDTIndex[sdtZwOpenFile] <> 0 then if FunctionSDTIndex[sdtNtQueryObject] <> 0 then Exit; finally //    ,    . Inc(pSectionAddr, lpBuffer.RegionSize); end; end; end;
      
      







この関数は、NTDLL.DLLライブラリのロードアドレスを決定し、このライブラリのエクスポートテーブルに移動し、必要な関数(この例ではNtSetInformationThread、ZwOpenFile、NtQueryObjectが考慮されます)に関するエントリを探し、メモリ内の実際のアドレスを決定し、必要な関数のSDTインデックスを読み取ります上記のマシン機能コード。 結果はFunctionSDTIndex配列に配置されます。



例に必要な関数の有効なSDTインデックスができたので、次に呼び出すZwOpenFile自体の宣言を直接調べます。



 NTSTATUS ZwOpenFile( _Out_ PHANDLE FileHandle, _In_ ACCESS_MASK DesiredAccess, _In_ POBJECT_ATTRIBUTES ObjectAttributes, _Out_ PIO_STATUS_BLOCK IoStatusBlock, _In_ ULONG ShareAccess, _In_ ULONG OpenOptions );
      
      







スタックに配置される6つのパラメーター。 1番目と4番目はリンクに続き、3番目のポインターです。 それでは、電話をかけます。

  //      // ZwOpenFile // =========================================================================== _Write('  ZwOpenFile'); _Write('    '); SysCallArgument := FunctionSDTIndex[sdtZwOpenFile]; oa.Length := SizeOf(TObjectAttributes); oa.RootDirectory := 0; oa.ObjectName := @UnicodeStr; oa.Attributes := OBJ_CASE_INSENSITIVE; oa.SecurityDescriptor := nil; oa.SecurityQualityOfService := nil; UnicodeStr.Buffer := StringToOleStr('??' + ParamStr(0)); UnicodeStr.Length := Length(UnicodeStr.Buffer) * SizeOf(WideChar); UnicodeStr.MaximumLength := UnicodeStr.Length + SizeOf(WideChar); asm //    mov SAVED_EBP, ebp mov SAVED_ESP, esp //   //         push FILE_SYNCHRONOUS_IO_NONALERT // OpenOptions push FILE_SHARE_READ + FILE_SHARE_WRITE + FILE_SHARE_DELETE // ShareAccess lea eax, iosb //   OUT  IoStatusBlock push eax //    lea eax, oa // ObjectAttributes  ,   push eax //  -    push FILE_READ_DATA + SYNCHRONIZE // DesiredAccess lea eax, hFile //   OUT  FileHandle push eax //     //       , //     , //          movzx eax, IsWOW64 or eax, eax jz @32Bit //   64-  lea eax, @64bit push eax push eax mov eax, WOW64Addr push eax mov eax, SysCallArgument xor ecx, ecx lea edx, dword ptr ss:[esp+4*3] ret @64bit: add esp, 4 jmp @FINALIZE @32Bit: //   32-  (XP  ) lea eax, @FINALIZE push eax push eax movzx eax, NeedInt2E or eax, eax jnz @NT_CODE mov edx, esp mov eax, SysCallArgument sysenter @NT_CODE: //   W2K   pop eax lea edx, esp + 4 mov eax, SysCallArgument int $2E nop @FINALIZE: //   mov Status, eax //    mov ebp, SAVED_EBP mov esp, SAVED_ESP end; if Status <> 0 then hFile := 0; _Write(Format('  %x', [Status])); _Write(Format(' %d', [hFile]))
      
      



;



実際、タスクは完了しています。



ニュアンス:ご覧のとおり、コードは3つの異なる方法で呼び出すことができます。



SYSENTER、INT2E、およびWOW64レジスタ。 これは、さまざまなオペレーティングシステムの実装の特性とそのビット数によるものです。 2番目のニュアンスは、EBP / ESPレジスタの保存です。 その理由は、64ビットシステムで関数を呼び出した後、スタック上のアライメントが少し異なるため、アプリケーションを破壊しないように強制的に復元するためです。



他の2つの関数への呼び出しはここでは考慮されませんが、要するに-NtSetInformationThreadはアプリケーションのメインスレッドをデバッガから切断します。 呼び出した後、BPをインストールしようとすると、別のエラー計画が作成されます。 たとえば、Delphi 7は次のエラーで応答します。



画像



その後、プロセスを中断して環境を再起動するだけです。



例のソースコードでは、DISABLE_HIDEFROMDEBUGGERディレクティブによってこの呼び出しが無効になっています。その動作を確認する場合は、このディレクティブの宣言をコメントアウトして例を再構築することを避けるためです。



2番目のNtQueryObject関数は、受信したファイルハンドルを操作する方法を示しています(例として)。



デモアプリケーションの結果は次のようになります。



画像



ここで例をご覧ください



あとがきとして。 このアプローチは万能薬ではなく、単にアプリケーションセキュリティを構築するアプローチの1つを示す試みです。 当然、このコードは有能な研究者からあなたを救うものではありません。 この実装オプションを回避する方法は3つあります。



1.ドライバーのパラメーターの置換

2.プロセス環境ブロック内のアプリケーションへのパスの置換

3.同じプロセスモニターに表示される関数の戻りアドレスでコードにアダプターを埋め込むことで、関数の結果を独自の関数に置き換えます。



しかし、新進のソフトウェア研究者を怖がらせることは助けになり、より高度な専門家を困惑させることさえあり、ソースコードの形でプログラムを配布することだけが専門家を救います;)



All Articles