diff --git a/Tests/Fs/Mods/Apache/503.html b/Tests/Fs/Mods/Apache/503.html new file mode 100644 index 0000000..177f983 --- /dev/null +++ b/Tests/Fs/Mods/Apache/503.html @@ -0,0 +1 @@ +Please, call us later. \ No newline at end of file diff --git a/Tests/VfsBaseTest.pas b/Tests/VfsBaseTest.pas index 9ce9ba7..2eb3188 100644 --- a/Tests/VfsBaseTest.pas +++ b/Tests/VfsBaseTest.pas @@ -46,8 +46,14 @@ begin VfsBase.MapDir(RootDir, RootDir + '\Mods\B', DONT_OVERWRITE_EXISTING); VfsBase.MapDir(RootDir, RootDir + '\Mods\A', DONT_OVERWRITE_EXISTING); VfsBase.RunVfs(SORT_FIFO); - VfsBase.GetVfsDirInfo(RootDir, '*', DirInfo, DirListing); + VfsBase.PauseVfs; + VfsBase.GetVfsDirInfo(RootDir, '*', DirInfo, DirListing); + DirListing.Rewind; + Check(DirListing.GetDebugDump() = '', 'Virtual directory listing must be empty when VFS is paused. Got: ' + DirListing.GetDebugDump()); + + VfsBase.RunVfs(SORT_FIFO); + VfsBase.GetVfsDirInfo(RootDir, '*', DirInfo, DirListing); DirListing.Rewind; Check(DirListing.GetDebugDump() = 'vcredist.bmp'#13#10'eula.1028.txt', 'Invalid virtual directoring listing. Got: ' + DirListing.GetDebugDump()); diff --git a/Tests/VfsControl.pas b/Tests/VfsControl.pas new file mode 100644 index 0000000..dd81884 --- /dev/null +++ b/Tests/VfsControl.pas @@ -0,0 +1,71 @@ +unit VfsControl; +(* + Facade unit for high-level VFS API. +*) + + +(***) interface (***) + +uses + Windows, SysUtils, + Utils, + VfsBase, VfsUtils, VfsHooks; + + +(* Runs all VFS subsystems, unless VFS is already running *) +function RunVfs (DirListingOrder: VfsBase.TDirListingSortType): boolean; + + +(***) implementation (***) + + +function GetCurrentDirW: WideString; +var + Buf: array [0..32767 - 1] of WideChar; + ResLen: integer; + +begin + result := ''; + ResLen := Windows.GetCurrentDirectoryW(sizeof(Buf), @Buf); + + if ResLen > 0 then begin + SetLength(result, ResLen); + Utils.CopyMem(ResLen * sizeof(WideChar), @Buf, PWideChar(result)); + end; +end; + +function SetCurrentDirW (const DirPath: WideString): boolean; +var + AbsPath: WideString; + +begin + AbsPath := VfsUtils.NormalizePath(DirPath); + result := Windows.SetCurrentDirectoryW(PWideChar(AbsPath)); +end; + +function RunVfs (DirListingOrder: VfsBase.TDirListingSortType): boolean; +var + CurrDir: WideString; + +begin + with VfsBase.VfsCritSection do begin + Enter; + + result := VfsBase.RunVfs(DirListingOrder); + + if result then begin + VfsHooks.InstallHooks; + + // Try to ensure, that current directory handle is tracked by VfsOpenFiles + CurrDir := GetCurrentDirW; + + if CurrDir <> '' then begin + SetCurrentDirW(CurrDir); + end; + end; + + Leave; + end; // .with +end; // function RunVfs + +end. \ No newline at end of file diff --git a/Tests/VfsIntegratedTest.pas b/Tests/VfsIntegratedTest.pas new file mode 100644 index 0000000..6622082 --- /dev/null +++ b/Tests/VfsIntegratedTest.pas @@ -0,0 +1,97 @@ +unit VfsIntegratedTest; + +(***) interface (***) + +uses + SysUtils, TestFramework, Windows, + Utils, WinUtils, ConsoleApi, + VfsUtils, VfsBase, VfsDebug, + VfsControl; + +type + TestIntegrated = class (TTestCase) + private + Inited: boolean; + + function GetRootDir: string; + + protected + procedure SetUp; override; + procedure TearDown; override; + + published + procedure TestGetFileAttributes; + end; + +(***) implementation (***) + + +procedure LogSomething (Operation, Message: pchar); stdcall; +begin + WriteLn('>> ', string(Operation), ': ', string(Message), #13#10); +end; + +function TestIntegrated.GetRootDir: string; +begin + result := SysUtils.ExtractFileDir(WinUtils.GetExePath) + '\Tests\Fs'; +end; + +procedure TestIntegrated.SetUp; +var + RootDir: string; + +begin + if not Inited then begin + Inited := true; + RootDir := Self.GetRootDir; + VfsBase.ResetVfs(); + VfsBase.MapDir(RootDir, RootDir + '\Mods\FullyVirtual', DONT_OVERWRITE_EXISTING); + VfsBase.MapDir(RootDir, RootDir + '\Mods\B', DONT_OVERWRITE_EXISTING); + VfsBase.MapDir(RootDir, RootDir + '\Mods\A', DONT_OVERWRITE_EXISTING); + VfsBase.MapDir(RootDir, RootDir + '\Mods\Apache', DONT_OVERWRITE_EXISTING); + VfsDebug.SetLoggingProc(LogSomething); + VfsControl.RunVfs(VfsBase.SORT_FIFO); + end; +end; + +procedure TestIntegrated.TearDown; +begin + VfsBase.PauseVfs(); + VfsDebug.SetLoggingProc(nil); +end; + +procedure TestIntegrated.TestGetFileAttributes; +var + RootDir: string; + + function HasValidAttrs (const Path: string; const RequiredAttrs: integer = 0; const ForbiddenAttrs: integer = 0): boolean; + var + Attrs: integer; + + begin + Attrs := Int(Windows.GetFileAttributes(pchar(Path))); + result := Attrs <> -1; + + if result then begin + if RequiredAttrs <> 0 then begin + result := (Attrs and RequiredAttrs) = RequiredAttrs; + end; + + if result and (ForbiddenAttrs <> 0) then begin + result := (Attrs and ForbiddenAttrs) = 0; + end; + end; + end; // .function HasValidAttrs + +begin + RootDir := Self.GetRootDir; + Check(not HasValidAttrs(RootDir + '\non-existing.non'), '{1}'); + Check(HasValidAttrs(RootDir + '\Hobbots\mms.cfg', 0, Windows.FILE_ATTRIBUTE_DIRECTORY), '{2}'); + Check(HasValidAttrs(RootDir + '\503.html', 0, Windows.FILE_ATTRIBUTE_DIRECTORY), '{3}'); + Check(HasValidAttrs(RootDir + '\Hobbots\', Windows.FILE_ATTRIBUTE_DIRECTORY), '{4}'); + Check(HasValidAttrs(RootDir + '\Mods', Windows.FILE_ATTRIBUTE_DIRECTORY), '{5}'); +end; + +begin + RegisterTest(TestIntegrated.Suite); +end. \ No newline at end of file diff --git a/Tests/VfsTestHelper.pas b/Tests/VfsTestHelper.pas new file mode 100644 index 0000000..624dada --- /dev/null +++ b/Tests/VfsTestHelper.pas @@ -0,0 +1,46 @@ +unit VfsTestHelper; +(* + +*) + + +(***) interface (***) + +uses + SysUtils, Windows, + Utils; + +(* Initializes debug console *) +procedure InitConsole; + + +(***) implementation (***) + + +procedure InitConsole; +var + Rect: TSmallRect; + BufSize: TCoord; + hIn: THandle; + hOut: THandle; + +begin + AllocConsole; + SetConsoleCP(GetACP); + SetConsoleOutputCP(GetACP); + hIn := GetStdHandle(STD_INPUT_HANDLE); + hOut := GetStdHandle(STD_OUTPUT_HANDLE); + pinteger(@System.Input)^ := hIn; + pinteger(@System.Output)^ := hOut; + BufSize.x := 120; + BufSize.y := 1000; + SetConsoleScreenBufferSize(hOut, BufSize); + Rect.Left := 0; + Rect.Top := 0; + Rect.Right := 120 - 1; + Rect.Bottom := 50 - 1; + SetConsoleWindowInfo(hOut, true, Rect); + SetConsoleTextAttribute(hOut, (0 shl 4) or $0F); +end; // .procedure InitConsole; + +end. \ No newline at end of file diff --git a/VfsBase.pas b/VfsBase.pas index 94637e4..c8a2335 100644 --- a/VfsBase.pas +++ b/VfsBase.pas @@ -36,7 +36,7 @@ type SORT_FIFO - Items of the first mapped directory will be listed before the second mapped directory items. SORT_LIFO - Items of The last mapped directory will be listed before all other mapped directory items. *) - TDirListingSortType = (SORT_FIFO, SORT_LIFO); + TDirListingSortType = (SORT_FIFO = 0, SORT_LIFO = 1); (* Single redirected VFS entry: file or directory *) TVfsItem = class @@ -79,6 +79,8 @@ type procedure RestoreVfsForThread; end; + TSingleArgExternalFunc = function (Arg: pointer = nil): integer; stdcall; + var (* Global VFS access synchronizer *) VfsCritSection: Concur.TCritSection; @@ -86,9 +88,13 @@ var function GetThreadVfsDisabler: TThreadVfsDisabler; -(* Runs VFS. Higher level API must install hooks in VfsCritSection protected area *) +(* Runs VFS. Higher level API must install hooks in VfsCritSection protected area. + Listing order is ignored if VFS is resumed from pause *) function RunVfs (DirListingOrder: TDirListingSortType): boolean; +(* Temporarily pauses VFS, but does not reset existing mappings *) +function PauseVfs: boolean; + (* Stops VFS and clears all mappings *) function ResetVfs: boolean; @@ -101,6 +107,9 @@ function GetVfsDirInfo (const AbsVirtPath, Mask: WideString; {OUT} var DirInfo: (* Maps real directory contents to virtual path. Target must exist for success *) function MapDir (const VirtPath, RealPath: WideString; OverwriteExisting: boolean; Flags: integer = 0): boolean; +(* Calls specified function with a single argument and returns its result. VFS is disabled for current thread during function exection *) +function CallWithoutVfs (Func: TSingleArgExternalFunc; Arg: pointer = nil): integer; stdcall; + (***) implementation (***) @@ -115,6 +124,9 @@ var (* Global VFS state indicator. If false, all VFS search operations must fail *) VfsIsRunning: boolean = false; + + (* If true, VFS file/directory hierarchy is built and no mapping is allowed untill full reset *) + VfsTreeIsBuilt: boolean = false; (* Automatical VFS items priority management *) OverwritingPriority: integer = INITIAL_OVERWRITING_PRIORITY; @@ -271,15 +283,32 @@ begin Enter; if not VfsIsRunning then begin - BuildVfsItemsTree(); - SortVfsDirListings(DirListingOrder); + if not VfsTreeIsBuilt then begin + BuildVfsItemsTree(); + SortVfsDirListings(DirListingOrder); + VfsTreeIsBuilt := true; + end; + VfsIsRunning := true; end; + Leave; + end; // .with + end; // .if +end; // .function RunVfs + +function PauseVfs: boolean; +begin + result := not DisableVfsForThisThread; + + if result then begin + with VfsCritSection do begin + Enter; + VfsIsRunning := false; Leave; end; end; -end; // .function RunVfs +end; function ResetVfs: boolean; begin @@ -288,8 +317,9 @@ begin if result then begin with VfsCritSection do begin Enter; - VfsIsRunning := false; VfsItems.Clear(); + VfsIsRunning := false; + VfsTreeIsBuilt := false; Leave; end; end; @@ -481,9 +511,36 @@ end; // .function _MapDir function MapDir (const VirtPath, RealPath: WideString; OverwriteExisting: boolean; Flags: integer = 0): boolean; begin - result := _MapDir(NormalizePath(VirtPath), NormalizePath(RealPath), nil, OverwriteExisting, AUTO_PRIORITY) <> nil; + with VfsCritSection do begin + Enter; + + result := not VfsIsRunning and not VfsTreeIsBuilt; + + if result then begin + result := _MapDir(NormalizePath(VirtPath), NormalizePath(RealPath), nil, OverwriteExisting, AUTO_PRIORITY) <> nil; + end; + + Leave; + end; end; +function CallWithoutVfs (Func: TSingleArgExternalFunc; Arg: pointer = nil): integer; stdcall; +begin + with GetThreadVfsDisabler do begin + try + DisableVfsForThread; + result := Func(Arg); + except + on E: Exception do begin + RestoreVfsForThread; + raise E; + end; + end; // .try + + RestoreVfsForThread; + end; // .with +end; // .function CallWithoutVfs + begin VfsCritSection.Init; VfsItems := DataLib.NewDict(Utils.OWNS_ITEMS, DataLib.CASE_SENSITIVE); diff --git a/VfsHooks.pas b/VfsHooks.pas new file mode 100644 index 0000000..4914629 --- /dev/null +++ b/VfsHooks.pas @@ -0,0 +1,556 @@ +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. \ No newline at end of file diff --git a/VfsPatching.pas b/VfsPatching.pas new file mode 100644 index 0000000..1385180 --- /dev/null +++ b/VfsPatching.pas @@ -0,0 +1,129 @@ +unit VfsPatching; +(* + Description: Code patching facilities, based on PatchForge library. + All hooks are thread-safe. +*) + + +(***) interface (***) + +uses + Windows, SysUtils, Utils, PatchForge; + + +(* Replaces original STDCALL function with the new one with the same prototype and one extra argument. + The argument is callable pointer, used to execute original function. The pointer is passed as THE FIRST + argument before other arguments. *) +function SpliceWinApi (OrigFunc, HandlerFunc: pointer): pointer; + + +(***) implementation (***) + + +type + (* Import *) + TPatchMaker = PatchForge.TPatchMaker; + TPatchHelper = PatchForge.TPatchHelper; + + +(* Writes arbitrary data to any write-protected section *) +function WriteAtCode (NumBytes: integer; {n} Src, {n} Dst: pointer): boolean; +var + OldPageProtect: integer; + +begin + {!} Assert(Utils.IsValidBuf(Src, NumBytes)); + {!} Assert(Utils.IsValidBuf(Dst, NumBytes)); + result := NumBytes = 0; + + if not result then begin + try + result := Windows.VirtualProtect(Dst, NumBytes, Windows.PAGE_EXECUTE_READWRITE, @OldPageProtect); + + if result then begin + Utils.CopyMem(NumBytes, Src, Dst); + Windows.VirtualProtect(Dst, NumBytes, OldPageProtect, @OldPageProtect); + end; + except + result := false; + end; + end; // .if +end; // .function WriteAtCode + +(* Writes patch to any write-protected section *) +function WritePatchAtCode (PatchMaker: TPatchMaker; {n} Dst: pointer): boolean; +var + Buf: Utils.TArrayOfByte; + +begin + {!} Assert(PatchMaker <> nil); + {!} Assert((Dst <> nil) or (PatchMaker.Size = 0)); + // * * * * * // + result := true; + + if PatchMaker.Size > 0 then begin + SetLength(Buf, PatchMaker.Size); + PatchMaker.ApplyPatch(pointer(Buf), Dst); + result := WriteAtCode(Length(Buf), pointer(Buf), Dst); + end; +end; // .function WritePatchAtCode + +function SpliceWinApi (OrigFunc, HandlerFunc: pointer): pointer; +const + CODE_ADDR_ALIGNMENT = 8; + +var +{O} p: PatchForge.TPatchHelper; +{OI} SpliceBridge: pbyte; // Memory is never freed + OrigFuncBridgeLabel: string; + OrigCodeBridgeStartPos: integer; + OverwrittenCodeSize: integer; + +begin + {!} Assert(OrigFunc <> nil); + {!} Assert(HandlerFunc <> nil); + p := TPatchHelper.Wrap(TPatchMaker.Create); + SpliceBridge := nil; + result := nil; + // * * * * * // + + // === BEGIN generating SpliceBridge === + // Add pointer to original function bridge as the first argument + p.WriteTribyte(PatchForge.INSTR_PUSH_PTR_ESP); + p.WriteInt(PatchForge.INSTR_MOV_ESP_PLUS_4_CONST32); + p.ExecActionOnApply(PatchForge.TAddLabelRealAddrAction.Create(p.NewAutoLabel(OrigFuncBridgeLabel))); + p.WriteInt(0); + + // Jump to new handler + p.Jump(PatchForge.JMP, HandlerFunc); + + // Ensure original code bridge is aligned + p.Nop(p.Pos mod CODE_ADDR_ALIGNMENT); + + // Set result to offset from splice bridge start to original function bridge + result := pointer(p.Pos); + + // Write original function bridge + p.PutLabel(OrigFuncBridgeLabel); + OrigCodeBridgeStartPos := p.Pos; + p.WriteCode(OrigFunc, PatchForge.TMinCodeSizeDetector.Create(sizeof(PatchForge.TJumpCall32Rec))); + OverwrittenCodeSize := p.Pos - OrigCodeBridgeStartPos; + p.Jump(PatchForge.JMP, Utils.PtrOfs(OrigFunc, OverwrittenCodeSize)); + // === END generating SpliceBridge === + + // Persist splice bridge + GetMem(SpliceBridge, p.Size); + WritePatchAtCode(p.PatchMaker, SpliceBridge); + + // Turn result from offset to absolute address + result := Ptr(integer(SpliceBridge) + integer(result)); + + // Create and apply hook at target function start + p.Clear(); + p.Jump(PatchForge.JMP, SpliceBridge); + WritePatchAtCode(p.PatchMaker, OrigFunc); + // * * * * * // + p.Release; +end; + +end. \ No newline at end of file diff --git a/VfsTest.dpr b/VfsTest.dpr index 702b9eb..f5feb2c 100644 --- a/VfsTest.dpr +++ b/VfsTest.dpr @@ -4,10 +4,13 @@ uses TestFramework, GuiTestRunner, VfsUtils, VfsBase, VfsDebug, VfsApiDigger, VfsExport, VfsOpenFiles, + VfsHooks, VfsControl, + VfsTestHelper, VfsDebugTest, VfsUtilsTest, VfsBaseTest, - VfsApiDiggerTest, VfsOpenFilesTest; + VfsApiDiggerTest, VfsOpenFilesTest, VfsIntegratedTest; begin + VfsTestHelper.InitConsole; TGUITestRunner.RunRegisteredTests; end. diff --git a/_TODO_.txt b/_TODO_.txt new file mode 100644 index 0000000..ebb4d42 --- /dev/null +++ b/_TODO_.txt @@ -0,0 +1,15 @@ +UTF-8 Logging + +SetCurrentDirectoryW(GetCurrentDirectoryW) +System.IsMultiThread for DLL and exported API + +(* Trying to turn off DEP *) +SetProcessDEPPolicyAddr := Windows.GetProcAddress(Kernel32Handle, 'SetProcessDEPPolicy'); + +if SetProcessDEPPolicyAddr <> nil then begin + if PatchApi.Call(PatchApi.STDCALL_, SetProcessDEPPolicyAddr, [0]) <> 0 then begin + Log.Write('VFS', 'SetProcessDEPPolicy', 'DEP was turned off'); + end else begin + Log.Write('VFS', 'SetProcessDEPPolicy', 'Failed to turn DEP off'); + end; +end; \ No newline at end of file