geni.site

Anti-anti-piracy and hiding from GetModuleHandle

This has certainly been done before. It’s also most certainly been done better, as this will only hide the module from GetModuleHandle and there are plenty other ways left to find it. This is done and written more as a fun and educational exercise; a restriction I placed on myself going into this was to not look up “how to hide module from GetModuleHandle” on Google.

Introduction

While trying to play R.E.P.O. using the RepoXR mod yesterday, the game would mysteriously crash on startup. The last message it would output in the log was [Info :OpenXR Loader] Attempting to initialize OpenXR on SteamVR before the Unity crash handler would show up on screen.

Googling the crash or searching for it on the mod’s Discord server provided no useful results. Contacting the mod’s author is no fun.

Crash analysis

Unity crashes leave a minidump file without us even having to bother enabling automatic memory dumps in Windows. How nice.

Let’s analyze the minidump in WinDbg:

It looks like the crash is coming from openxr_loader.dll. Our next step is going through the call stack to try to figure out what this code’s doing. Whatever’s at the top of it is trying to dereference a NULL pointer.

Here’s our call stack:

openxr_loader!xrWaitSwapchainImage+0x3c141
openxr_loader!xrGetInstanceProcAddr+0x7ae7
openxr_loader!xrGetInstanceProcAddr+0x7668
openxr_loader+0x7228
openxr_loader+0x86d3
openxr_loader+0x15468
UnityOpenXR!DiagnosticReport_StartReport+0x7c96
UnityOpenXR!unity_ext_RequestEnableExtensionString+0x45
0x00000156`9f3c3a52

First, we’re going to see what’s at xrWaitSwapchainImage+0x3c141:

While at first sight this looks like some weird indecipherable SIMD mess, from a quick analysis we can find out that this is just the CRT’s strlen. Not particularly useful, so we go down the call stack to xrGetInstanceProcAddr+0x7ae7:

I’ve named things appropriately to make it easier to understand, but you can tell what’s going on just off the GetModuleHandleA call. Yeah, I’ve been intentionally omitting that I’m running a cracked version of the game up until now, since it didn’t originally cross my mind that it could be a problem…

Anti-piracy

The mod’s openxr_loader.dll has an anti-piracy check (yes, an anti-piracy measure in a mod for a game that has no anti-piracy, whatever man). Presumably it’s put in a random DLL as a red herring (because who seriously expects anti-piracy in openxr_loader.dll), so that the mod author can laugh at pirates when they ask for support. (honestly I don’t blame him too much it’s probably funny)

I personally like to believe it’s more like a filter for Eastern European children who can’t speak English rather than an actual anti-piracy measure.

GetModuleHandle Internals

Now while the easy solution would be to just patch openxr_loader.dll in BepInEx/patchers/RepoXR/RuntimeDeps/, where’s the fun in that? Instead, we’re going to write a BepInEx plugin that will hide the “OnlineFix64.dll” module.

To do this, we first have to ask ourselves: How does GetModuleHandle work?
One way to figure this out is to reverse-engineer Windows, or read a book on NT internals or something, but I think there’s a more fun option: I feel like we can most likely rely on ReactOS to roughly match what Windows does.

You don’t need to understand or go through every line here. The relevant bits will be (admittedly briefly) explained. Additionally, I’ve taken out parts of the functions that I didn’t really deem necessary for this article.
HMODULE
WINAPI
GetModuleHandleW(LPCWSTR lpModuleName)
{
    HMODULE hModule;
    NTSTATUS Status;

    /* If current module is requested - return it right away */
    if (!lpModuleName)
        return ((HMODULE)NtCurrentPeb()->ImageBaseAddress);

    /* Use common helper routine */
    Status = BasepGetModuleHandleExW(TRUE,
                                     GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                                     lpModuleName,
                                     &hModule);

    /* If it wasn't successful - return 0 */
    if (!NT_SUCCESS(Status)) hModule = 0;

    /* Return the handle */
    return hModule;
}

This is ReactOS’s GetModuleHandleW implementation. I’m not going to go through and explain all of the inner workings of it, but the general path to get to any meaningful code in our case is:
GetModuleHandleA -> GetModuleHandleW -> BasepGetModuleHandleExW -> GetModuleHandleForUnicodeString -> LdrGetDllHandle -> LdrGetDllHandleEx

Lookup logic

We’ll now take a look at LdrGetDllHandleEx where the actually interesting stuff begins:

NTSTATUS
NTAPI
LdrGetDllHandleEx(IN ULONG Flags,
                  IN PWSTR DllPath OPTIONAL,
                  IN PULONG DllCharacteristics OPTIONAL,
                  IN PUNICODE_STRING DllName,
                  OUT PVOID *DllHandle OPTIONAL)
{
    NTSTATUS Status;
    PLDR_DATA_TABLE_ENTRY LdrEntry;
    UNICODE_STRING RedirectName, DllString1, RawDllName;
    PUNICODE_STRING pRedirectName, CompareName;
    PWCHAR p1, p2, p3;
    BOOLEAN Locked, RedirectedDll;
    ULONG_PTR Cookie;
    ULONG LoadFlag, Length;

    ...........

    /* Use the cache if we can */
    if (LdrpGetModuleHandleCache)

	...........

Looks like there’s a cache that stores the last module which GetModuleHandle was called for. I don’t think this should be an issue, and in practice it hasn’t been.

    /* Do the lookup */
    if (LdrpCheckForLoadedDll(DllPath,
                              &RawDllName,
                              ((ULONG_PTR)DllPath == 1) ? TRUE : FALSE,
                              RedirectedDll,
                              &LdrEntry))
    {
        /* Update cached entry */
        LdrpGetModuleHandleCache = LdrEntry;

        /* Return success */
        Status = STATUS_SUCCESS;
    }

LdrpCheckForLoadedDll appears to be where the actual lookup happens.

/* NOTE: This function is b0rked and in the process of being slowly unf*cked */
BOOLEAN
NTAPI
LdrpCheckForLoadedDll(IN PWSTR DllPath,
                      IN PUNICODE_STRING DllName,
                      IN BOOLEAN Flag,
                      IN BOOLEAN RedirectedDll,
                      OUT PLDR_DATA_TABLE_ENTRY *LdrEntry)
{
    ULONG HashIndex;
    PLIST_ENTRY ListHead, ListEntry;
    PLDR_DATA_TABLE_ENTRY CurEntry;
    BOOLEAN FullPath = FALSE;
    PWCHAR wc;
    WCHAR NameBuf[266];
    UNICODE_STRING FullDllName, NtPathName;
    ULONG Length;
    OBJECT_ATTRIBUTES ObjectAttributes;
    NTSTATUS Status;
    HANDLE FileHandle, SectionHandle;
    IO_STATUS_BLOCK Iosb;
    PVOID ViewBase = NULL;
    SIZE_T ViewSize = 0;
    PIMAGE_NT_HEADERS NtHeader, NtHeader2;

	......

    /* Look in the hash table if flag was set */
lookinhash:
    if (Flag)
    {
        /* Get hash index */
        HashIndex = LDR_GET_HASH_ENTRY(DllName->Buffer[0]);

        /* Traverse that list */
        ListHead = &LdrpHashTable[HashIndex];
        ListEntry = ListHead->Flink;
        while (ListEntry != ListHead)	
        {
            /* Get the current entry */
            CurEntry = CONTAINING_RECORD(ListEntry, LDR_DATA_TABLE_ENTRY, HashLinks);

            /* Check base name of that module */
            if (RtlEqualUnicodeString(DllName, &CurEntry->BaseDllName, TRUE))
            {
                /* It matches, return it */
                *LdrEntry = CurEntry;
                return TRUE;
            }

            /* Advance to the next entry */
            ListEntry = ListEntry->Flink;
        }

        /* Module was not found, return failure */
        return FALSE;
    }

	......

    /* Go check the hash table */
    if (!FullPath)
    {
        Flag = TRUE;
        goto lookinhash;
    }

    /* Loop the module list */
    ListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
    ListEntry = ListHead->Flink;
    while (ListEntry != ListHead)
    {
        /* Get the current entry and advance to the next one */
        CurEntry = CONTAINING_RECORD(ListEntry,
                                     LDR_DATA_TABLE_ENTRY,
                                     InLoadOrderLinks);
        ListEntry = ListEntry->Flink;

        /* Check if it's being unloaded */
        if (!CurEntry->InMemoryOrderModuleList.Flink) continue;

        /* Check if name matches */
        if (RtlEqualUnicodeString(&FullDllName,
                                  &CurEntry->FullDllName,
                                  TRUE))
        {
            /* Found it */
            *LdrEntry = CurEntry;
            return TRUE;
        }
    }

    ...........

    /* Get pointer to the NT header of this section */
    Status = RtlImageNtHeaderEx(0, ViewBase, ViewSize, &NtHeader);
    if (!(NT_SUCCESS(Status)) || !(NtHeader))
    {
        /* Unmap the section and fail */
        NtUnmapViewOfSection(NtCurrentProcess(), ViewBase);
        return FALSE;
    }

    /* Go through the list of modules again */
    ListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;
    ListEntry = ListHead->Flink;
    while (ListEntry != ListHead)
    {
        /* Get the current entry and advance to the next one */
        CurEntry = CONTAINING_RECORD(ListEntry,
                                     LDR_DATA_TABLE_ENTRY,
                                     InLoadOrderLinks);
        ListEntry = ListEntry->Flink;

        /* Check if it's in the process of being unloaded */
        if (!CurEntry->InMemoryOrderModuleList.Flink) continue;

        /* The header is untrusted, use SEH */
        _SEH2_TRY
        {
            /* Check if timedate stamp and sizes match */
            if ((CurEntry->TimeDateStamp == NtHeader->FileHeader.TimeDateStamp) &&
                (CurEntry->SizeOfImage == NtHeader->OptionalHeader.SizeOfImage))
            {
                /* Time, date and size match. Let's compare their headers */
                NtHeader2 = RtlImageNtHeader(CurEntry->DllBase);
                if (RtlCompareMemory(NtHeader2, NtHeader, sizeof(IMAGE_NT_HEADERS)))
                {
                    /* Headers match too! Finally ask the kernel to compare mapped files */
                    Status = ZwAreMappedFilesTheSame(CurEntry->DllBase, ViewBase);
                    if (NT_SUCCESS(Status))
                    {
                        /* This is our entry!, unmap and return success */
                        *LdrEntry = CurEntry;
                        NtUnmapViewOfSection(NtCurrentProcess(), ViewBase);
                        _SEH2_YIELD(return TRUE;)
                    }
                }
            }
        }
        _SEH2_EXCEPT (EXCEPTION_EXECUTE_HANDLER)
        {
            _SEH2_YIELD(break;)
        }
        _SEH2_END;
    }

	...........
}

The comment above the function definition doesn’t inspire much confidence in the validity of the implementation. However, I think the “b0rked” stuff doesn’t really matter in our case.

The uninitiated may be confused about the CONTAINING_RECORD macro.
It’s used for getting the offset of the beginning of a struct from an intrusive linked list: a linked list that points to other linked lists. Here’s a diagram of the data structure that I quickly made that may or may not help because I suck at explaining things:

You can sort of think of the macro as rewinding to the beginning of the struct from the linked list entry. It’s defined as follows:

#define CONTAINING_RECORD(address, type, field) \
   ((type *)(((ULONG_PTR)address) - (ULONG_PTR)(&(((type *)0)->field))))

From looking at lines such as ListHead = &NtCurrentPeb()->Ldr->InLoadOrderModuleList;, we can make a somewhat educated guess that we have to remove the module from InLoadOrderModuleList. At the lookinhash label, we find out that this function will first try to find our module in LdrpHashTable.

lookinhash:
    if (Flag)
    {
        /* Get hash index */
        HashIndex = LDR_GET_HASH_ENTRY(DllName->Buffer[0]);

        /* Traverse that list */
        ListHead = &LdrpHashTable[HashIndex];
        ListEntry = ListHead->Flink;
        while (ListEntry != ListHead)	
        {
            /* Get the current entry */
            CurEntry = CONTAINING_RECORD(ListEntry, LDR_DATA_TABLE_ENTRY, HashLinks);

            /* Check base name of that module */
            if (RtlEqualUnicodeString(DllName, &CurEntry->BaseDllName, TRUE))
            {
                /* It matches, return it */
                *LdrEntry = CurEntry;
                return TRUE;
            }

            /* Advance to the next entry */
            ListEntry = ListEntry->Flink;
        }

        /* Module was not found, return failure */
        return FALSE;
    }

Conveniently for us, it looks like the base name check will fail if we remove the module from HashLinks.
Our goal then is to make both the LdrpHashTable search and the InLoadOrderModuleList search fail.

Implementation

Finally, our plan is roughly as follows:

  1. Get the LDR_DATA_TABLE_ENTRY for our module by iterating InLoadOrderModuleList
  2. Remove it from InLoadOrderModuleList
  3. Remove it from HashLinks

But first, how do we even get to InLoadOrderModuleList from our plugin?

Accessing the module list

The first step here is figuring out what NtCurrentPeb() is. ReactOS defines it as:
#define NtCurrentPeb() (NtCurrentTeb()->ProcessEnvironmentBlock)
The Process Environment Block is basically a data structure the OS uses for process data bookkeeping. There’s plenty more to it of course, but the topic of this post is not a deep-dive into NT internals. Our shortest path to it will be through NtCurrentTeb().

Fortunately, we can use the definition provided to us by the Windows SDK as we only need the Ldr member:

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;

We can get to the Thread Environment Block by calling NtCurrentTeb, which is also provided to us by the Windows SDK. This is also one of the few NT functions that are documented in the MSDN:
“The NtCurrentTeb routine returns a pointer to the Thread Environment Block (TEB) of the current thread.”

_TEB * NtCurrentTeb();

https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-ntcurrentteb

The Windows SDK definition for the TEB is as follows:

typedef struct _TEB {
  PVOID Reserved1[12];
  PPEB  ProcessEnvironmentBlock;
  PVOID Reserved2[399];
  BYTE  Reserved3[1952];
  PVOID TlsSlots[64];
  BYTE  Reserved4[8];
  PVOID Reserved5[26];
  PVOID ReservedForOle;
  PVOID Reserved6[4];
  PVOID TlsExpansionSlots;
} TEB, *PTEB;
A fuller definition of the PEB and TEB structs can be found in the Wine, ReactOS or Process Hacker winternl.h headers.

Applying what we’ve learned so far, we can get to InLoadOrderModuleList through something like:

NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InLoadOrderModuleList

The Windows SDK gives us the following definitions:

typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID Reserved3[2];
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union
    {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

InLoadOrderModuleList is missing from PEB_LDR_DATA and HashLinks is missing from LDR_DATA_TABLE_ENTRY… There are plenty of other definitions of this struct out there which do include these. We’ve been going off ReactOS code so far, but we’re going to use Process Hacker’s definitions since they’re more complete (not that it really matters):

typedef struct _PEB_LDR_DATA
{
    ULONG Length;
    BOOLEAN Initialized;
    HANDLE SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
    PVOID EntryInProgress;
    BOOLEAN ShutdownInProgress;
    HANDLE ShutdownThreadId;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY
{
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;	
	...........
    LIST_ENTRY HashLinks;
    ...........
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

In our case, each link in InLoadOrderModuleList is really a LDR_DATA_TABLE_ENTRY.

Proof of Concept

Now that we have everything we need, we can begin implementing our logic:

PEB* peb = NtCurrentTeb()->ProcessEnvironmentBlock;
LIST_ENTRY* head = &peb->Ldr->InLoadOrderModuleList;

LIST_ENTRY* cur_entry = head->Flink;

while (cur_entry != head) {    
    LDR_DATA_TABLE_ENTRY* module = CONTAINING_RECORD(
        cur_entry, 
        LDR_DATA_TABLE_ENTRY, 
        InLoadOrderLinks
    );

    if (module->BaseDllName.Buffer && StrCmpW(module->BaseDllName.Buffer, L"OnlineFix64.dll") == 0) {
        // NOTE(geni): Remove from InLoadOrderLinks
        LIST_ENTRY* loadBlink = module->InLoadOrderLinks.Blink;
        LIST_ENTRY* loadFlink = module->InLoadOrderLinks.Flink;
        if (loadBlink && loadFlink) {
            loadBlink->Flink = loadFlink;
            loadFlink->Blink = loadBlink;
        }

        // NOTE(geni): Remove from HashLinks
        LIST_ENTRY* hashBlink = module->HashLinks.Blink;
        LIST_ENTRY* hashFlink = module->HashLinks.Flink;
        if (hashBlink && hashFlink) {
            hashBlink->Flink = hashFlink;
            hashFlink->Blink = hashBlink;
        }
        break;
    }
    cur_entry = cur_entry->Flink;
}

All that said, just to end the post with a sub-100 LOC solution…

The initial implementation was in C since it was possible entirely with what we’ve learned up until now (also I just prefer writing C).

I later reimplemented it as a generic BepInEx plugin, which you can use in other Unity games where mod authors have also taken it upon themselves to end video game piracy. There’s nothing really interesting about it and I didn’t write much about it because some of the stuff we did here apparently isn’t easily doable in .NET, so I googled how to do it… sorry. See the end of the post for a link.

Conclusion

Anti-piracy checks in mods are stupid. The game’s developers would just do it themselves if they cared… but I’m also well aware of how stupid and monolingual most pirates are. So… whatever.

Go download the mod here, and enjoy your illegitimately acquired R.E.P.O. (or any other Unity games with mods that check for OnlineFix the same way):
https://github.com/geniiii/BepInEx-HideOnlineFix