Home Blog Battleye Analysis Part 1 – Window Detection

Battleye Analysis Part 1 – Window Detection

by reversingthread.info

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

You may also like

Leave a Comment