NTFS Reparse Points

Hi, Habr. I present to you a guide to NTFS Reparse points (hereinafter RP), reparse points. This article is for those who are just starting to learn the intricacies of developing the Windows kernel and its environment. In the beginning I will explain the theory with examples, then I will give an interesting problem.


RP is one of the key features of the NTFS file system, which can be useful in solving backup and recovery tasks. As a result, Acronis is very interested in this technology.

useful links

If you want to figure out the topic yourself, check out these resources. The theoretical part that you will see next is a short retelling of materials from this list.

Theory

A Reparse Point (RP) is an object of a given size with programmer-defined data and a unique tag. Custom object represented by structure REPARSE_GUID_DATA_BUFFER

typedef struct _REPARSE_GUID_DATA_BUFFER {
  ULONG  ReparseTag;
  USHORT ReparseDataLength;
  USHORT Reserved;
  GUID   ReparseGuid;
  struct {
    UCHAR DataBuffer[1];
  } GenericReparseBuffer;
} REPARSE_GUID_DATA_BUFFER, *PREPARSE_GUID_DATA_BUFFER;

The size of the RP data block is up to 16 kilobytes.
ReparseTag – 32-bit tag.
ReparseDataLength – data size
DataBuffer – pointer to user data

RPs provided by Microsoft can be represented by the structure REPARSE_DATA_BUFFER… It should not be used for custom RPs.

Consider the tag format:

M – Microsoft Bit; If this bit is set, then the tag is developed by Microsoft.
L – Delay bit; If this bit is set, then the data referenced by the RP is located on the medium with a slow response speed and a long data output delay.
R – Reserved bit;
N – Name change bit; If this bit is set, then the file or directory represents another named entity in the file system.
Tag value – Requested from Microsoft;

Each time the application creates or deletes an RP, NTFS updates the metadata file \ $ Extend \ $ Reparse… It is in this file that RP records are stored. This centralized storage allows any application to sort and efficiently search for the desired object.

The Windows RP engine provides support for symbolic links, remote storage systems, and volume and directory mount points.

Hard links in Windows are not an actual object, but simply a synonym to the same file on disk. These are not separate filesystem objects, but simply another file name in the file location table. This is how hard links differ from symbolic links.

To use RP technology, we need to write:

  • Small application with privileges SE_BACKUP_NAME or SE_RESTORE_NAME, which will create a file containing the RP structure, set the required field ReparseTag and fill in DataBuffer
  • A kernel-mode driver that will read the buffer data and handle calls to this file.

Create your own RP file

1. We get the necessary privileges

void GetPrivilege(LPCTSTR priv)
{
	HANDLE hToken;
	TOKEN_PRIVILEGES tp;
	OpenProcessToken(GetCurrentProcess(),
		TOKEN_ADJUST_PRIVILEGES, &hToken);
	LookupPrivilegeValue(NULL, priv, &tp.Privileges[0].Luid);
	tp.PrivilegeCount = 1;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	AdjustTokenPrivileges(hToken, FALSE, &tp,
		sizeof(TOKEN_PRIVILEGES), NULL, NULL);
	CloseHandle(hToken);
}

GetPrivilege(SE_BACKUP_NAME);
GetPrivilege(SE_RESTORE_NAME);
GetPrivilege(SE_CREATE_SYMBOLIC_LINK_NAME);

2. Preparing the structure REPARSE_GUID_DATA_BUFFER… For example, we will write a simple line in the RP data “My reparse data”

TCHAR data[] = _T("My reparse data");
BYTE reparseBuffer[sizeof(REPARSE_GUID_DATA_BUFFER) + sizeof(data)];
PREPARSE_GUID_DATA_BUFFER rd = (PREPARSE_GUID_DATA_BUFFER) reparseBuffer;

ZeroMemory(reparseBuffer, sizeof(REPARSE_GUID_DATA_BUFFER) + sizeof(data));

// {07A869CB-F647-451F-840D-964A3AF8C0B6}
static const GUID my_guid = { 0x7a869cb, 0xf647, 0x451f, { 0x84, 0xd, 0x96, 0x4a, 0x3a, 0xf8, 0xc0, 0xb6 }};

rd->ReparseTag = 0xFF00;
rd->ReparseDataLength = sizeof(data);
rd->Reserved = 0;
rd->ReparseGuid = my_guid;
memcpy(rd->GenericReparseBuffer.DataBuffer, &data, sizeof(data));

3. Create a file.

LPCTSTR name = _T("TestReparseFile");

