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