geni.site

Anti-anti-piracy and hiding from the PEB

Introduction

This is a short follow-up to the previous post on defeating RepoXR’s anti-piracy.

This post assumes the reader is a bit more familiar with Windows crap than last time. I suggest reading the first part if you haven’t already, as most of the terminology here has been covered and explained in said previous post.

Five months after my original blog post, the author of RepoXR has finally released an update. I’m curious to see if anything’s changed.

As for most of this I didn’t have the game on my system, we’re going to strictly perform a static analysis here. I did download it for the end of the post though.

Binary diff

For this, we’re going to use the Diaphora IDA plugin. First we’ll take a look at the old anti-piracy code to see if there’s been any changes.

I lost my old IDA database, so the names aren’t perfect in the screenshots… sorry.

First, we’ll take a look at the partial matches, since those are the most likely to lead us to discover any changes to the anti-piracy; the mod author seems to like sneaking the anti-piracy code into existing functions.

Now we want to explore the first partial match, which as we can see is the function where the old anti-piracy code was located.

It looks like the old GetModuleHandle method has been retired. We have to take a look at the other partial matches to see if perhaps a new detection method has been snuck into a new function.

I will spare you the details of the other partial matches here as they appear to mostly be compiler-related differences.

We do eventually reach sub_180024290, with a very suspicious block of code that appears to do some PEB walking to check for the presence of a specific module.

Analysis

This code, at a glance, appears to walk the PEB and check for the presence of some module in its in-memory module list. Here’s a somewhat cleaned-up version:

PEB_LDR_DATA p_InMemoryOrderModuleList = &NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList;
for (PLIST_ENTRY current_entry = p_InMemoryOrderModuleList->Flink; current_entry != p_InMemoryOrderModuleList; current_entry = current_entry->InLoadOrderLinks.Flink) {
    PLDR_DATA_TABLE_ENTRY current_module = CONTAINING_RECORD(current_entry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    if (current_module == NULL) {
        continue;
    }
    dll_name      = current_module->FullDllName;
    base_dll_name = dll_name;

    // NOTE(geni): boring path cleaning code that i didn't care enough to clean up
    v8 = current_module->FullDllName.Buffer;
    if (v8) {
        if (current_module->FullDllName.Length) {
            v9  = v8 + 2 * (current_module->FullDllName.Length >> 1) - 2;
            v10 = v9;
            if (v9 >= v8) {
                while (*v10 != '\\' && *v10 != '/') {
                    v10 -= 2LL;
                    if (v10 < v8) {
                        goto cleaned;
                    }
                }
                v8 = (v10 + 2);
            }
        cleaned:
            base_dll_name.Buffer        = v8;
            base_dll_name.Length        = 2 * (((v9 - v8) >> 1) + 1);
            base_dll_name.MaximumLength = base_dll_name.Length;
            dll_name                    = base_dll_name;
        }
    }
    String = dll_name;
    if (RtlHashUnicodeString(&String, 1u, 1u, HashValue) < 0 || HashValue[0] != 605587766) {
        continue;
    }

    if (suspicious_thread) {
        if (!WaitForSingleObject(suspicious_thread, 0)) {
            break;
        }
    } else {
        image_base              = current_module->DllBase;
        openxr_loader_module    = LoadLibraryA("openxr_loader");
        thread_param            = malloc(sizeof(ThreadParam));
        thread_param->imageBase = image_base;
        thread_param->hModule   = openxr_loader_module;
        suspicious_thread       = CreateThread(0, 0, suspicious_thread_function, thread_param, 0, 0);
    }
}

if (suspicious_thread) {
    CloseHandle(suspicious_thread);
}

The RtlHashUnicodeString call is quite suspicious… Why are we looking for a specific DLL with a name that hashes into 605587766? I get the hunch that this is just a way to obfuscate a GetModuleHandle call for OnlineFix64.dll

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

#pragma comment(lib, "ntdll")

int main() {
    UNICODE_STRING str;
    RtlInitUnicodeString(&str, L"OnlineFix64.dll");

    ULONG hash;
    RtlHashUnicodeString(&str, 1, 1, &hash);

    printf("RtlHashUnicodeString for \"OnlineFix64.dll\" is: %lu\n", hash);
}

Of course it is.

Anti-piracy

Now that we know this is just anti-piracy, we can try to look into how it’s implemented. As we can quite clearly see, it’s in the aptly labeled suspicious_thread_function.

__int64 __fastcall suspicious_thread_function(struct ThreadParam* parameter) {
    struct _MEMORY_BASIC_INFORMATION mbi;
    DWORD                            old_protect;

    byte* start         = parameter->imageBase;
    byte* currentRegion = parameter->imageBase;
    // NOTE(geni): idc enough. this is obviously SizeOfImage from the PE header
    byte* end = parameter->imageBase +
                *(parameter->imageBase + *(parameter->imageBase + 15) + 80);

    int random_probably = sub_1800512A8();
    Sleep(random_probably % 5001 + 5000);  // 5-10 seconds

    if (start < end) {
        do {
            if (!VirtualQuery(currentRegion, &mbi, sizeof mbi)) {
                break;
            }

            if (mbi.AllocationBase == start) {
                VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_READONLY,
                               &old_protect);
            }
            currentRegion += mbi.RegionSize;
        } while (currentRegion < end);
    }
    FreeLibrary(parameter->hModule);
    return 0;
}