_tprintf(_T("Creating empty filen"));
HANDLE hFile = CreateFile(name,
	GENERIC_READ | GENERIC_WRITE, 
	0, NULL,
	CREATE_NEW, 
	FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
	NULL);
if (INVALID_HANDLE_VALUE == hFile)
{
_tprintf(_T("Failed to create filen"));
	return -1;
}

4. We fill the file with our structure using the method DeviceIoControl with parameter FSCTL_SET_REPARSE_POINT

_tprintf(_T("Creating reparsen"));
if (!DeviceIoControl(hFile, FSCTL_SET_REPARSE_POINT, rd, rd->ReparseDataLength + REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, NULL, 0, &dwLen, NULL))
{
	CloseHandle(hFile);
	DeleteFile(name);

	_tprintf(_T("Failed to create reparsen"));
	return -1;
}

CloseHandle(hFile);

The complete code of this application can be found here

After assembly and launch, we have a file. Utility fsutil will help you look at the file we created and make sure our data is in place.

RP processing

It’s time to look at this file from the side of the vigorous space. I will not go into the details of the driver mini-filter device. There is a good explanation in the official documentation from Microsoft with code examples… And we will look at post callback method.

Need to re-request IRP with parameter FILE_OPEN_REPARSE_POINT… For this we will call FltReissueSynchronousIo… This function will repeat the request, but with updated Create.Options

Inside the structure PFLT_CALLBACK_DATA there is a field TagData… If you call the method FltFsControlFile with code FSCTL_GET_REPARSE_POINT, then we get our buffer with data.

// todo конечно стоит проверить наш ли это тэг, а не только его наличие
if (Data->TagData != NULL) 
{
if ((Data->Iopb->Parameters.Create.Options & FILE_OPEN_REPARSE_POINT) != FILE_OPEN_REPARSE_POINT)
    {
      Data->Iopb->Parameters.Create.Options |= FILE_OPEN_REPARSE_POINT;

      FltSetCallbackDataDirty(Data);
      FltReissueSynchronousIo(FltObjects->Instance, Data);
    }

    status = FltFsControlFile(
      FltObjects->Instance, 
      FltObjects->FileObject, 
      FSCTL_GET_REPARSE_POINT, 
      NULL, 
      0, 
      reparseData,
      reparseDataLength,
      NULL
    );
}

Then you can use this data depending on the task. You can re-request IRP… Or initiate a completely new request. For example, in the project LazyCopy the RP data stores the path to the original file. The author does not start copying when the file is opened, but only re-saves data from RP to stream context of this file. Data starts to be copied the moment the file is read or written. Here are the highlights of his project:

// Operations.c - PostCreateOperationCallback

NT_IF_FAIL_LEAVE(LcGetReparsePointData(FltObjects, &fileSize, &remotePath, &useCustomHandler));

NT_IF_FAIL_LEAVE(LcFindOrCreateStreamContext(Data, TRUE, &fileSize, &remotePath, useCustomHandler, &streamContext, &contextCreated));

// Operations.c - PreReadWriteOperationCallback

status = LcGetStreamContext(Data, &context);

NT_IF_FAIL_LEAVE(LcGetFileLock(&nameInfo->Name, &fileLockEvent));

NT_IF_FAIL_LEAVE(LcFetchRemoteFile(FltObjects, &context->RemoteFilePath, &nameInfo->Name, context->UseCustomHandler, &bytesFetched));

NT_IF_FAIL_LEAVE(LcUntagFile(FltObjects, &nameInfo->Name));
NT_IF_FAIL_LEAVE(FltDeleteStreamContext(FltObjects->Instance, FltObjects->FileObject, NULL));

In fact, RPs have a wide range of uses and open up many possibilities for solving various problems. We will analyze one of them by solving the following problem.

Problem

Toy Half-life can run in two modes: software mode and hardware mode, which differ in the way graphics are rendered in the game. A little digging the toy into IDA Pro, you can see that the modes differ in the loaded method LoadLibrary library: sw.dll or hw.dll

Depending on the input arguments (for example “-Soft”) one or another line is selected and thrown into the function call LoadLibrary

The essence of the task is to prevent the toy from loading into software mode, yes so that the user does not notice it. Ideally, so that the user does not even realize that regardless of his choice, he is loaded into hardware mode

Of course, it would be possible to patch the executable file, or replace the dll file, and even just copy hw.dll and rename the copy to sw.dllbut we are not looking for easy ways. Plus, if the game is updated or reinstalled, the effect will disappear.

