Written first integration test. Added debug console support

This commit is contained in:
Berserker 2019-05-03 22:15:36 +03:00
parent 65e182822a
commit 6b5de215d2
10 changed files with 990 additions and 9 deletions

View File

@ -0,0 +1 @@
Please, call us later.

View File

@ -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());

71
Tests/VfsControl.pas Normal file
View File

@ -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.

View File

@ -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.

46
Tests/VfsTestHelper.pas Normal file
View File

@ -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.

View File

@ -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);

556
VfsHooks.pas Normal file
View File

@ -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.

129
VfsPatching.pas Normal file
View File

@ -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.

View File

@ -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.

15
_TODO_.txt Normal file
View File

@ -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;