The new thread sleeps for 5-10 seconds (for no other reason than to mess with the pirate), then iterates through the memory regions of the OnlineFix module and sets each one’s memory protection to read-only.
This clears the execution flag, causing a crash and a stack trace that directly references OnlineFix64.dll:

OnlineFix64!Steam_TerminateGameConnection+0x8a55d3
ntdll!LdrpCallInitRoutineInternal+0x22
ntdll!LdrpCallInitRoutine+0x93
ntdll!LdrpInitializeThread+0x297
ntdll!LdrpInitialize+0xa7
ntdll!LdrpInitializeInternal+0x5a
ntdll!LdrInitializeThunk+0xe

This is a stark contrast from the more stealthy stack trace of before, which would’ve made one believe that the mod is simply crashing:

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

The goal of this, as I had suspected and later confirmed by contacting the mod’s author (shoutout for being cool with me breaking your DRM), is to show OnlineFix64.dll at the top of the stack trace, so that people blame OnlineFix rather than the mod’s openxr_loader, and are hence more likely to just buy the game.

However, there’s a more fun side effect, in that it obfuscates the location of the code that actually causes the crash. This makes it slightly harder to patch out, at least for an LLM or something.

Proof of Concept

The more technically inclined may have been thinking about how the old proof of concept from the first part was supposed to hide the module from the PEB, and would probably be wondering what the point of this whole article is. And they would be somewhat correct in thinking this, if we ignore the fact that the PoC doesn’t hide it from all of the module lists. This is the exact oversight that caused it to fail after the checks were changed.

Here is our old PoC again, for context:

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

Do you notice how it only unlinks it from InLoadOrderLinks and HashLinks? You may recall that this was done because those were the two lists that GetModuleHandleA looks in.
But where does our new PEB walker look?

PEB_LDR_DATA p_InMemoryOrderModuleList = &NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList;
for (PLIST_ENTRY current_entry = p_InMemoryOrderModuleList->Flink; current_entry != p_InMemoryOrderModuleList; current_entry = current_entry->InLoadOrderLinks.Flink) {

Ah, it looks in InMemoryOrderModuleList instead.

The solution is hence quite simple; we unlink the module from LDR_DATA_TABLE_ENTRY’s InMemoryOrderLinks:

LIST_ENTRY* memBlink = module->InMemoryOrderLinks.Blink;
LIST_ENTRY* memFlink = module->InMemoryOrderLinks.Flink;
if (memBlink && memFlink) {
    memBlink->Flink = memFlink;
    memFlink->Blink = memBlink;
}

And yet again, we end the post with a simple 6 line addition… Perhaps we’ll see a third part some months from now.

Props to DaXcess for being cool about this whole thing.