Decision

I suggest the following solution: write a small mini filter driver. It will run continuously and will not be affected by reinstalling and updating the game. Let’s register the driver for the operation IRP_MJ_CREATEbecause every time the executable calls LoadLibrary, it essentially opens the library file. As soon as we notice that the game process is trying to open the library sw.dll, we will return the status STATUS_REPARSE and ask to repeat the request, but for opening hw.dll… Result: the library we needed opened, although user space asked us for another

To begin with, we need to understand what process is trying to open the library, because we need to turn our trick only for the game process. To do this, right in DriverEntry we will need to call PsSetCreateProcessNotifyRoutine and register a method that will be called every time a new process appears in the system.

NT_IF_FAIL_LEAVE(PsSetCreateProcessNotifyRoutine(IMCreateProcessNotifyRoutine, FALSE));

In this method, we must get the name of the executable file. To do this, you can use ZwQueryInformationProcess

NT_IF_FAIL_LEAVE(PsLookupProcessByProcessId(ProcessId, &eProcess));

NT_IF_FAIL_LEAVE(ObOpenObjectByPointer(eProcess, OBJ_KERNEL_HANDLE, NULL, 0, 0, KernelMode, &hProcess));

NT_IF_FAIL_LEAVE(ZwQueryInformationProcess(hProcess,
                                               ProcessImageFileName,
                                               buffer,
                                               returnedLength,
                                               &returnedLength));

If it matches the desired one, in our case hl.exe, then you need to remember it PID

target = &Globals.TargetProcessInfo[i];
if (RtlCompareUnicodeString(&processNameInfo->Name, &target->TargetName, TRUE) == 0)
{
      target->NameInfo = processNameInfo;
      target->isActive = TRUE;
      target->ProcessId = ProcessId;

      LOG(("[IM] Found process creation: %wZn", &processNameInfo->Name));
}

So, now we have saved in the global object PID the process of our game. You can go to pre create callback… There we should get the name of the file to open. This will help us FltGetFileNameInformation… This function cannot be called on DPC interrupt level (read more about IRQL), however, we are going to make the call exclusively on pre create, which guarantees us a level not higher than APC

status = FltGetFileNameInformation(Data, FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_FILESYSTEM_ONLY | FLT_FILE_NAME_ALLOW_QUERY_ON_REPARSE, &fileNameInfo);

Then everything is simple if our name is sw.dll, then you need to replace it with FileObject on hw.dll… And return the status STATUS_REPARSE

// may be it is sw
if (RtlCompareUnicodeString(&FileNameInfo->Name, &strSw, TRUE) == 0)
{
// concat
NT_IF_FAIL_LEAVE(IMConcatStrings(&replacement, &FileNameInfo->ParentDir, &strHw));

// then need to change
NT_IF_FAIL_LEAVE(IoReplaceFileObjectName(FileObject, replacement.Buffer, replacement.Length));
}

Data->IoStatus.Status = STATUS_REPARSE;
Data->IoStatus.Information = IO_REPARSE;
return FLT_PREOP_COMPLETE;

Of course, the implementation of the project as a whole is somewhat more complex, but I tried to reveal the main points. The whole project with details here

Testing

To simplify our test runs, instead of a game, we will run a small application and libraries with the following content:

// testapp.exe
#include "TestHeader.h"

int main()
{
	TestFunction();
	return 0;
}

// testdll0.dll
#include "../include/TestHeader.h"
#include <iostream>

// This is an example of an exported function.
int TestFunction()
{
std::cout << "hello from test dll 0" << std::endl;
return 0;
}

// testdll1.dll
#include "../include/TestHeader.h"
#include <iostream>

// This is an example of an exported function.
int TestFunction()
{
std::cout << "hello from test dll 1" << std::endl;
return 0;
}

Let’s collect testapp.exe from testdll0.dll, and copy them to the virtual machine (which is where we plan to launch), and also prepare testdll1.dll… The task of our driver will be to replace testdll0 on testdll1… We will understand that we have succeeded if we see the message in the console “Hello from test dll 1”, instead of “Hello from test dll 0”… Let’s run it without a driver to make sure our test application works correctly:

Now let’s install and run the driver:

Now, running the same application, we will get a completely different output to the console, since another library was loaded

Plus, in the application written for the driver, we see logs that say that we really caught our open request and replaced one file with another. The test was successful, it’s time to test the solution on the game itself. Enjoy 🙂

I hope it was helpful and interesting. Write your questions in the comments, I will try to answer.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *