unit VfsHooks; (* Description: WinNT code hooks package. *) (***) interface (***) uses Windows, SysUtils, Math, Utils, WinNative, Concur, StrLib, Alg, VfsBase, VfsUtils, VfsPatching, VfsDebug, VfsApiDigger, VfsOpenFiles; (* Installs VFS hooks, if not already installed, in a thread-safe manner *) procedure InstallHooks; (***) implementation (***) var HooksCritSection: Concur.TCritSection; HooksInstalled: boolean = false; NativeNtQueryAttributesFile: WinNative.TNtQueryAttributesFile; NativeNtQueryFullAttributesFile: WinNative.TNtQueryFullAttributesFile; NativeNtOpenFile: WinNative.TNtOpenFile; NativeNtCreateFile: WinNative.TNtCreateFile; NativeNtClose: WinNative.TNtClose; NativeNtQueryDirectoryFile: WinNative.TNtQueryDirectoryFile; (* There is no 100% portable and reliable way to get file path by handle, unless file creation/opening was tracked. Thus we rely heavily on VfsOpenFiles. In Windows access to files in curren directory under relative paths is performed via [hDir, RelPath] pair, thus it's strongly recommended to ensure, that current directory handle is tracked by VfsOpenedFiles. It can be perfomed via SetCurrentDir(GetCurrentDir) after VFS was run *) function GetFilePathByHandle (hFile: THandle): WideString; begin result := VfsOpenFiles.GetOpenedFilePath(hFile); end; (* Returns single absolute path, not dependant on RootDirectory member. '\??\' prefix is always removed, \\.\ and \\?\ paths remain not touched. *) function GetFileObjectPath (ObjectAttributes: POBJECT_ATTRIBUTES): WideString; var FilePath: WideString; DirPath: WideString; begin FilePath := ObjectAttributes.ObjectName.ToWideStr(); result := ''; if FilePath <> '' then begin if FilePath[1] = '\' then begin FilePath := VfsUtils.StripNtAbsPathPrefix(FilePath); end; if ObjectAttributes.RootDirectory <> 0 then begin DirPath := GetFilePathByHandle(ObjectAttributes.RootDirectory); if DirPath <> '' then begin if DirPath[Length(DirPath)] <> '\' then begin result := DirPath + '\' + FilePath; end else begin result := DirPath + FilePath; end; end; end else begin result := FilePath; end; end; // .if end; // .function GetFileObjectPath function Hook_NtQueryAttributesFile (OrigFunc: WinNative.TNtQueryAttributesFile; ObjectAttributes: POBJECT_ATTRIBUTES; FileInformation: PFILE_BASIC_INFORMATION): NTSTATUS; stdcall; var ExpandedPath: WideString; RedirectedPath: WideString; ReplacedObjAttrs: WinNative.TObjectAttributes; FileInfo: TNativeFileInfo; HadTrailingDelim: boolean; begin if VfsDebug.LoggingEnabled then begin WriteLog('NtQueryAttributesFile', Format('Dir: %d. Path: "%s"', [ObjectAttributes.RootDirectory, ObjectAttributes.ObjectName.ToWideStr()])); end; ReplacedObjAttrs := ObjectAttributes^; ReplacedObjAttrs.Length := sizeof(ReplacedObjAttrs); ExpandedPath := GetFileObjectPath(ObjectAttributes); RedirectedPath := ''; if ExpandedPath <> '' then begin RedirectedPath := VfsBase.GetVfsItemRealPath(StrLib.ExcludeTrailingDelimW(ExpandedPath, @HadTrailingDelim), @FileInfo); end; // Return cached VFS file info if RedirectedPath <> '' then begin if not HadTrailingDelim or Utils.HasFlag(FILE_ATTRIBUTE_DIRECTORY, FileInfo.Base.FileAttributes) then begin FileInformation.CreationTime := FileInfo.Base.CreationTime; FileInformation.LastAccessTime := FileInfo.Base.LastAccessTime; FileInformation.LastWriteTime := FileInfo.Base.LastWriteTime; FileInformation.ChangeTime := FileInfo.Base.ChangeTime; FileInformation.FileAttributes := FileInfo.Base.FileAttributes; result := WinNative.STATUS_SUCCESS; end else begin result := WinNative.STATUS_NO_SUCH_FILE; end; end // Query file with real path else begin RedirectedPath := ExpandedPath; if RedirectedPath <> '' then begin if RedirectedPath[1] <> '\' then begin RedirectedPath := '\??\' + RedirectedPath; end; ReplacedObjAttrs.RootDirectory := 0; ReplacedObjAttrs.Attributes := ReplacedObjAttrs.Attributes or WinNative.OBJ_CASE_INSENSITIVE; ReplacedObjAttrs.ObjectName.AssignExistingStr(RedirectedPath); end; result := OrigFunc(@ReplacedObjAttrs, FileInformation); end; // .else if VfsDebug.LoggingEnabled then begin WriteLog('NtQueryAttributesFile', Format('Result: %x. Attrs: 0x%x. Path: "%s" => "%s"', [result, FileInformation.FileAttributes, string(ExpandedPath), string(RedirectedPath)])); end; end; // .function Hook_NtQueryAttributesFile function Hook_NtQueryFullAttributesFile (OrigFunc: WinNative.TNtQueryFullAttributesFile; ObjectAttributes: POBJECT_ATTRIBUTES; FileInformation: PFILE_NETWORK_OPEN_INFORMATION): NTSTATUS; stdcall; var ExpandedPath: WideString; RedirectedPath: WideString; ReplacedObjAttrs: WinNative.TObjectAttributes; HadTrailingDelim: boolean; begin if VfsDebug.LoggingEnabled then begin WriteLog('NtQueryFullAttributesFile', Format('Dir: %d. Path: "%s"', [ObjectAttributes.RootDirectory, ObjectAttributes.ObjectName.ToWideStr()])); end; ReplacedObjAttrs := ObjectAttributes^; ReplacedObjAttrs.Length := sizeof(ReplacedObjAttrs); ExpandedPath := GetFileObjectPath(ObjectAttributes); RedirectedPath := ''; if ExpandedPath <> '' then begin RedirectedPath := VfsBase.GetVfsItemRealPath(StrLib.ExcludeTrailingDelimW(ExpandedPath, @HadTrailingDelim)); end; if RedirectedPath = '' then begin RedirectedPath := ExpandedPath; end else if HadTrailingDelim then begin RedirectedPath := RedirectedPath + '\'; end; if (RedirectedPath <> '') and (RedirectedPath[1] <> '\') then begin RedirectedPath := '\??\' + RedirectedPath; end; ReplacedObjAttrs.RootDirectory := 0; ReplacedObjAttrs.Attributes := ReplacedObjAttrs.Attributes or WinNative.OBJ_CASE_INSENSITIVE; ReplacedObjAttrs.ObjectName.AssignExistingStr(RedirectedPath); result := OrigFunc(@ReplacedObjAttrs, FileInformation); if VfsDebug.LoggingEnabled then begin WriteLog('NtQueryFullAttributesFile', Format('Result: %x. Attrs: 0x%x. Path: "%s" => "%s"', [result, FileInformation.FileAttributes, string(ExpandedPath), string(RedirectedPath)])); end; end; // .Hook_NtQueryFullAttributesFile function Hook_NtOpenFile (OrigFunc: WinNative.TNtOpenFile; FileHandle: PHANDLE; DesiredAccess: ACCESS_MASK; ObjectAttributes: POBJECT_ATTRIBUTES; IoStatusBlock: PIO_STATUS_BLOCK; ShareAccess: ULONG; OpenOptions: ULONG): NTSTATUS; stdcall; begin if VfsDebug.LoggingEnabled then begin WriteLog('NtOpenFile', ObjectAttributes.ObjectName.ToWideStr()); end; result := WinNative.NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, nil, 0, ShareAccess, WinNative.FILE_OPEN, OpenOptions, nil, 0); end; function Hook_NtCreateFile (OrigFunc: WinNative.TNtCreateFile; FileHandle: PHANDLE; DesiredAccess: ACCESS_MASK; ObjectAttributes: POBJECT_ATTRIBUTES; IoStatusBlock: PIO_STATUS_BLOCK; AllocationSize: PLARGE_INTEGER; FileAttributes: ULONG; ShareAccess: ULONG; CreateDisposition: ULONG; CreateOptions: ULONG; EaBuffer: PVOID; EaLength: ULONG): NTSTATUS; stdcall; var ExpandedPath: WideString; RedirectedPath: WideString; ReplacedObjAttrs: WinNative.TObjectAttributes; HadTrailingDelim: boolean; FileInfo: Windows.TWin32FindDataW; begin if VfsDebug.LoggingEnabled then begin WriteLog('NtCreateFile', ObjectAttributes.ObjectName.ToWideStr()); end; ReplacedObjAttrs := ObjectAttributes^; ReplacedObjAttrs.Length := sizeof(ReplacedObjAttrs); ExpandedPath := GetFileObjectPath(ObjectAttributes); RedirectedPath := ''; if (ExpandedPath <> '') and ((DesiredAccess and WinNative.DELETE) = 0) and (CreateDisposition = WinNative.FILE_OPEN) then begin RedirectedPath := VfsBase.GetVfsItemRealPath(StrLib.ExcludeTrailingDelimW(ExpandedPath, @HadTrailingDelim), @FileInfo); end; if RedirectedPath = '' then begin RedirectedPath := ExpandedPath; end else if HadTrailingDelim then begin RedirectedPath := RedirectedPath + '\'; end; if (RedirectedPath <> '') and (RedirectedPath[1] <> '\') then begin RedirectedPath := '\??\' + RedirectedPath; end; ReplacedObjAttrs.RootDirectory := 0; ReplacedObjAttrs.Attributes := ReplacedObjAttrs.Attributes or WinNative.OBJ_CASE_INSENSITIVE; ReplacedObjAttrs.ObjectName.AssignExistingStr(RedirectedPath); result := OrigFunc(FileHandle, DesiredAccess, @ReplacedObjAttrs, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength); if (result = WinNative.STATUS_SUCCESS) and Utils.HasFlag(WinNative.FILE_SYNCHRONOUS_IO_NONALERT, CreateOptions) and Utils.HasFlag(WinNative.SYNCHRONIZE, DesiredAccess) then begin VfsOpenFiles.SetOpenedFileInfo(FileHandle^, TOpenedFile.Create(FileHandle^, ExpandedPath)); end; if VfsDebug.LoggingEnabled then begin if ExpandedPath <> StripNtAbsPathPrefix(RedirectedPath) then begin WriteLog('NtCreateFile', Format('Access: 0x%x. Handle: %x. Status: %x. Redirected "%s" => "%s"', [DesiredAccess, FileHandle^, result, StrLib.WideToAnsiSubstitute(ExpandedPath), StrLib.WideToAnsiSubstitute(StripNtAbsPathPrefix(RedirectedPath))])); end else begin WriteLog('NtCreateFile', Format('Access: 0x%x. Handle: %x. Status: %x. Path: "%s"', [DesiredAccess, FileHandle^, result, StrLib.WideToAnsiSubstitute(ExpandedPath)])); end; end; end; // .function Hook_NtCreateFile function Hook_NtClose (OrigFunc: WinNative.TNtClose; hData: HANDLE): NTSTATUS; stdcall; begin if VfsDebug.LoggingEnabled then begin WriteLog('NtClose', Format('Handle: %x', [integer(hData)])); end; with OpenFilesCritSection do begin Enter; result := OrigFunc(hData); if WinNative.NT_SUCCESS(result) then begin VfsOpenFiles.DeleteOpenedFileInfo(hData); end; Leave; end; if VfsDebug.LoggingEnabled then begin WriteLog('NtClose', Format('Status: %x', [integer(result)])); end; end; // .function Hook_NtClose function IsSupportedFileInformationClass (FileInformationClass: integer): boolean; begin result := (FileInformationClass <= High(byte)) and (FILE_INFORMATION_CLASS(byte(FileInformationClass)) in [FileBothDirectoryInformation, FileDirectoryInformation, FileFullDirectoryInformation, FileIdBothDirectoryInformation, FileIdFullDirectoryInformation, FileNamesInformation]); end; type TFileInfoConvertResult = (TOO_SMALL_BUF, COPIED_ALL, TRUNCATED_NAME); TTruncatedNamesStrategy = (DONT_TRUNCATE_NAMES, TRUNCATE_NAMES); function ConvertFileInfoStruct (SrcInfo: PNativeFileInfo; TargetFormat: FILE_INFORMATION_CLASS; {n} Buf: pointer; BufSize: integer; TruncatedNamesStrategy: TTruncatedNamesStrategy; {OUT} var BytesWritten: integer): TFileInfoConvertResult; var {n} FileNameBuf: pointer; FileNameBufSize: integer; StructBaseSize: integer; StructFullSize: integer; begin {!} Assert(SrcInfo <> nil); {!} Assert(IsSupportedFileInformationClass(ord(TargetFormat)), Format('Unsupported file information class: %d', [ord(TargetFormat)])); FileNameBuf := nil; // * * * * * // BytesWritten := 0; StructBaseSize := WinNative.GetFileInformationClassSize(TargetFormat); StructFullSize := StructBaseSize + Int(SrcInfo.Base.FileNameLength); if (Buf = nil) or (BufSize < StructBaseSize) then begin result := TOO_SMALL_BUF; exit; end; result := COPIED_ALL; if BufSize < StructFullSize then begin result := TRUNCATED_NAME; if TruncatedNamesStrategy = DONT_TRUNCATE_NAMES then begin exit; end; end; case TargetFormat of FileNamesInformation: PFILE_NAMES_INFORMATION(Buf).FileNameLength := SrcInfo.Base.FileNameLength; FileBothDirectoryInformation, FileDirectoryInformation, FileFullDirectoryInformation, FileIdBothDirectoryInformation, FileIdFullDirectoryInformation: begin Utils.CopyMem(StructBaseSize, @SrcInfo.Base, Buf); end; else {!} Assert(IsSupportedFileInformationClass(ord(TargetFormat)), Format('Unexpected unsupported file information class: %d', [ord(TargetFormat)])); end; FileNameBufSize := Min(BufSize - StructBaseSize, SrcInfo.Base.FileNameLength) and not $00000001; FileNameBuf := Utils.PtrOfs(Buf, StructBaseSize); Utils.CopyMem(FileNameBufSize, PWideChar(SrcInfo.FileName), FileNameBuf); BytesWritten := StructBaseSize + FileNameBufSize; end; // .function ConvertFileInfoStruct function Hook_NtQueryDirectoryFile (OrigFunc: WinNative.TNtQueryDirectoryFile; FileHandle: HANDLE; Event: HANDLE; ApcRoutine: pointer; ApcContext: PVOID; Io: PIO_STATUS_BLOCK; Buffer: PVOID; BufLength: ULONG; InfoClass: integer (* FILE_INFORMATION_CLASS *); SingleEntry: BOOLEAN; {n} Mask: PUNICODE_STRING; RestartScan: BOOLEAN): NTSTATUS; stdcall; const ENTRIES_ALIGNMENT = 8; type PPrevEntry = ^TPrevEntry; TPrevEntry = packed record NextEntryOffset: ULONG; FileIndex: ULONG; end; var {Un} OpenedFile: TOpenedFile; {Un} FileInfo: TFileInfo; {n} BufCurret: pointer; {n} PrevEntry: PPrevEntry; BufSize: integer; BufSizeLeft: integer; BytesWritten: integer; IsFirstEntry: boolean; Proceed: boolean; TruncatedNamesStrategy: TTruncatedNamesStrategy; StructConvertResult: TFileInfoConvertResult; EmptyMask: UNICODE_STRING; EntryName: WideString; begin OpenedFile := nil; FileInfo := nil; BufCurret := nil; PrevEntry := nil; BufSize := 0; // * * * * * // with OpenFilesCritSection do begin if Mask = nil then begin EmptyMask.Reset; Mask := @EmptyMask; end; if VfsDebug.LoggingEnabled then begin WriteLog('NtQueryDirectoryFile', Format('Handle: %x. InfoClass: %s. Mask: %s', [integer(FileHandle), WinNative.FileInformationClassToStr(InfoClass), AnsiString(Mask.ToWideStr())])); end; Enter; // FIXME REWRITE ME //OpenedFile := OpenedFiles[pointer(FileHandle)]; if (OpenedFile = nil) or (Event <> 0) or (ApcRoutine <> nil) or (ApcContext <> nil) then begin WriteLog('NtQueryDirectoryFile', Format('Calling native NtQueryDirectoryFile. OpenedFile: %x. %d %d %d', [integer(OpenedFile), integer(Event), integer(ApcRoutine), integer(ApcContext)])); result := OrigFunc(FileHandle, Event, ApcRoutine, ApcContext, Io, Buffer, BufLength, InfoClass, SingleEntry, Mask, RestartScan); end else begin int(Io.Information) := 0; result := STATUS_SUCCESS; if RestartScan then begin SysUtils.FreeAndNil(OpenedFile.DirListing); end; OpenedFile.FillDirListing(Mask.ToWideStr()); Proceed := (Buffer <> nil) and (BufLength > 0); // Validate buffer if not Proceed then begin result := STATUS_INVALID_BUFFER_SIZE; end else begin BufSize := Utils.IfThen(int(BufLength) > 0, int(BufLength), High(int)); end; // Validate information class if Proceed then begin Proceed := IsSupportedFileInformationClass(InfoClass); if not Proceed then begin result := STATUS_INVALID_INFO_CLASS; end; end; // Signal of scanning end, if necessary if Proceed then begin Proceed := not OpenedFile.DirListing.IsEnd; if not Proceed then begin result := STATUS_NO_MORE_FILES; end; end; // Scan directory if Proceed then begin BufCurret := Buffer; BytesWritten := 1; while (BytesWritten > 0) and OpenedFile.DirListing.GetNextItem(FileInfo) do begin // Align next record to 8-bytes boundary from Buffer start BufCurret := pointer(int(Buffer) + Alg.IntRoundToBoundary(int(Io.Information), ENTRIES_ALIGNMENT)); BufSizeLeft := BufSize - (int(BufCurret) - int(Buffer)); IsFirstEntry := OpenedFile.DirListing.FileInd = 1; if IsFirstEntry then begin TruncatedNamesStrategy := TRUNCATE_NAMES; end else begin TruncatedNamesStrategy := DONT_TRUNCATE_NAMES; end; StructConvertResult := ConvertFileInfoStruct(@FileInfo.Data, FILE_INFORMATION_CLASS(byte(InfoClass)), BufCurret, BufSizeLeft, TruncatedNamesStrategy, BytesWritten); if VfsDebug.LoggingEnabled then begin EntryName := Copy(FileInfo.Data.FileName, 1, Min(BytesWritten - WinNative.GetFileInformationClassSize(InfoClass), FileInfo.Data.Base.FileNameLength) div 2); WriteLog('NtQueryDirectoryFile', 'Written entry: ' + EntryName); end; //VarDump(['Converted struct to buf offset:', int(BufCurret) - int(Buffer), 'Written:', BytesWritten, 'Result:', ord(StructConvertResult)]); with PFILE_ID_BOTH_DIR_INFORMATION(BufCurret)^ do begin NextEntryOffset := 0; FileIndex := 0; end; if StructConvertResult = TOO_SMALL_BUF then begin OpenedFile.DirListing.SeekRel(-1); if IsFirstEntry then begin result := STATUS_BUFFER_TOO_SMALL; end; end else if StructConvertResult = TRUNCATED_NAME then begin if IsFirstEntry then begin result := STATUS_BUFFER_OVERFLOW; Inc(int(Io.Information), BytesWritten); end else begin OpenedFile.DirListing.SeekRel(-1); end; end else if StructConvertResult = COPIED_ALL then begin if PrevEntry <> nil then begin int(Io.Information) := int(BufCurret) - int(Buffer) + BytesWritten; end else begin int(Io.Information) := BytesWritten; end; end; // .else if (BytesWritten > 0) and (PrevEntry <> nil) then begin PrevEntry.NextEntryOffset := cardinal(int(BufCurret) - int(PrevEntry)); end; PrevEntry := BufCurret; //Msg(Format('Written: %d. Total: %d', [BytesWritten, int(Io.Information)])); if SingleEntry then begin BytesWritten := 0; end; end; // .while end; // .if Io.Status.Status := result; end; // .else Leave; end; // .with if VfsDebug.LoggingEnabled then begin WriteLog('NtQueryDirectoryFile', Format('Status: %x. Written: %d bytes', [integer(result), integer(Io.Information)])); end; end; // .function Hook_NtQueryDirectoryFile procedure InstallHooks; var hDll: Windows.THandle; NtdllHandle: integer; begin with HooksCritSection do begin Enter; if not HooksInstalled then begin HooksInstalled := true; // Ensure, that library with VFS hooks installed is never unloaded if System.IsLibrary then begin WinNative.GetModuleHandleExW(WinNative.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS or WinNative.GET_MODULE_HANDLE_EX_FLAG_PIN, @InstallHooks, hDll); end; NtdllHandle:= Windows.GetModuleHandle('ntdll.dll'); {!} Assert(NtdllHandle <> 0, 'Failed to load ntdll.dll library'); WriteLog('InstallHook', 'Installing NtQueryAttributesFile hook'); NativeNtQueryAttributesFile := VfsPatching.SpliceWinApi ( VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtQueryAttributesFile'), @Hook_NtQueryAttributesFile ); // WriteLog('InstallHook', 'Installing NtQueryFullAttributesFile hook'); // NativeNtQueryFullAttributesFile := VfsPatching.SpliceWinApi // ( // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtQueryFullAttributesFile'), // @Hook_NtQueryFullAttributesFile // ); // WriteLog('InstallHook', 'Installing NtOpenFile hook'); // NativeNtOpenFile := VfsPatching.SpliceWinApi // ( // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtOpenFile'), // @Hook_NtOpenFile // ); // WriteLog('InstallHook', 'Installing NtCreateFile hook'); // NativeNtCreateFile := VfsPatching.SpliceWinApi // ( // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtCreateFile'), // @Hook_NtCreateFile // ); // WriteLog('InstallHook', 'Installing NtClose hook'); // NativeNtClose := VfsPatching.SpliceWinApi // ( // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtClose'), // @Hook_NtClose // ); // WriteLog('InstallHook', 'Installing NtQueryDirectoryFile hook'); // NativeNtQueryDirectoryFile := VfsPatching.SpliceWinApi // ( // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtQueryDirectoryFile'), // @Hook_NtQueryDirectoryFile // ); end; // .if Leave; end; // .with end; // .procedure InstallHooks begin HooksCritSection.Init; end.