Reversing Thread
  • Home
  • About me
@2024 Reversing Thread - All Right Reserved.
Blog

Battleye Analysis Part 1 – Window Detection

by reversingthread.info 20 de June de 2024

Introduction

This research delves into Battleye’s detection mechanisms, this part 1 is focused on identifying and analyzing suspicious windows. This analysis aims to understand these methods for research purposes only, not to bypass or attack the software. The provided code was extracted and has been beautified for clarity.

Those detection were analysed from the game DayZ, at date 06/20/2024.

Window detection overview

BattlEye employs three primary methods for detecting suspicious windows. The first method involves scanning for blacklisted window names and class names, while the second method focuses on analyzing window styles and attributes, such as topmost or transparent properties, the third one is a check for validating if the enumerated windows was actually run. Additionally, its implements a fourth layer of protection by verifying the integrity of the functions used to perform these checks, ensuring that they have not been hooked or tampered with.

Breakdown

1. Report Buffer and Report Function

BattlEye employs a 0x5400-byte report buffer to store detection data. The buffer’s first byte is always 0, the second identifies the report type, and the rest contains detection details. BattlEye uses malloc for dynamic allocation. A separate 0x5000-byte buffer temporarily holds detection data before copying it to the report buffer prior to sending.

struct Report
{
    BYTE       Unknown;
    ReportType ReportType;
    BYTE       ReportData[21502];
};

using BattleyeReport_t = void(BYTE* reportBuffer, uint32_t reportSize, uint32_t Unknown);

BattleyeReport_t BattleyeReport = nullptr;

2. Report Types

While this enum is incomplete and will be expanded upon later, it contains the three primary report types relevant to window detection. It is important to note that the DetectedAbnormalWindow_Handle_File report type also includes checks for blacklisted window names, opened handles and files, which will be discussed in further detail later in this analysis.

enum class ReportType : uint8_t
{
    DetectedBlackListedWindow          = 0x33,
    DetectedAbnormalWindow_Handle_File = 0x3C,
    DetectedFailToEnumerateWindow      = 0x44
};

3. Window Iteration

BattlEye initiates the window detection process by retrieving the topmost window using the GetTopWindow function. It then enters an infinite loop, iterating through the windows by calling GetWindow with the current topWindow as the first argument and GW_HWNDNEXT as the second. The loop continues until the parentWindow becomes NULL. The indexAtBufferReport is incremented by 0x1C plus the size of the data added to the report buffer for each window. The detection process persists as long as indexAtBufferReport remains smaller than 0x4E80. The detection’s only occurs on windows that do not belong to the game process itself.

void CheckWindows()
{
    DWORD processId       = 0;
    char  windowName[220] = {};
    HWND  parentWindow    = nullptr;
    HWND  currentWindow;


    HWND topWindow = GetTopWindow(nullptr);
    bool exitLoop  = false;


    int indexAtBufferReport = 4;
    while (true)
    {
        // Get the process ID of the current window
        GetWindowThreadProcessId(topWindow, &processId);

        // Check if we are not in same process.
        if (GetCurrentProcessId() != processId)
        {
            // Get the window name
            GetWindowTextA(topWindow, &windowName[2], 128);

            // Detect window name.
            DetectWindowNames(topWindow, windowName);

            // Detect Windows Style & More Window name.
            DetectAbnormalWindows(topWindow, parentWindow, windowName, processId);
        }


        if (!parentWindow && GetCurrentProcessId() == processId && (
                currentWindow = GetWindow(topWindow, GW_CHILD)) !=
            nullptr)
        {
            parentWindow = topWindow;
            topWindow    = currentWindow;
        }
        else
        {
            while (true)
            {
                // Get next window
                topWindow = GetWindow(topWindow, GW_HWNDNEXT);
                if (topWindow)
                {
                    if (indexAtBufferReport <= 0x4E80)
                    {
                        break;
                    }
                }

                if (!parentWindow)
                {
                    exitLoop = true;
                    break;
                }
                topWindow    = parentWindow;
                parentWindow = nullptr;
            }
        }


        // This is the minimum size increment each report. The index is calculated by also adding the data from the window which was detected.
        indexAtBufferReport += 0x1C;

        // We reached the end of the windows
        if (exitLoop)
            break;
    }

    std::printf("[+] Done!\n");
}

4. Detecting blacklisted window names

This is the first method, BattlEye retrieves the class name and window name of each window and compares them against a predefined list of blacklisted names. If a match is found, a report is sent. It is important to note that the blacklisted window names in this case are specifically targeted towards the game DayZ. When analyzing other games, such as Rainbow Six or PUBG, or any other Battleye game, the blacklisted strings may differ based on the specific cheats discovered in those environments. Additionally, the class name detection here is used to identify a name that is later utilized in another aspect of BattlEye’s detection mechanism, which is not directly related to the window itself.

#define REPORT_BUFFER_LENGHT 0x5400

void DetectWindowNames(HWND hwndWindow, char* windowName)
{
    int  reportSize = 13; // The initial reportSize is 13.
    int  nameLength = strlen(windowName);
    bool foundDetectedFlag; // This flag is used in futher shellcode.
    bool foundBlackListedWindow;

    // Infinite loop
    for (int j = 0; ; ++j)
    {
        // Check if there atleast 5 characters in the window name
        if (j >= nameLength - 5)
        {
            // GetClassNameA
            if (GetClassNameA(hwndWindow, windowName, 64) && !strcmp(windowName, "TaskMana"))
            {
                foundDetectedFlag = true;
            }
        }
        else
        {
            // Detected the following blacklisted window names
            if (!strcmp(&windowName[j + 2], "Chod's") ||
                !strcmp(&windowName[j + 2], "Satan5") ||
                !strcmp(&windowName[j + 2], "kernelch"))
            {
                foundBlackListedWindow = true;
                break;
            }
        }
    }

    if (foundBlackListedWindow)
    {
        Report report     = {};
        report.Unknown    = 0;
        report.ReportType = ReportType::DetectedBlackListedWindow;

        // Check 
        if (reportSize + nameLength + 3 <= REPORT_BUFFER_LENGHT)
        {
            *reinterpret_cast<uint16_t*>(&report.Unknown + reportSize) = nameLength + 1; // Write to offset 13
            for (int i                            = 0; i < nameLength + 1; ++i)
                report.ReportData[i + reportSize] = windowName[i]; // Start at offset 15


            reportSize += nameLength + 3;
            BattleyeReport(&report, reportSize, 0);
        }
    }
}

5. Detection of abnormal windows

The detection of abnormal windows primarily relies on analyzing window attributes. The system retrieves both GWL_STYLE and GWL_EXSTYLE using GetWindowLongA, and then performs a series of attribute checks. If any of these checks are triggered, a report is sent to the server.

It’s worth noting that while the checks in the pseudocode are represented as hexadecimal values for brevity, they correspond to standard Windows API definitions such as WS_EX_TOPMOST and WS_EX_TRANSPARENT.

Interestingly, this detection mechanism also incorporates checks for specific window names. However, these are reported using the DetectedAbnormalWindow_Handle_File rather than the previously discussed blacklisted window detection.

Upon matching any of the specified flags, BattlEye compiles a report containing the window’s attributes and sends it to their servers for further analysis. This comprehensive approach enables BattlEye to detect a wide range of potential cheat software, even those attempting to disguise themselves through careful window property manipulation.

void DetectAbnormalWindows(HWND hwndWindow, HWND parentWindow, char* windowName, int windowPid)
{
    int     startIndex            = 4;
    int     startIndex2           = 0;
    wchar_t windowNameUnicode[64] = {};
    char    windowNameASCII[230]  = {};
    bool    notepadWindowFound    = false; // This is a check for the future
    RECT    windowRect            = {};
    int     reportSize            = 0;

    // Get the window style flags
    int styleFlags   = GetWindowLongA(hwndWindow, GWL_STYLE);
    int exStyleFlags = GetWindowLongA(hwndWindow, GWL_EXSTYLE);

    // Get the window rect
    GetWindowRect(hwndWindow, &windowRect);

    // Check if windows is hidden and the window name is "MSPaintA"
    if (!(styleFlags & WS_VISIBLE) && !strcmp(windowName, "MSPaintA"))
    {
        HANDLE targetProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, windowPid);
        if (targetProcess)
        {
            CloseHandle(targetProcess);
            return;
        }

        if (GetLastError() != ERROR_INVALID_PARAMETER)
            return;
    }

    int        nameLength             = GetWindowTextW(hwndWindow, windowNameUnicode, 64);
    const auto resultBytesWrittenName = WideCharToMultiByte(
                                                            CP_UTF8,
                                                            0,
                                                            windowNameUnicode,
                                                            nameLength,
                                                            windowNameASCII + startIndex + 1,
                                                            255,
                                                            nullptr,
                                                            nullptr);

    *(BYTE*)(windowNameASCII + startIndex) = resultBytesWrittenName;
    startIndex2                            = startIndex + resultBytesWrittenName + 1;

    nameLength                         = GetClassNameW(hwndWindow, windowNameUnicode, 64);
    const auto resultBytesWrittenClass = WideCharToMultiByte(
                                                             CP_UTF8,
                                                             0,
                                                             windowNameUnicode,
                                                             nameLength,
                                                             windowNameASCII + startIndex2 + 1,
                                                             255,
                                                             nullptr,
                                                             nullptr);

    *(BYTE*)(windowNameASCII + startIndex2) = resultBytesWrittenClass;

    if (windowNameASCII[startIndex2] == 7 && strcmp(&windowNameASCII[1], "Notepad"))
    {
        notepadWindowFound = true;
    }

    // No idea yet...
    auto v56 = startIndex2 + *(BYTE*)(windowNameASCII + startIndex2) + 1;

    HANDLE  procHandle            = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, windowPid);
    wchar_t lpExeNameW[128]       = {0};
    DWORD   lpExeNameSize         = 128;
    bool    foundProcessImagePath = false;
    if (procHandle)
    {
        if (QueryFullProcessImageNameW(procHandle, 0, lpExeNameW, &lpExeNameSize))
        {
            lpExeNameSize = WideCharToMultiByte(
                                                CP_UTF8,
                                                0,
                                                lpExeNameW,
                                                lpExeNameSize,
                                                windowNameASCII + v56 + 1,
                                                255,
                                                nullptr,
                                                nullptr);

            if (lpExeNameSize)
            {
                foundProcessImagePath = true;
            }
        }

        CloseHandle(procHandle);
    }

    uint32_t blackListedWindowCount = 0;
    for (HWND m = GetWindow(hwndWindow, GW_CHILD); m; m = GetWindow(m, GW_HWNDNEXT))
    {
        char tmpWindowName[32] = {};
        if (!GetWindowTextA(m, tmpWindowName, 32))
            continue;


        if (!strcmp(tmpWindowName, "recoil")
            || !strcmp(tmpWindowName, "Recoil")
            || !strcmp(tmpWindowName, "No-Recoil")
            || !strcmp(tmpWindowName, "No-recoil")
            || !strcmp(tmpWindowName, "Triggerb")
            || !strcmp(tmpWindowName, "triggerb")
            || !strcmp(tmpWindowName, "RapidFir")
            || !strcmp(tmpWindowName, "Rapidfir")
            || !strcmp(tmpWindowName, "Rapid Fir")
            || !strcmp(tmpWindowName, "Rapid fir")
            || !strcmp(tmpWindowName, "Rapid fir")
            || !strcmp(tmpWindowName, "Chance (%")
            || !strcmp(tmpWindowName, "(%):")
            || !strcmp(tmpWindowName, "drakonia"))

        {
            blackListedWindowCount++;
        }
    }

    DWORD fileAttribute[10]{};
    GetFileAttributesExW(lpExeNameW, GetFileExInfoStandard, &fileAttribute);

    auto sendReport = [](uint8_t imagePathLength, DWORD imageSize, uint32_t styleFlags, uint32_t exStyleFlags,
                         RECT    rect,
                         int     reportSize)
    {
        Report report     = {};
        report.Unknown    = 0;
        report.ReportType = ReportType::DetectedAbnormalWindow_Handle_File;

        struct
        {
            BYTE     ReportType           = 0x3C;
            BYTE     Unknown              = 0;
            uint8_t  ImagePathLength      = 0;
            DWORD    ImageSize            = 0;
            CHAR     lpExeName[128]       = {};
            char     WindowName[64]      = {};
            char     WindowClassName[64] = {};
            uint32_t styleFlags           = 0;
            uint32_t exStyleFlags         = 0;
            RECT     windowRect           = {};
        } reportInfo;

        reportInfo.ImagePathLength = imagePathLength;
        reportInfo.ImageSize       = imageSize;
        reportInfo.styleFlags      = styleFlags;
        reportInfo.exStyleFlags    = exStyleFlags;
        reportInfo.windowRect      = rect;
        // etc


        BattleyeReport(&report, reportSize, 0);
        
    };


    if (blackListedWindowCount)
    {
         sendReport(static_cast<uint8_t>(lpExeNameSize), fileAttribute[8], styleFlags, exStyleFlags, windowRect,
                   reportSize);
        return;
    }


    if (parentWindow && exStyleFlags & WS_EX_LAYERED || styleFlags & WS_VISIBLE)
    {
        sendReport(0, 0, styleFlags, exStyleFlags, windowRect, reportSize);
        return;
    }

    // Check if the window is layered and topmost
    if (exStyleFlags & WS_EX_LAYERED && exStyleFlags & WS_EX_TOPMOST)
    {
         sendReport(static_cast<uint8_t>(lpExeNameSize), fileAttribute[8], styleFlags, exStyleFlags, windowRect,
                   reportSize);
        return;
    }

    if ((exStyleFlags | styleFlags) == 0x14CF0100)
    {
         sendReport(static_cast<uint8_t>(lpExeNameSize), fileAttribute[8], styleFlags, exStyleFlags, windowRect,
                   reportSize);
        return;
    }

    int combinationWindowFlags = exStyleFlags & styleFlags;

    constexpr uint32_t blacklistedFlagsCombination[] = {
        0x34CF0100, 0x14EF0310, 0x34EF0310, 0x14EF0110, 0x34EF0110, 0x17090020, 0x17090000, 0x16090020,
        0x94080020, 0x94080080, 0x9C080080, 0x160A0080, 0x16CA0008, 0xD60A0080,
        0xD6080101, 0x160D0020, 0x940800A0, 0x16CF0101, 0x36CF0101, 0x160D0000, 0x94080000, 0x16C20100,
        0x16C80100, 0x16080080, 0x160C0000, 0x1E0900A0, 0x9C880020, 0x9C0800A0, 0x9C080024, 0x9C080020, 0x150908A0,
        0x16020008, 0x9C080000, 0xD40800A0, 0x94000010, 0xB4000010, 0x94880020, 0x1E0D0028, 0x140800A0, 0x14080020,
        0x14080080, 0x9C880220, 0x960B00A0, 0x140908A0, 0x160A0000, 0x960814B0, 0x9D080000, 0x16CA0108, 0x36CA0108,
        0x160800A0, 0x9C1F0137, 0x160A0020, 0x9C1F01B7, 0x94080220, 0x960A00A0, 0x9CA80020, 0x960A0080, 0x9C0900A0,
        0x96080020, 0x960800A0, 0x9C1800A0, 0x9C4800A0, 0xD6080020, 0x9E1800A0, 0x1C0800A0, 0x94880000, 0x9D080020,
        0xDC0A0020, 0x1C0900A0, 0x961900A0, 0x964B00A0, 0x9E1840A0, 0x1C480020, 0x9E0C00A0, 0x16CE0101, 0x36CE0101,
        0x960904A0, 0x14EC0110, 0x9C0C00A0, 0x948802A0, 0x9C080220, 0x9C0A6060, 0x14CF0108, 0x34CF0108, 0x15080020,
        0x14CA0101, 0x34CA0101, 0x16020000, 0x94000088, 0x96000000, 0x94030400, 0x96030400, 0x9C09004C, 0x94CD01CD

    };

    // Check if the combination of flags is blacklisted
    if (std::ranges::find(blacklistedFlagsCombination,
                          combinationWindowFlags) !=
        std::end(blacklistedFlagsCombination))
    {
        sendReport(static_cast<uint8_t>(lpExeNameSize), fileAttribute[8], styleFlags, exStyleFlags, windowRect,
                   reportSize);
        return;
    }

    // Check if the window is layered and the window name is "MainWind" or the window is iconic
    if ((combinationWindowFlags == 0x16CF0100 || combinationWindowFlags == 0x36CF0100) && (
            !strcmp(&windowName[2], "MainWind") || (exStyleFlags & WS_EX_LAYERED) != 0))
    {
        sendReport(static_cast<uint8_t>(lpExeNameSize), fileAttribute[8], styleFlags, exStyleFlags, windowRect,
                   reportSize);
        return;
    }

    if (combinationWindowFlags == 0x17CF0100 && !strlen(windowName)
        || (combinationWindowFlags & 0xFFFFF) == 0xBA7A0
        || (combinationWindowFlags & 0xFFFFF) == 0x80323
        || (combinationWindowFlags & 0xFFFFF) == 0x90A25
        || (combinationWindowFlags & 0xFFFFF) == 0x90A65
        || (combinationWindowFlags & 0xFFFFF) == 0xE0181
        || (combinationWindowFlags & 0xFFFFF) == 0xE0080
        || exStyleFlags == 0x5800A0
        || exStyleFlags == 0xC00A0
        || (exStyleFlags & 0x80024) == 0x80024
        || (combinationWindowFlags & 0x9C090020) == 0x9C090020
        || (combinationWindowFlags & 0xD00800A0) == 0xD00800A0
        || combinationWindowFlags == 0x94000000 && !strlen(windowName)
        || (exStyleFlags & 0x80000) != 0
        && (strcmp(windowNameASCII + v56 + 1, "IME") == 0
            || strcmp(windowNameASCII + v56 + 1, "MSCT") == 0
            || strcmp(windowNameASCII + 2, "BattlEye") == 0
            || strcmp(windowNameASCII + v56 + 1, "WorkrW") == 0 && (combinationWindowFlags & 0xF) != 0
        )
    )
    {
        sendReport(static_cast<uint8_t>(lpExeNameSize), fileAttribute[8], styleFlags, exStyleFlags, windowRect,
                   reportSize);
    }
}

