Virtual-File-System/VfsHooks.pas

556 lines
21 KiB
ObjectPascal

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.