5. Failed to enumerated windows

The enumerated window employs a counter that increments with each execution. Any attempt to circumvent the execution would result in a zero counter value, triggering a DetectedFailToEnumerateWindow report.

  Report report     = {};
  report.Unknown    = 0;
  report.ReportType = ReportType::DetectedAbnormalWindow_Handle_File;
  
  BattleyeReport(&report, reportSize, 0);

6. Integrity checks

The integrity first checks for simple hooks on GetWindowLongA by examining its prologue for immediate moves or early returns. Failing that, it recursively resolves jump CALL and follows JMP instructions across GetTopWindow, GetWindow, and GetWindowLongA. If targetFunctionAddr is valid at the end, then its reported to the server with DetectedAbnormalWindow_Handle_File.

void PerformIntegrityChecks()
{
    // Get addresses of relevant functions from user32.dll
    uintptr_t getWindowLongAAddr = (uintptr_t)GetProcAddress(GetModuleHandleA("user32.dll"), "GetWindowLongA");
    uintptr_t getWindowAddr      = (uintptr_t)GetProcAddress(GetModuleHandleA("user32.dll"), "GetWindow");
    uintptr_t getTopWindowAddr   = (uintptr_t)GetProcAddress(GetModuleHandleA("user32.dll"), "GetTopWindow");

    uintptr_t targetFunctionAddr = NULL;

    // Checks GetWindowLongA 
    if (*(uint8_t*)getWindowLongAAddr == 0xB8 ||
        *(uint16_t*)getWindowLongAAddr == 0xb848 ||
        *(uint8_t*)getWindowLongAAddr == 0xC3) // RET
    {
        targetFunctionAddr = getWindowLongAAddr;
    }
    else
    {
        // If GetWindowLongA seems unmodified, check other functions
        uintptr_t currentFunctionAddr = NULL;

        for (int functionIndex = 0; functionIndex < 3; ++functionIndex)
        {
            // Select function to check based on the loop index
            if (functionIndex == 0)
                currentFunctionAddr = getTopWindowAddr;
            else if (functionIndex == 1)
                currentFunctionAddr = getWindowAddr;
            else
                currentFunctionAddr = getWindowLongAAddr;

            // Follow jump instructions to find the actual function address
            for (uintptr_t instructionPtr = currentFunctionAddr; ; targetFunctionAddr = instructionPtr)
            {
                // Follow JMP (0xE9) or CALL (0xE8) instructions
                while (*(uint8_t*)instructionPtr == 0xE9 || *(uint8_t*)instructionPtr == 0xE8)
                {
                    instructionPtr += *(int32_t*)(instructionPtr + 1) + 5;
                    targetFunctionAddr = instructionPtr;
                }

                // Check for JMP [RIP+disp32] instruction (0x25FF)
                if (*(uint16_t*)instructionPtr != 0x25FF)
                    break;

                // Follow the jump
                instructionPtr = *(uintptr_t*)(instructionPtr + *(int32_t*)(instructionPtr + 2) + 6);
            }
        }
    }

    // if targetFunctionAddris valid, then it found a hook and its reported to the server.
}

Conclusion

This analysis of window detection mechanisms is just the first part of a broader investigation into its anti-cheat system. While we’ve uncovered sophisticated techniques for identifying suspicious windows, BattlEye’s protection extends far beyond this. Future posts will delve into other aspects of its strategy, providing a more comprehensive understanding of its approach to maintaining game integrity. So stay tuned for future posts.

References

https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-gettopwindow
https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindow
https://learn.microsoft.com/pt-br/windows/win32/api/winuser/nf-winuser-getwindowlonga

20 de June de 2024 0 comment
1 TwitterEmail
Blog

The Finals – Defeating Theia Anti-Tamper

by reversingthread.info 10 de January de 2024

The Finals

The Finals is a multiplayer first-person shooter game developed by Embark Studios. They were able to attract a lot of users from the beta to the launch, and it’s currently one of the most played games on steam.

They were successful in attracting a large number of users from the beta phase to the official launch, and currently, they are one of the most popular games being played on Steam.

Theia

Theia, created by ZeroItLab, is a tool designed to prevent tampering, debugging, and obfuscation in various games such as The Finals, EA FC 24, and the discontinued The Cycle Frontier game. It fills a void left by the acquisition of Byfron Company when Roblox purchased them. Theia offers similar features to Byfron, including anti-debugging, anti-analysis, obfuscation of game pages, and hardware ID tracking. In this post, our main focus will be on decrypting all the game pages.

Defeating their Encryption

Initially, it should be noted that the process at hand is highly time consuming. The individuals responsible for this task have effectively concealed, encrypted, and implemented measures to obstruct analysis of their system. In order to thoroughly examine it, we must overcome certain protective measures, granting us greater freedom to conduct our analysis.

1. Get the game running without EAC

To achieve greater freedom, we should eliminate EAC (Easy Anti Cheat), thereby allowing us to solely focus on handling user-mode protections and giving us control over the Kernel.

However, the developers of Theia are well aware of this and have not made it easy for us to prevent their Anti-Cheat from loading. They persistently communicate with the driver to confirm its loading status. If it is not loaded, they simply prevent the process from running. So, what we can do about it? Well, we create a fake EAC driver and reply to their requests as they want.

Initially, we need to intercept DeviceIoControl to monitor the exchanged data during communication with the EAC driver. Once we have gathered this information, we can construct our own EAC driver and respond accordingly.

The following is a compilation of IOCTL requests made to the EAC Driver. However, the specific names of these IOCTLs are currently unknown.

0x226003 - We return the following bytes [0x19, 0x04, 0x00, 0x00]

0x226013 - If that is the first request we return [0x04, 0x00, 0x00, 0x00] else we return [01 00 00 00]

0x22e017 - We can just return STATUS_SUCCESS

Knowing those IOCTL, our task becomes crafting the fake EAC driver. We should ensure that the driver employs the same device, event names, and effectively handles the IOCTL requests. Then compile and load the driver.

#define IOCTL_UNKNOWN_BASE                                              FILE_DEVICE_UNKNOWN
#define IOCTL_EAC_DEBUG_ECHO                                            CTL_CODE(IOCTL_UNKNOWN_BASE, 0x0800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_EAC_UNK0                                                    0x226003
#define IOCTL_EAC_UNK1                                                    0x226013
#define IOCTL_EAC_UNK2                                                    0x22e017

PDRIVER_OBJECT g_pDriverObject = nullptr;
PDEVICE_OBJECT g_pDeviceObject = nullptr;

WCHAR g_szDeviceName[260];
WCHAR g_szDeviceLnkName[260];

BOOLEAN    g_IsFirstCall = TRUE;
KSPIN_LOCK g_SpinLock;

HANDLE eventHandle1, eventHandle2, eventHandle3;

PVOID  sectionBaseAddress = NULL;
HANDLE sectionHandle = NULL;

NTSTATUS CreateNamedEvent(PCWSTR eventName, PHANDLE pEventHandle)
{
    UNICODE_STRING    uniName;
    OBJECT_ATTRIBUTES objAttr;
    NTSTATUS          status;

    RtlInitUnicodeString(&uniName, eventName);
    InitializeObjectAttributes(&objAttr, &uniName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    status = ZwCreateEvent(pEventHandle, EVENT_ALL_ACCESS, &objAttr, SynchronizationEvent, FALSE);

    return status;
}

NTSTATUS CreateFakeEACSection(PHANDLE sectionHandlePtr, PLARGE_INTEGER maximumSize)
{
    UNICODE_STRING    sectionName;
    OBJECT_ATTRIBUTES objAttr;
    NTSTATUS          status;

    RtlInitUnicodeString(&sectionName, L"\\BaseNamedObjects\\EasyAntiCheat_EOSBin");
    InitializeObjectAttributes(&objAttr, &sectionName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);

    maximumSize->QuadPart = 4096; // Set the size of the section (e.g., 4 KB)

    status = ZwCreateSection(sectionHandlePtr, SECTION_ALL_ACCESS, &objAttr, maximumSize, PAGE_READWRITE, SEC_COMMIT,
        NULL);
    if (!NT_SUCCESS(status))
    {
        return status;
    }
    return status;
}


VOID DriverUnload(PDRIVER_OBJECT pDriverObj)
{
    NTSTATUS status = STATUS_SUCCESS;

    //close events
    if (eventHandle1 != NULL)
    {
        ZwClose(eventHandle1);
    }

    if (eventHandle2 != NULL)
    {
        ZwClose(eventHandle2);
    }

    if (eventHandle3 != NULL)
    {
        ZwClose(eventHandle3);
    }

    //remove section
    if (sectionBaseAddress != NULL)
        ZwUnmapViewOfSection(NtCurrentProcess(), sectionBaseAddress);
    if (sectionHandle != NULL)
        ZwClose(sectionHandle);


    //Delete Sym link n Device
    UNICODE_STRING uncLinkName;
    RtlInitUnicodeString(&uncLinkName, g_szDeviceLnkName);

    status = IoDeleteSymbolicLink(&uncLinkName);
    if (!NT_SUCCESS(status))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "[EasyAntiCheat_EOS] DriverUnload::Unable to delete SymbolicLink NTSTATUS -> % u \n", status);
    }

    IoDeleteDevice(pDriverObj->DeviceObject);
    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS]Driver Unload\n");
}

NTSTATUS DispatchUnused(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    UNREFERENCED_PARAMETER(pDevObj);
    UNREFERENCED_PARAMETER(pIrp);


    NTSTATUS NtStatus = STATUS_SUCCESS;
    return NtStatus;
}

NTSTATUS DispatchCreate(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    UNREFERENCED_PARAMETER(pDevObj);

    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS]Dispatch Create\n");

    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

NTSTATUS DispatchClose(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    UNREFERENCED_PARAMETER(pDevObj);

    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS]Dispatch Close\n");

    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

NTSTATUS DispatchIoctl(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    //UNREFERENCED_PARAMETER(pDevObj);

    NTSTATUS           status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrpStack;
    ULONG              uIoControlCode;
    PVOID              pIoBuffer;
    ULONG              uInSize;
    ULONG              uOutSize;

    pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
    uIoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
    uInSize = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutSize = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;

    switch (uIoControlCode)
    {
    case IOCTL_EAC_DEBUG_ECHO:
    {
        __try
        {
            DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS]Hello from Ring0!\n");
            status = STATUS_SUCCESS;
        }
        __except (EXCEPTION_EXECUTE_HANDLER)
        {
            status = STATUS_UNSUCCESSFUL;
        }
        break;
    }
    case IOCTL_EAC_UNK0: //0x226003
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "[EasyAntiCheat_EOS] TRIGGERED CHECK 0x226003 (InputBufferLength: %lu) (OutputBufferLength: %lu)\n",
            uInSize, uOutSize);

        unsigned char DummyReturnData[4] = { 0x19, 0x04, 0x00, 0x00 };

        PVOID pBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;

        // Ensure pBuffer is valid and large enough
        if (pBuffer != nullptr && uOutSize >= sizeof(DummyReturnData))
        {
            RtlCopyMemory(pBuffer, &DummyReturnData[0], sizeof(DummyReturnData));
            uOutSize = sizeof(DummyReturnData);
            status = STATUS_SUCCESS;
            DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS] RETURNING [19 04 00 00] \n");

            KIRQL oldIrql;
            KeAcquireSpinLock(&g_SpinLock, &oldIrql);

            g_IsFirstCall = FALSE; // Toggle

            KeReleaseSpinLock(&g_SpinLock, oldIrql);
        }
        else
        {
            uOutSize = 0;
            status = STATUS_BUFFER_TOO_SMALL;
        }


        break;
    }
    case IOCTL_EAC_UNK1: //0x226013
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS] TRIGGERED CHECK 0x226013 \n");

        unsigned char DummyReturnDataA[4] = {
            0x04, 0x00, 0x00, 0x00
        };

        unsigned char DummyReturnDataB[4] = {
            0x01, 0x00, 0x00, 0x00
        };


        PVOID pBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;

        // Ensure pBuffer is valid and large enough
        if (pBuffer != nullptr && uOutSize >= sizeof(DummyReturnDataA))
        {
            KIRQL oldIrql;
            KeAcquireSpinLock(&g_SpinLock, &oldIrql);

            // Check the flag and toggle it
            if (g_IsFirstCall)
            {
                RtlCopyMemory(pBuffer, &DummyReturnDataA, sizeof(DummyReturnDataA));
                DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
                    "[EasyAntiCheat_EOS] RETURNING [04 00 00 00] FIRST CALL \n");
            }
            else
            {
                RtlCopyMemory(pBuffer, &DummyReturnDataB, sizeof(DummyReturnDataB));
                //g_IsFirstCall = TRUE; // Reset for the next call
                DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
                    "[EasyAntiCheat_EOS] RETURNING [01 00 00 00] SEC CALL \n");
            }

            KeReleaseSpinLock(&g_SpinLock, oldIrql);

            uOutSize = sizeof(DummyReturnDataA);
            status = STATUS_SUCCESS;
        }
        else
        {
            uOutSize = 0;
            status = STATUS_BUFFER_TOO_SMALL;
        }

        break;
    }
    case IOCTL_EAC_UNK2: //0x22e017
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS] TRIGGERED CHECK 0x22e017\n");
        status = STATUS_SUCCESS;
        break;
    }
    default:
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS] Triggered Unknown IOCTL: [%x] \n",
            uIoControlCode);
        break;
    }
    } //Switch End

    if (status == STATUS_SUCCESS)
        pIrp->IoStatus.Information = uOutSize;
    else
        pIrp->IoStatus.Information = 0;

    pIrp->IoStatus.Status = status;

    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return status;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING RegistryPath)
{
    //UNREFERENCED_PARAMETER(pRegistryString);

    NTSTATUS       status = STATUS_SUCCESS;
    UNICODE_STRING uncDeviceName;
    UNICODE_STRING uncLinkName;

    //set global var
    g_pDriverObject = pDriverObj;


    //Check Unicode String
    status = RtlUnicodeStringValidate(RegistryPath);
    if (!NT_SUCCESS(status))
    {
        return STATUS_INVALID_PARAMETER;
    }



    //setup the MajorFunctions to nothing first
    for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
        pDriverObj->MajorFunction[i] = DispatchUnused;

    pDriverObj->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
    pDriverObj->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;
    pDriverObj->DriverUnload = DriverUnload;


    // extract the driver name from the registry path
    LPCWSTR szDriverName = wcsrchr(RegistryPath->Buffer, L'\\');
    if (szDriverName == nullptr)
        return STATUS_INVALID_PARAMETER;
    szDriverName++;


    szDriverName = L"EasyAntiCheat_EOS";

    //Make Device Name : \\Device\\BEDaisy
    status = RtlStringCchPrintfW(g_szDeviceName, ARRAYSIZE(g_szDeviceName), L"\\Device\\%ws", szDriverName);
    if (!NT_SUCCESS(status))
        return STATUS_INVALID_PARAMETER;

    //convert the device name to a unicode string struct
    RtlInitUnicodeString(&uncDeviceName, g_szDeviceName);


    //Make Symbolic Link : \\DosDevices\\Global\\BEMapr  \\DosDevices\\BEMapr
    if (IoIsWdmVersionAvailable(1, 0x10))
        status = RtlStringCchPrintfW(g_szDeviceLnkName, ARRAYSIZE(g_szDeviceLnkName), L"\\DosDevices\\Global\\%ws",
            szDriverName);
    else
        status = RtlStringCchPrintfW(g_szDeviceLnkName, ARRAYSIZE(g_szDeviceLnkName), L"\\DosDevices\\%ws",
            szDriverName);

    if (!NT_SUCCESS(status))
        return STATUS_INVALID_PARAMETER;


    // convert the symlink to a unicode string struct
    RtlInitUnicodeString(&uncLinkName, g_szDeviceLnkName);


    // create device
    status = IoCreateDevice(pDriverObj, 0, &uncDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE,
        &g_pDeviceObject);
    if (!NT_SUCCESS(status))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS] IoCreateDevice Failed %u \n", status);
        return status;
    }

    //setup flags for buffered io
    g_pDeviceObject->Flags |= DO_BUFFERED_IO;

    // create link
    status = IoCreateSymbolicLink(&uncLinkName, &uncDeviceName);
    if (!NT_SUCCESS(status))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS] IoCreateSymbolicLink Failed %u \n",
            status);
        IoDeleteDevice(g_pDeviceObject);
        return status;
    }

    //init spinlock
    KeInitializeSpinLock(&g_SpinLock);

    //make events
    status = CreateNamedEvent(L"\\BaseNamedObjects\\EasyAntiCheat_EOSEventDriver", &eventHandle1);
    if (!NT_SUCCESS(status))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "[EasyAntiCheat_EOS] CreateNamedEvent Failed %u for [EasyAntiCheat_EOSEventDriver] \n", status);
        eventHandle1 = NULL;
    }

    status = CreateNamedEvent(L"\\BaseNamedObjects\\EasyAntiCheat_EOSEventGame", &eventHandle2);
    if (!NT_SUCCESS(status))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "[EasyAntiCheat_EOS] CreateNamedEvent Failed %u for [EasyAntiCheat_EOSEventGame] \n", status);
        eventHandle2 = NULL;
    }

    status = CreateNamedEvent(L"\\BaseNamedObjects\\EasyAntiCheat_EOSEventModule", &eventHandle3);
    if (!NT_SUCCESS(status))
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
            "[EasyAntiCheat_EOS] CreateNamedEvent Failed %u for [EasyAntiCheat_EOSEventModule] \n", status);
        eventHandle3 = NULL;
    }

    //Create section
    LARGE_INTEGER maximumSize;
    NTSTATUS      sectionCreateStatus = CreateFakeEACSection(&sectionHandle, &maximumSize);
    if (NT_SUCCESS(sectionCreateStatus))
    {
        sectionBaseAddress = NULL;
        SIZE_T viewSize = 0;
        status = ZwMapViewOfSection(sectionHandle, NtCurrentProcess(), &sectionBaseAddress, 0, 0, NULL, &viewSize,
            ViewShare, 0, PAGE_READWRITE);
        if (!NT_SUCCESS(status))
        {
            ZwClose(sectionHandle);
            DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
                "[EasyAntiCheat_EOS]CreateFakeEACSection ZwMapViewOfSection Failed: %u \n", status);
        }
        else
        {
            RtlZeroMemory(sectionBaseAddress, maximumSize.QuadPart);
            DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
                "[EasyAntiCheat_EOS] CreateFakeEACSection [EasyAntiCheat_EOSBin] \n");
        }
    }
    else
    {
        DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS]CreateFakeEACSection Failed: %u \n",
            sectionCreateStatus);
    }


    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS]Driver Loaded \n");
    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[EasyAntiCheat_EOS]Name: %wZ LinkName : %wZ \n", uncDeviceName,
        uncLinkName);

    return STATUS_SUCCESS;
}
Expand

2. Creating a custom Launcher

To directly launch the game without going through the EAC startup, we develop a custom launcher to replace the existing one in the game folder. Another option is to hook Steam when it initiates the process, but we are going with replacing the launcher for this.

Here’s the code for our launcher:

void LauncherTest(int argc, char** argv)
{
    STARTUPINFOA processStartupInfo{0};
    processStartupInfo.cb = sizeof(processStartupInfo); // setup size of strcture in bytes
    PROCESS_INFORMATION processInfo{nullptr};


    // Main .exe (Like Discovery.exe, FC24.exe, or any other)
    auto commandLineToExecute = FormarString(xorstr_(R"("C:\Program Files (x86)\Steam\steamapps\common\The Finals\Discovery\Binaries\Win64\Discovery.exe")"), ExePath().c_str());


    // Load the arguments passed by the game itself. We can skip any if we want
    for (int i = 1; i < argc; i++)
    {
        // Add the argument to the command line
        commandLineToExecute = commandLineToExecute.append(FormarString(" %s", argv[i]));
    }

    // Append Extra commands
    commandLineToExecute.append(xorstr_(R"( Discovery)"));

    auto result = CreateProcessA(nullptr, (LPSTR)commandLineToExecute.c_str(), nullptr, nullptr, false, 0,
                                               nullptr,
                                               nullptr,
                                               &processStartupInfo, &processInfo);

   auto  getlasterror = GetLastError();
    std::cout << "error: " << getlasterror << std::endl;
    std::cout << "CreateProcessA: " << result << std::endl;

    if (getlasterror)
    {
        CloseHandle(processInfo.hProcess);
        CloseHandle(processInfo.hThread);
    }
    
    Sleep(5000);
}


int main(int argc, char** argv)
{
    LauncherTest(argc, argv);
    return 0;
}

Once compiled, rename it to Discovery.exe and replace the one at: C:\Program Files (x86)\Steam\steamapps\common\The Finals

3. EOS SDK

The EOS SDK, provided by Epic Games, is a comprehensive set of services and tools for streamlined online game development. It offers various functionalities such as cross-platform play, matchmaking, anti-cheat measures, lobbies, achievements, leaderboards, and more.

To be able to get the game running, we need to address EOS as well. Since we are using our fake EAC driver, we have two options: either simulate the EOS functions by loading a proxy DLL and handling the requests, or completely remove it.

Surprisingly, we can simply delete or rename the EOSSDK-Win64-Shipping.dll file located at C:\Program Files (x86)\Steam\steamapps\common\The Finals\Engine\Binaries\Win64, and the game will still launch successfully.

After that we have the game running.

4. Understading the page obsfucation

The Discovery.exe game is made up of three allocations. The first allocation has the encrypted pages marked as PAGE_NOACCESS, the second allocation has the pages marked as RW and the encrypted pages contain the byte 0xCC, and the third and last page contain the encrypted bytes.


How the decryption process works?

When the game attempts to access an encrypted page, it causes an exception known as EXCEPTION_ACCESS_VIOLATION. This exception is then caught by a handler, where some checks are performed. If everything is deemed fine, the page is decrypted and marked as RX, then the execution resumes. It is important to note that the exception is not triggered by attempting to READ/WRITE the page, either from the game module or their module runtime.dll; rather, the page must be executed to initiate the decryption process.

After a while the pages are encrypted back and marked as PAGE_NOACCESS again.

5. Decrypting it

There are several methods available for decrypting a page. One approach involves executing the page, which triggers the decryption routine. As a result, the page will contain the decrypted code and be marked as RX. Achieving this is relatively straightforward. We can either create an assembly shellcode that jumps or calls a specific function in the encrypted page, or alternatively, we can create a thread and pass the encrypted page address as the routine.

Keep in mind that if we provide an encrypted page address to the CreateThread function and it encounters a faulty instruction, the entire program will crash. However, there is a simple solution available. We can register our own exception handler, catch the error, and exit the thread without any concern. The same applies when creating a shellcode.

Decrypting a page.

Decrypting using their decryption routine

To locate their decryption routine, we can employ a VEH handler to capture the EXCEPTION_ACCESS_VIOLATION exception. Within our handler, we can instruct it to return the value EXCEPTION_CONTINUE_SEARCH, allowing the search for the decryption routine to continue. In our Debugger, we set a breakpoint in our handler and create a thread that calls the encrypted page. Once our exception captures it, we have the option to single step or generate a trace.

But what else we could use to find their decryption routine?

Well, thats pretty easy, remember the three allocations of the game memory I mentioned earlier? Well, they aren’t just copies without a purpose. Each allocation serves a specific function.

Let’s assign names to the allocations as per their utility:

  • Allocation 1: It’s where the pages are marked as PAGE_NOACCESS.
  • Allocation 2: It’s a RW allocation, the encrypted pages has 0xCC instead the original bytes.
  • Allocation 3: It’s the allocation which has the encrypted code.

Allocation One
Allocation Two and Three

Once the game executes an encrypted page, the decryption routine is run. The decrypted code is then copied to Allocation 2. Following this, the page permission at Allocation 1 is set to RWX and then the decrypted code from Allocation 2 is copied to Allocation 1. Finally, Allocation 1 is set to RX. That sparked me a idea: I cannot set a breakpoint in a page with PAGE_NOACCESS, but I can certainly set a breakpoint in a RW page, which in our case is Allocation 2.

Thus, I placed a breakpoint on an Allocation 2 page to catch accesses. After that, I created a thread that will attempt execution inside an encrypted page and voila: the decrypted code was going to be written to the page, then I checked where the write was coming from. It looks like it’s the code responsible for copying the decrypted code. After some analysis I figured out that this function is actually the page decryption routine.

Here is the pseudo code of the Decrypt function

// Functin offset: 0x694f80

unsigned __int64 __fastcall DECRYPT_PAGE(__int64 page_ptr_in_allocation_two, __int64 page_ptr_in_allocation_three, __int64 encryptionKeyData)
{
  unsigned __int64 v6; // r12
  __int64 current_ptr_in_page_in_allocation_two; // r13
  __int64 current_ptr_in_page_in_allocation_three; // rbp
  unsigned __int64 offset; // r15
  unsigned __int64 current_index_in_page; // rax
  bool is_not_end_of_page; // cf
  __int64 i; // rax
  __m128i v13[8]; // [rsp+20h] [rbp-88h] BYREF

  v6 = page_ptr_in_allocation_two - page_ptr_in_allocation_three;
  current_ptr_in_page_in_allocation_two = page_ptr_in_allocation_two + 1;
  current_ptr_in_page_in_allocation_three = page_ptr_in_allocation_three + 1;
  offset = 0;
  do
  {
    GetKeyDataForOffset((Key *)encryptionKeyData, offset, v13[0].m128i_i8, 0x40);
    if ( v6 > 0xF )
    {
      *(__m128i *)(page_ptr_in_allocation_two + offset) = _mm_sub_epi8(_mm_loadu_si128((const __m128i *)(page_ptr_in_allocation_three + offset)), v13[0]);
      *(__m128i *)(page_ptr_in_allocation_two + (offset | 0x10)) = _mm_sub_epi8(_mm_loadu_si128((const __m128i *)(page_ptr_in_allocation_three + (offset | 0x10))), v13[1]);
      *(__m128i *)(page_ptr_in_allocation_two + (offset | 0x20)) = _mm_sub_epi8(_mm_loadu_si128((const __m128i *)(page_ptr_in_allocation_three + (offset | 0x20))), v13[2]);
      *(__m128i *)(page_ptr_in_allocation_two + (offset | 0x30)) = _mm_sub_epi8(_mm_loadu_si128((const __m128i *)(page_ptr_in_allocation_three + (offset | 0x30))), v13[3]);
    }
    else
    {
      for ( i = 0; i != 64; i += 2 )
      {
        *(_BYTE *)(current_ptr_in_page_in_allocation_two + i - 1) = *(_BYTE *)(current_ptr_in_page_in_allocation_three + i - 1) - v13[0].m128i_i8[i];
        *(_BYTE *)(current_ptr_in_page_in_allocation_two + i) = *(_BYTE *)(current_ptr_in_page_in_allocation_three + i) - v13[0].m128i_i8[i + 1];
      }
    }

    current_index_in_page = offset + 0x40;
    current_ptr_in_page_in_allocation_two += 64;
    current_ptr_in_page_in_allocation_three += 64;
    is_not_end_of_page = offset < 4032;
    offset += 0x40;
  }
  while ( is_not_end_of_page );

  return current_index_in_page;
}

Once I figured out what the function was actually doing, I placed a breakpoint on it to determine its arguments. After running it for a few times, I was able to uncover them.

Arguments:

– First argument: Pointer to an encrypted page in Allocation 2
– Second argument: Pointer to a page in Allocation 3, which contains the encrypted code
– Third argument: Decryption data key

To determine the data key structure and its corresponding layout we repeated the previous process a few times. Eventually, we obtained the following struct.

struct Key
{
   m128     key1;
   m128     key2;
}

class decrypt_key_class
{
public:
    Key       general_one;                      //0x0000
    Key       general_two;                      //0x0020
    uint64_t  zero;                             //0x0040
    Key       unique_key;                       //0x0048
    int32_t  page_index;                        //0x0068
    char     pad_006C[28];                      //0x006C
    uint32_t not_int_a_bunch_of_flags_uint8_t;  //0x0088
    char     pad_008C[1764];                    //0x008C
    
    // The not_int_a_bunch_of_flags_uint8_t is not actually a int value, but since
    // it always the same value, i've just converted to int, they are actually bytes flag.
   
}; 

Explanation of the fields:

  • The keys “general_one” and “general_two” are identical, and are both static constants located in runtime.dll. These keys remain unchanged even after process restarts.
  • The “unique_key” refers to a key that is distinct for every page. These keys are unchanging and can be located in the runtime.dll. These keys remain unchanged even after process restarts.
  • “zero” is actually always zero.
  • “page_index” is actually the index of the page that we are decrypting it.
  • The variable “not_int_a_bunch_of_flags_uint8_t” is a collection of uint8_t flags whose purpose I didn’t reverse, considering they had the same value across the pages.
Key data structure in runtime.

Crafting the encrypted key data

To obtain the “general key”, we can hook the decryption routine and check the third argument, which is a pointer to the key structure, and then extract the key from it. Following this, we can search for those values in runtime.dll and make a signature for them.

The unique key can be found by using the same strategy. In this case we’re interested in current page_index and unique_key. Then we can search for the unique key in runtime.dll and use the page index to find where is the beginning of the encryption key list.

// This give us the page index, we can compare with the page_index in the decryption routine. You will see that we are correct.

current_page_index = ( ptr_page_encrypted - ptr_base ) / 0x1000;
decryption_key_size = 0x20;

// So if we can find the page_index, and we know the size of the encryption key, it then calculate where it starts.

// Step 
// 1. Find the current encryption key that we got from the hooked function in the runtime.dll
// 2. Since we also the index where we are, and we know the size of the encryption key. We do page_index * decryption_key_size.
// 3. We the result_value, we subtract the ptr found in runtime.dll - result_value and magic!
// 4. We are now in the begin of encryption key list.
Unique key list in IDA

Finally, we can use the same method again to obtain “not_int_a_bunch_of_flags_uint8_t“. This field receives the same value for all pages.

Crafting a encryption data key:

#define OFFSET_BEGIN_KEY_DECRYPT_LIST 0x41D2A0
#define FLAG_VALUE = 0x100024

    static decrypt_key_class* GetKeyData(uint32_t pageIndex)
    {
        auto result = new decrypt_key_class;
        RtlZeroMemory(result, sizeof decrypt_key_class);

        auto uniqueKey = (uintptr_t*)(runtime_imageBase + OFFSET_BEGIN_KEY_DECRYPT_LIST +   (pageIndex * 0x20));


        result->page_index = pageIndex;


        result->general_one.key1.m128_u64[0] = 0x98ADD1365BF4D30A;
        result->general_one.key1.m128_u64[1] = 0x6D22E7E35A9D06B5;
        result->general_one.key2.m128_u64[0] = 0x9EC9C0A169CEB5D3;
        result->general_one.key2.m128_u64[1] = 0x7DEC1707A2910127;
        
        result->general_two.key1.m128_u64[0] = 0x98ADD1365BF4D30A;
        result->general_two.key1.m128_u64[1] = 0x6D22E7E35A9D06B5;
        result->general_two.key2.m128_u64[0] = 0x9EC9C0A169CEB5D3;
        result->general_two.key2.m128_u64[1] = 0x7DEC1707A2910127;


        result->unique_key.key1.m128_u64[0] = uniqueKey[0];
        result->unique_key.key1.m128_u64[1] = uniqueKey[1];
        result->unique_key.key2.m128_u64[0] = uniqueKey[2];
        result->unique_key.key2.m128_u64[0] = uniqueKey[3];

        result->not_int_a_bunch_of_flags_uint8_t = FLAG_VALUE;


        return result;
    }

Calling the Decryption routine

With all the necessary components in place—the decryption routine, keys, and puzzle pieces—it is now time to execute the function.

using DecryptPage_t = uintptr_t(__fastcall*)(uintptr_t allocation_2, uintptr_t allocation_3, uintptr_t keyData);

uintptr_t DecryptPage(uint32_t pageIndex)
{
   
        if (pageIndex > 0)
            pageIndex--;


        static DecryptPage_t decryptPage = nullptr;
        if (!decryptPage)
        {
            // Set Decrypt routine address
            decryptPage = (DecryptPage_t)(runtime_imageBase + 0x694f80);
        }


        // Set the page we are looking to decrypt
        uintptr_t decryptAddrCC        = allocation_2 + pageIndex * 0x1000;
        uintptr_t decryptAddrEncrypted = allocation_3 + pageIndex * 0x1000;

        // Get the page Key where we want to look.
        auto decryptionKeyData = decrypt_key_class::GetKeyData(pageIndex + 1);
        
        
        auto result = decryptPage(decryptAddrCC, decryptAddrEncrypted,
                                                   (uintptr_t)decryptionKeyData);
        return result;

}

Wrapping all together and creating a dumper

void InitiateDumping()
{
    printf("======== [Initiated dumping] ======== \n");


    auto baseModuleAddr = base_address_allocation_2;

    // Get Image Size
    PIMAGE_NT_HEADERS pNtHeader        = RtlImageNtHeader((PVOID)baseModuleAddr);
    auto              imageSize        = pNtHeader->OptionalHeader.SizeOfImage;
    auto              moduleEndAddress = baseModuleAddr + imageSize;


    // Our current page dumping.
    uint32_t currentIndexPage = 1;

    // BlackListed Pages from Byfron
    constexpr uint32_t blacklistedPagesIndex[1000] = {};

    // Allocate Memory for saving the dump.
    auto bufferDataModule = (uintptr_t)VirtualAlloc(nullptr, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    RtlZeroMemory((void*)bufferDataModule, moduleEndAddress - baseModuleAddr);


    memcpy((void*)bufferDataModule, (void*)baseModuleAddr, 0x1000);

    uintptr_t decryptPage = 0;
    uintptr_t mybuffer    = 0;
    do
    {
        decryptPage = baseModuleAddr + currentIndexPage * 0x1000;
        mybuffer    = bufferDataModule + currentIndexPage * 0x1000;


        printf("[%d] Decrypting page: %p\n", currentIndexPage, decryptPage);

        // Are we going to skip the page?
        bool skipPage = false;

        // Loop all blacklisted page and check if matches the current one
        for (const unsigned int i : blacklistedPagesIndex)
        {
            if (i == currentIndexPage)
            {
                skipPage = true;
                break;
            }
        }

        // Check if we are going to skip the page.
        if (skipPage)
        {
            currentIndexPage++;                    // Increment
            memset((void*)mybuffer, 0x90, 0x1000); // Nope the page
            continue;
        }


        auto check = Misc::IsPageNoAcccess(base_address_allocation_1 + currentIndexPage * 0x1000);
        if (check)
        {
            auto result = DecryptPage(currentIndexPage);
        }

        memcpy((void*)mybuffer, (void*)decryptPage, 0x1000);


        currentIndexPage++;
    }
    while (baseModuleAddr + (currentIndexPage * 0x1000) < moduleEndAddress);

    Misc::SaveDllToDisk((unsigned char*)bufferDataModule, pNtHeader->OptionalHeader.SizeOfImage, "C:\\dumps\\Discovery_dumped.exe");

    printf("======== [Dumped] ======== \n");
}
Result after the decryption. All pages with original code.

Injecting a DLL

Theia places inline hooks in some modules to strengthen their protection mechanisms. Those include hooks inside ntdll, DirectX modules and user32 callbacks. Here’s a list of all functions that need to be restored so we can inject a DLL.

ntdll.dll hooks to be removed:

Conclusion

In conclusion, Theia presents a formidable challenge for achieving any desired purpose due to the numerous barriers it imposes. The presence of anti-debuggers, live communication with the kernel driver, hypervisor detection, stack obfuscation, analysis tool detection, and runtime.dll protection further complicate the task. As my experience goes, it is recommended to implement blacklisted pages that can only be decrypted using a special function or key. Additionally, the use of dynamic keys instead of static ones and preventing all pages from being unlocked simultaneously can enhance security. It is also crucial to regularly check if any hooks have been removed and take appropriate action by closing the game if any reversing tool is detected. These improvements may make the Theia more resilient and challenging to overcome.

I had a great fun and headache analyzing it, The journey itself was enjoyable, and I successfully achieved my goal.

Thanks

Cra0 for the help provided throughout my journey.

10 de January de 2024 0 comment
4 TwitterEmail

@2024 Reversing Thread - All Right Reserved.


Back To Top