流行的反作弊BattlEye的逆向工程


BattlEye主要是德国第三方反作弊游戏,主要由32岁的Bastian Heiko Suter开发 。 它为游戏发行商提供(或试图向其提供)使用通用保护机制的易于使用的防作弊系统,以及针对特定游戏的作弊检测以优化安全性。 如产品网站上所述,它始终处于现代技术的最前沿,并使用创新的保护和检测方法; 显然,这是开发商的国籍QUALITY MADE IN GERMANYQUALITY MADE IN GERMANY 。 BattlEye由许多元素组成,这些元素可以一起在为使用产品付费的游戏中找到作弊者。 四个主要元素是:

  • 服务
    • 与BattlEye BEServer服务器通信的Windows系统服务,该服务器提供与BEDaisyBEClient的客户端-服务器通信。
  • 贝迪西
    • Windows内核驱动程序,记录预防性事件处理机制和微型过滤器,以防止作弊者非法修改游戏
  • Becient
    • 动态连接的Windows库,负责大多数检测向量,包括本文中介绍的那些向量。 初始化后,它会附加到游戏过程中。
  • 观察者
    • 专有的后端服务器,负责收集信息并针对作弊者采取特定措施。

Shellcode


最近,Internet上出现了BattlEye shell代码的转储,我们决定写一些有关BattlEye当前版本的确切信息。 我们已经六个月没有分析BattlEye了,所以我们上一次对shellcode的转储很可能已经过时了。 假设BattlEye仅完成了外壳代码并且没有删除先前的检测过程,仅从最后一次转储的内存中恢复了代码的各个部分。

怎么了


据称,BattlEye将shellcode从其服务器流式传输到名为BEService的Windows服务。 该服务与游戏内的BEClient模块进行通信。 数据交换通过\.namedpipeBattleye ,直到2018年未加密。 现在,所有传输的数据都使用具有非常小的密钥的xor加密器进行了加密,这使得执行众所周知的纯文本攻击变得异常容易。 当将外壳程序代码传输到客户端时,它会在所有已知模块之外定位并执行,这使确定变得容易。 要创建shellcode转储,您可以处理标准Windows API函数(例如CreateFile,ReadFile等),并转储所有已知模块之外的所有调用模块的相应存储区(请求返回地址的存储信息),或者定期扫描游戏的虚拟内存空间,以查找所有已知模块外部的可执行内存,并将其转储到磁盘。 同时,您需要跟踪哪些区域已经被转储,因此,您将不会得到很多相同的转储。

解说


为了美观起见, 强烈修改了本文中提供的伪代码的片段。 您将无法转储BattlEye外壳程序代码并立即识别这些部分。 shellcode不包含函数调用,并且本文中部署了许多算法。 但是实际上,这并不重要,因为当您阅读完有关这个可怕的上古的资料后,您将有机会解决它(

内存排序


反作弊中最常见的检测机制是内存枚举和内存扫描,以搜索已知的作弊图像。 它很容易实现,并且如过去所示,使用正确的方法,如果您没有忘记汇编程序的基础知识并将常用功能的序言列入黑名单,则非常有效。

Battleye会遍历游戏进程的整个地址空间(在此情况下为当前进程),并对页面性能进行各种检查,并在相应的内存空间之外查找shell代码。

这是在Battleye中实现的方式:

 // MEMORY ENUMERATION for (current_address = 0 // QUERY MEMORY_BASIC_INFORMATION NtQueryVirtualMemory(GetCurrentProcess(), current_address, 0, &memory_information, 0x30, &return_length) >= 0 current_address = memory_information.base_address + memory_information.region_size) { const auto outside_of_shellcode = memory_information.base_address > shellcode_entry || memory_information.base_address + memory_information.region_size <= shellcode_entry const auto executable_memory = memory_information.state == MEM_COMMIT && (memory_information.protect == PAGE_EXECUTE || memory_information.protect == PAGE_EXECUTE_READ || memory_information.protect == PAGE_EXECUTE_READWRITE const auto unknown_whitelist = memory_information.protect != PAGE_EXECUTE_READWRITE || memory_information.region_size != 100000000 if (!executable_memory || !outside_of_shellcode || !unknown_whitelist) continue // RUN CHECKS memory::anomaly_check(memory_information memory::pattern_check(current_address, memory_information memory::module_specific_check_microsoft(memory_information memory::guard_check(current_address, memory_information memory::module_specific_check_unknown(memory_information } 

记忆异常


BattlEye标记内存地址空间中的所有异常,主要是与加载的映像不对应的可执行模块的内存:

 void memory::anomaly_check(MEMORY_BASIC_INFORMATION memory_information) { // REPORT ANY EXECUTABLE PAGE OUTSIDE OF KNOWN MODULES if (memory_information.type == MEM_PRIVATE || memory_information.type == MEM_MAPPED) { if ((memory_information.base_address & 0xFF0000000000) != 0x7F0000000000 && // UPPER EQUALS 0x7F (memory_information.base_address & 0xFFF000000000) != 0x7F000000000 && // UPPER EQUALS 0x7F0 (memory_information.base_address & 0xFFFFF0000000) != 0x70000000 && // UPPER EQUALS 0x70000 memory_information.base_address != 0x3E0000)) { memory_report.unknown = 0 memory_report.report_id = 0x2F memory_report.base_address = memory_information.base_address memory_report.region_size = memory_information.region_size memory_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&memory_report, sizeof(memory_report), 0 } } } 

扫描图案


如上所述,BattlEye还扫描本地进程的内存以查找各种清晰定义的模式,如以下所示的实现所示。

读取此伪代码时,您可以猜测可以通过重写每个已加载模块的代码区域来规避这些检查 ,因为它们不会扫描搜索已知图像中的图案。 为了不进行完整性检查,您需要下载所有打包和列入白名单的区域,并重写标记为RWX的代码区域,因为如果不模拟打包程序,我们将无法执行完整性检查。 在当前版本的BattlEye shellcode中,这些内存模式是硬编码的:

 [05 18] ojectsPUBGChinese [05 17] BattleGroundsPrivate_CheatESP [05 17] [%.0fm] %s [05 3E] 0000Neck0000Chest0000000Mouse 10 [05 3F] PlayerESPColor [05 40] Aimbot: %d02D3E2041 [05 36] HackMachine [05 4A] VisualHacks.net [05 50] 3E232F653E31314E4E563D4276282A3A2E463F757523286752552E6F30584748 [05 4F] DLLInjection-master\x64\Release\ [05 52] NameESP [05 48] Skullhack [05 55] .rdata$zzzdbg [05 39] AimBot [05 39] EB4941803C123F755C623FEB388D41D0FBEC93C977583E930EB683E1DF [05 5F] 55E9 [05 5F] 57E9 [05 5F] 60E9 [05 68] D3D11Present initialised [05 6E] [ %.0fM ] [05 74] [hp:%d]%dm [05 36] 48836424380488D4C2458488B5424504C8BC848894C24304C8BC7488D4C2460 [05 36] 741FBA80000FF15607E0085C07510F2F1087801008B8788100EB [05 36] 40F2AA156F8D2894E9AB4489535D34F9CPOSITION0000COL [05 7A] FFE090 [05 79] %s00%d00POSITION0000COLOR0000000 [05 36] 8E85765DCDDA452E75BA12B4C7B94872116DB948A1DAA6B948A7676BB948902C [05 8A] n<assembly xmlsn='urn:schemas-mi 

这些存储模式还包含一个两字节的标头,即未知静态值05和唯一标识符。

我们不会看到的是BattlEye还动态地从BEServer流化模式并将其发送给BEClient ,但是我们将不在本文中讨论。

通过以下算法对它们进行迭代扫描:

 void memory::pattern_check(void* current_address, MEMORY_BASIC_INFORMATION memory_information) { const auto is_user32 = memory_information.allocation_base == GetModuleHandleA("user32.dll" // ONLY SCAN PRIVATE MEMORY AND USER32 CODE SECTION if (memory_information.type != MEM_PRIVATE && !is_user32) continue for (address = current_address address != memory_information.base_address + memory_information.region_size address += PAGE_SIZE) // PAGE_SIZE { // READ ENTIRE PAGE FROM LOCAL PROCESS INTO BUFFER if (NtReadVirtualMemory(GetCurrentProcess(), address, buffer, PAGE_SIZE, 0) < 0) continue for (pattern_index = 0 pattern_index < 0x1C/*PATTERN COUNT*/ ++pattern_index) { if (pattern[pattern_index].header == 0x57A && !is_user32) // ONLY DO FFE090 SEARCHES WHEN IN USER32 continue for (offset = 0 pattern[pattern_index].length + offset <= PAGE_SIZE ++offset) { const auto pattern_matches = memory::pattern_match(&address[offset], pattern[pattern_index // BASIC PATTERN MATCH if (pattern_matches) { // PATTERN FOUND IN MEMORY pattern_report.unknown = 0 pattern_report.report_id = 0x35 pattern_report.type = pattern[index].header pattern_report.data = &address[offset pattern_report.base_address = memory_information.base_address pattern_report.region_size = memory_information.region_size pattern_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&pattern_report, sizeof(pattern_report), 0 } } } } } 

验证特定模块(Microsoft)


模块检查报告游戏中加载的特定模块的存在:

 void memory::module_specific_check_microsoft(MEMORY_BASIC_INFORMATION memory_information) { auto executable = memory_information.protect == PAGE_EXECUTE || memory_information.protect == PAGE_EXECUTE_READ || memory_information.protect == PAGE_EXECUTE_READWRITE auto allocated = memory_information.state == MEM_COMMIT if (!allocated || !executable) continue auto mmres_handle = GetModuleHandleA("mmres.dll" auto mshtml_handle = GetModuleHandleA("mshtml.dll" if (mmres_handle && mmres_handle == memory_information.allocation_base) { battleye_module_anomaly_report module_anomaly_report module_anomaly_report.unknown = 0 module_anomaly_report.report_id = 0x5B module_anomaly_report.identifier = 0x3480 module_anomaly_report.region_size = memory_information.region_size battleye::report(&module_anomaly_report, sizeof(module_anomaly_report), 0 } else if (mshtml_handle && mshtml_handle == memory_information.allocation_base) { battleye_module_anomaly_report module_anomaly_report module_anomaly_report.unknown = 0 module_anomaly_report.report_id = 0x5B module_anomaly_report.identifier = 0xB480 module_anomaly_report.region_size = memory_information.region_size battleye::report(&module_anomaly_report, sizeof(module_anomaly_report), 0 } } 

检查特定模块(未知)


系统已添加了对特定模块的检查,这表明服务器已加载满足以下任何条件的模块:

 void memory::module_specific_check_unknown(MEMORY_BASIC_INFORMATION memory_information) { const auto dos_header = (DOS_HEADER*)module_handle const auto pe_header = (PE_HEADER*)(module_handle + dos_header->e_lfanew const auto is_image = memory_information.state == MEM_COMMIT && memory_information.type == MEM_IMAGE if (!is_image) return const auto is_base = memory_information.base_address == memory_information.allocation_base if (!is_base) return const auto match_1 = time_date_stamp == 0x5B12C900 && *(__int8*)(memory_information.base_address + 0x1000) == 0x00 && *(__int32*)(memory_information.base_address + 0x501000) != 0x353E900 const auto match_2 = time_date_stamp == 0x5A180C35 && *(__int8*)(memory_information.base_address + 0x1000) != 0x00 const auto match_2 = time_date_stamp == 0xFC9B9325 && *(__int8*)(memory_information.base_address + 0x6D3000) != 0x00 if (!match_1 && !match_2 && !match_3) return const auto buffer_offset = 0x00 // OFFSET DEPENDS ON WHICH MODULE MATCHES, RESPECTIVELY 0x501000, 0x1000 AND 0x6D3000 unknown_module_report.unknown1 = 0 unknown_module_report.report_id = 0x46 unknown_module_report.unknown2 = 1 unknown_module_report.data = *(__int128*)(memory_information.base_address + buffer_offset battleye::report(&unknown_module_report, sizeof(unknown_module_report), 0 } 

我们不知道哪个模块满足这些条件,但是我们怀疑这是尝试检测非常有限的一组特定作弊模块的尝试。

附录:@ how02通知我们, action_x64.dll模块的0x5B12C900并包含可写入的代码区域; 如前所述,这可用于利用。

记忆保护


BattlEye还实现了一个非常可疑的检测过程,在我们看来,该过程将查找具有PAGE_GUARD标志设置的内存,而无需实际检查是否已设置PAGE_GUARD标志:

 void memory::guard_check(void* current_address, MEMORY_BASIC_INFORMATION memory_information) { if (memory_information.protect != PAGE_NOACCESS) { auto bad_ptr = IsBadReadPtr(current_address, sizeof(temporary_buffer auto read = NtReadVirtualMemory( GetCurrentProcess(), current_address, temporary_buffer, sizeof(temporary_buffer), 0 if (read < 0 || bad_ptr) { auto query = NtQueryVirtualMemory( GetCurrentProcess(), current_address, 0, &new_memory_information, sizeof(new_memory_information), &return_length memory_guard_report.guard = query < 0 || new_memory_information.state != memory_information.state || new_memory_information.protect != memory_information.protect if (memory_guard_report.guard) { memory_guard_report.unknown = 0 memory_guard_report.report_id = 0x21 memory_guard_report.base_address = memory_information.base_address memory_guard_report.region_size = (int)memory_information.region_size memory_guard_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&memory_guard_report, sizeof(memory_guard_report), 0 } } } } 

窗口排序


外壳程序代码BattlEye遍历游戏中当前可见的每个窗口,绕过窗口的上下(按z值)。 此枚举排除了游戏内部的窗口GetWindowThreadProcessId ,这是通过调用GetWindowThreadProcessId来确定的。 因此,您可以将相应的函数绑定到窗口的假所有者,以便BattlEye不会检查window

 void window_handler::enumerate() { for (auto window_handle = GetTopWindow window_handle window_handle = GetWindow(window_handle, GW_HWNDNEXT), // GET WINDOW BELOW ++window_handler::windows_enumerated) // INCREMENT GLOBAL COUNT FOR LATER USAGE { auto window_process_pid = 0 GetWindowThreadProcessId(window_handle, &window_process_pid if (window_process_pid == GetCurrentProcessId()) continue // APPEND INFORMATION TO THE MISC. REPORT, THIS IS EXPLAINED LATER IN THE ARTICLE window_handler::handle_summary(window_handle constexpr auto max_character_count = 0x80 const auto length = GetWindowTextA(window_handle, window_title_report.window_title, max_character_count // DOES WINDOW TITLE MATCH ANY OF THE BLACKLISTED TITLES? if (!contains(window_title_report.window_title, "CheatAut") && !contains(window_title_report.window_title, "pubg_kh") && !contains(window_title_report.window_title, "conl -") && !contains(window_title_report.window_title, "PerfectA") && !contains(window_title_report.window_title, "AIMWA") && !contains(window_title_report.window_title, "PUBG AIM") && !contains(window_title_report.window_title, "HyperChe")) continue // REPORT WINDOW window_title_report.unknown_1 = 0 window_title_report.report_id = 0x33 battleye::report(&window_title_report, sizeof(window_title_report) + length, 0 } } 

搜索异常


如果检查的窗口少于两个,则将通知发送到服务器。 这样做可能是为了防止修补相应的功能,这些功能不允许BattlEye shell代码检查任何窗口:

 void window_handler::check_count() { if (window_handler::windows_enumerated > 1) return // WINDOW ENUMERATION FAILED, MOST LIKELY DUE TO HOOK window_anomaly_report.unknown_1 = 0 window_anomaly_report.report_id = 0x44 window_anomaly_report.enumerated_windows = windows_enumerated battleye::report(&window_anomaly_report, sizeof(window_anomaly_report), 0 } 

流程分类


通过调用CreateToolhelp32Snapshot迭代所有正在运行的进程,但不会处理任何错误 ,因此非常易于修补,并避免了以下检测步骤:

路径检查


如果映像至少在两个子目录中(从磁盘根目录开始计数),并且当对应映像的路径中至少包含以下几行时,系统将标记进程:

 Desktop Temp FileRec Documents Downloads Roaming tmp.ex notepad. ...\. cmd.ex 

如果可执行文件的路径与以下行之一相对应,则服务器会收到有关可执行文件路径的通知,以及有关父进程是否为以下文件之一的信息(包含发送给服务器的相应标志位):

 steam.exe [0x01] explorer.exe [0x02] lsass.exe [0x08] cmd.exe [0x10] 

如果客户端无法使用适当的QueryLimitedInformation权限打开描述符,则它将设置标志位0x04 ,如果OpenProcess调用失败时的错误原因不是ERROR_ACCESS_DENIED ,这将为我们提供相应标志值的最后一个枚举容器:

 enum BATTLEYE_PROCESS_FLAG { STEAM = 0x1, EXPLORER = 0x2, ERROR = 0x4, LSASS = 0x8, CMD = 0x10 } 

如果父进程是Steam,则立即为用户设置标志,并通过通知ID 0x40通知服务器

图片名称


如果该过程符合下面列出的许多标准中的任何一个,那么您将立即设置一个标志,并将其报告给服务器,通知ID为0x38

    "Loadlibr"    "Rng "    "A0E7FFFFFF81"    "RNG "    "90E54355"    "2.6.ex"    "TempFile.exe" 

Steam游戏叠加


BattlEye监视Steam游戏叠加过程,该过程负责大多数Steam用户已知的游戏内叠加。 Steam Games叠加层的完整主机名是gameoverlayui.exe ; 众所周知,它经常用于渲染漏洞,因为在游戏窗口中很容易被黑客入侵和执行非法渲染。 验证具有以下条件:

 file size != 0 && image name contains (case insensitive) gameoverlayu 

专门针对Steam游戏进行的其他检查几乎与针对游戏过程本身执行的过程相同,因此在伪代码中被省略了。

Steam覆盖内存扫描


会覆盖Steam游戏的叠加过程,以查看图案和异常情况。 我们无法深入研究兔子洞,也无法发现这些模式的用途,因为它们非常笼统并且很可能与作弊模块相关联。

 void gameoverlay::pattern_scan(MEMORY_BASIC_INFORMATION memory_information) { // PATTERNS: // Home // F1 // FFFF83C48C30000000000 // \.pipe%s // C760000C64730 // 60C01810033D2 // ... // PATTERN SCAN, ALMOST IDENTICAL CODE TO THE AFOREMENTIONED PATTERN SCANNING ROUTINE gameoverlay_memory_report.unknown_1 = 0 gameoverlay_memory_report.report_id = 0x35 gameoverlay_memory_report.identifier = 0x56C gameoverlay_memory_report.data = &buffer[offset gameoverlay_memory_report.base_address = memory_information.base_address gameoverlay_memory_report.region_size = (int)memory_information.region_size gameoverlay_memory_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&gameoverlay_memory_report, sizeof(gameoverlay_memory_report), 0 } 

扫描过程还会以下载的图像之外的可执行代码的形式查找任何异常,这表明破解者将代码注入了覆盖过程:

 void gameoverlay::memory_anomaly_scan(MEMORY_BASIC_INFORMATION memory_information) { // ... // ALMOST IDENTICAL ANOMALY SCAN COMPARED TO MEMORY ENUMERATION ROUTINE OF GAME PROCESS gameoverlay_report.unknown = 0 gameoverlay_report.report_id = 0x3B gameoverlay_report.base_address = memory_information.base_address gameoverlay_report.region_size = memory_information.region_size gameoverlay_report.memory_info = memory_information.type | memory_information.protect | memory_information.state battleye::report(&gameoverlay_report, sizeof(gameoverlay_report), 0 } 

Steam游戏覆盖保护


如果覆盖Steam游戏的进程受到Windows进程(例如Light(WinTcb))的某种保护,则服务器将收到有关它的通知。

 void gameoverlay::protection_check(HANDLE process_handle) { auto process_protection = 0 NtQueryInformationProcess( process_handle, ProcessProtectionInformation, &process_protection, sizeof(process_protection), nullptr if (process_protection == 0) // NO PROTECTION return gameoverlay_protected_report.unknown = 0 gameoverlay_protected_report.report_id = 0x35 gameoverlay_protected_report.identifier = 0x5B1 gameoverlay_protected_report.data = process_protection battleye::report(&gameoverlay_protected_report, sizeof(gameoverlay_protected_report), 0 } 

另外,如果相应的OpenProcess调用将ERROR_ACCESS_DENIED返回到叠加过程,则会发送有关ID为3B的用户的通知。

分类模块


还搜索Steam游戏覆盖过程模块,尤其gameoverlayui.dll vgui2_s.dllgameoverlayui.dll 。 从gameoverlayui.dll开始,对这些模块进行了几次检查。

如果满足此条件: [gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC ,则[gameoverlayui.dll+6C779] == 08BE55DC3CCCCB8????????C3CCCCCC扫描以字节为单位存储的地址处的vtable ???????? 。 如果这些vtable元素中的任何一个在源模块gameoverlayui.dll之外或指向int 3指令,则将向用户报告通知ID为3B的服务器。

 void gameoverlay::scan_vtable(HANDLE process_handle, char* buffer, MODULEENTRY32 module_entry) { char function_buffer[16 for (vtable_index = 0 vtable_index < 20 vtable_index += 4) { NtReadVirtualMemory( process_handle, *(int*)&buffer[vtable_index], &function_buffer, sizeof(function_buffer), 0 if (*(int*)&buffer[vtable_index] < module_entry.modBaseAddr || *(int*)&buffer[vtable_index] >= module_entry.modBaseAddr + module_entry.modBaseSize || function_buffer[0] == 0xCC ) // FUNCTION PADDING { gameoverlay_vtable_report.report_id = 0x3B gameoverlay_vtable_report.vtable_index = vtable_index gameoverlay_vtable_report.address = buffer[vtable_index battleye::report(&gameoverlay_vtable_report, sizeof(gameoverlay_vtable_report), 0 } } } 

还对vgui2_s.dll模块执行了特定的验证过程:

 void vgui::scan() { if (!equals(vgui_buffer, "6A08B31FF561C8BD??????????FF96????????8BD????????8B1FF90")) { auto could_read = NtReadVirtualMemory( process_handle, module_entry.modBaseAddr + 0x48338, vgui_buffer, 8, 0) >= 0 constexpr auto pattern_offset = 0x48378 // IF READ DID NOT FAIL AND PATTERN IS FOUND if (could_read && equals(vgui_buffer, "6A46A06A26A")) { vgui_report.unknown_1 = 0 vgui_report.report_id = 0x3B vgui_report.unknown_2 = 0 vgui_report.address = LODWORD(module_entry.modBaseAddr) + pattern_offset // READ TARGET BUFFER INTO REPORT NtReadVirtualMemory( process_handle, module_entry.modBaseAddr + pattern_offset, vgui_report.buffer, sizeof(vgui_report.buffer), 0 battleye::report(&vgui_report, sizeof(vgui_report), 0 } } else if ( // READ ADDRESS FROM CODE NtReadVirtualMemory(process_handle, *(int*)&vgui_buffer[9], vgui_buffer, 4, 0) >= 0 && // READ POINTER TO CLASS NtReadVirtualMemory(process_handle, *(int*)vgui_buffer, vgui_buffer, 4, 0) >= 0 && // READ POINTER TO VIRTUAL TABLE NtReadVirtualMemory(process_handle, *(int*)vgui_buffer, vgui_buffer, sizeof(vgui_buffer), 0) >= 0) { for (vtable_index = 0 vtable_index < 984 vtable_index += 4 ) // 984/4 VTABLE ENTRY COUNT { NtReadVirtualMemory(process_handle, *(int*)&vgui_buffer[vtable_index], &vtable_entry, sizeof(vtable_entry), 0 if (*(int*)&vgui_buffer[vtable_index] < module_entry.modBaseAddr || *(int*)&vgui_buffer[vtable_index] >= module_entry.modBaseAddr + module_entry.modBaseSize || vtable_entry == 0xCC ) { vgui_vtable_report.unknown = 0 vgui_vtable_report.report_id = 0x3B vgui_vtable_report.vtable_index = vtable_index vgui_vtable_report.address = *(int*)&vgui_buffer[vtable_index battleye::report(&vgui_vtable_report, sizeof(vgui_vtable_report), 0 } } } 

此过程检查offset的变化,offset 48378是代码区域的位置:

 push 04 push offset aCBuildslaveSte_4 ; "c:\buildslave\steam_rel_client_win32"... push offset aAssertionFaile_7 ; "Assertion Failed: IsValidIndex(elem)" 

然后,该过程将检查非常具体且看似无用的更改:

 push 04 push 00 push 02 push ?? 

我们找不到与上述两项检查中的第一项都不匹配的vgui2_s.dll副本,因此我们无法找到它检查的vtable。

蒸汽覆盖流


在叠加Steam游戏过程中的流也会移动:

 void gameoverlay::check_thread(THREADENTRY32 thread_entry) { const auto tread_handle = OpenThread(THREAD_SUSPEND_RESUME|THREAD_GET_CONTEXT, 0, thread_entry.th32ThreadID if (thread_handle) { suspend_count = ResumeThread(thread_handle if (suspend_count > 0) { SuspendThread(thread_handle gameoverlay_thread_report.unknown = 0 gameoverlay_thread_report.report_id = 0x3B gameoverlay_thread_report.suspend_count = suspend_count battleye::report(&gameoverlay_thread_report, sizeof(gameoverlay_thread_report), 0 } if (GetThreadContext(thread_handle, &context) && context.Dr7) { gameoverlay_debug_report.unknown = 0 gameoverlay_debug_report.report_id = 0x3B gameoverlay_debug_report.debug_register = context.Dr0 battleye::report(&gameoverlay_debug_report, sizeof(gameoverlay_debug_report), 0 } } } 

最小二乘


Windows lsass.exe进程(也称为“本地安全授权”进程)的内存地址空间也将被扫描,并将所有异常报告给服务器,就像之前的两次检查一样:

 if (equals(process_entry.executable_path, "lsass.exe")) { auto lsass_handle = OpenProcess(QueryInformation, 0, (unsigned int)process_entry.th32ProcessID if (lsass_handle) { for (address = 0 NtQueryVirtualMemory(lsass_handle, address, 0, &lsass_memory_info, 0x30, &bytes_needed) >= 0 address = lsass_memory_info.base_address + lsass_memory_info.region_size) { if (lsass_memory_info.state == MEM_COMMIT && lsass_memory_info.type == MEM_PRIVATE && (lsass_memory_info.protect == PAGE_EXECUTE || lsass_memory_info.protect == PAGE_EXECUTE_READ || lsass_memory_info.protect == PAGE_EXECUTE_READWRITE)) { // FOUND EXECUTABLE MEMORY OUTSIDE OF MODULES lsass_report.unknown = 0 lsass_report.report_id = 0x42 lsass_report.base_address = lsass_memory_info.base_address lsass_report.region_size = lsass_memory_info.region_size lsass_report.memory_info = lsass_memory_info.type | lsass_memory_info.protect | lsass_memory_info.state battleye::report(&lsass_report, sizeof(lsass_report), 0 } } CloseHandle(lsass_handle } } 

LSASS以前曾在漏洞利用程序中用于执行内存操作,因为任何需要Internet连接的进程都必须提供LSASS访问权限。BattlEye当前正在通过手动从读取/写入操作中清除进程句柄,然后附加ReadProcessMemory/ WriteProcessMemory,将调用重定向到其BEDaisy驱动程序来处理此问题接下来,BEDaisy决定存储操作是否合法。如果他认为该操作是合法的,则继续进行操作,否则有意地将机器打开蓝屏。

各种通知


BattlEye收集各种信息,并将其与通知ID一起发送到服务器3C此信息包含以下元素:

  • 带有WS_EX_TOPMOST标志或其类似物的任何窗口
    • (Unicode)
    • (Unicode)
    • Window style
    • Window extended style
    • -
    • -
  • (VM_WRITE|VM_READ)
  • :
    • ….ContentPaksTslGame-WindowsNoEditor_assets_world.pak
    • ….ContentPaksTslGame-WindowsNoEditor_ui.pak
    • ….ContentPaksTslGame-WindowsNoEditor_sound.pak

  • :
    • ….BLGameCookedContentScriptBLGame.u
  • NtGetContextThread
    • (E9),

NoEye


BattlEye实现了一个相当懒惰的检查,以检测可访问的公共Rootkit绕过称为NoEye的反作弊:如果在磁盘上找到了该库,则系统使用GetFileAttributesExA来检查文件大小BE_DLL.dll

 void noeye::detect() { WIN32_FILE_ATTRIBUTE_DATA file_information if (GetFileAttributesExA("BE_DLL.dll", 0, &file_information)) { noeye_report.unknown = 0 noeye_report.report_id = 0x3D noeye_report.file_size = file_information.nFileSizeLow battleye::report(&noeye_report, sizeof(noeye_report), 0 } } 

驱动程序可用性


检查哔声和空设备;如果有,将生成通知。在正常状态下,这两个设备在系统中不可用,这可能表明有人手动打开了该设备。此技术称为驱动程序设备劫持。这是为了确保与恶意驱动程序交换IOCTL数据,而无需为此驱动程序使用单独的驱动程序对象。

 void driver::check_beep() { auto handle = CreateFileA("\\.\Beep", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0 if (handle != INVALID_HANDLE_VALUE) { beep_report.unknown = 0 beep_report.report_id = 0x3E battleye::report(&beep_report, sizeof(beep_report), 0 CloseHandle(handle } } 

 void driver::check_null() { auto handle = CreateFileA("\\.\Null", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0 if (handle != INVALID_HANDLE_VALUE) { null_report.unknown = 0 null_report.report_id = 0x3E battleye::report(&null_report, sizeof(null_report), 0 CloseHandle(handle } } 

睡眠三角洲


另外,BattlEye可以从当前线程请求一秒钟的不活动状态,并测量不活动(睡眠)前后的周期数之差:

 void sleep::check_delta() { const auto tick_count = GetTickCount Sleep(1000 const auto tick_delta = GetTickCount() - tick_count if (tick_delta >= 1200) { sleep_report.unknown = 0 sleep_report.report_id = 0x45 sleep_report.delta = tick_delta battleye::report(&sleep_report, sizeof(sleep_report), 0 } } 

7zip


BattlEye中添加了一个非常懒惰的完整性检查,因此用户无法将7zip库加载到游戏进程和覆盖区域中。这是由用户完成的,目的是降低先前描述的模式扫描和异常检测的严重性。BattlEye只是决定为此特定的7zip库添加完整性检查。

 void module::check_7zip() { constexpr auto sz_7zipdll = "..\..\Plugins\ZipUtility\ThirdParty\7zpp\dll\Win64\7z.dll" const auto module_handle = GetModuleHandleA(sz_7zipdll if (module_handle && *(int*)(module_handle + 0x1000) != 0xFF1441C7) { sevenzip_report.unknown_1 = 0 sevenzip_report.report_id = 0x46 sevenzip_report.unknown_2 = 0 sevenzip_report.data1 = *(__int64*)(module_handle + 0x1000 sevenzip_report.data2 = *(__int64*)(module_handle + 0x1008 battleye::report(&sevenzip_report, sizeof(sevenzip_report), 0 } } 

硬件抽象层


BattlEye检查是否存在动态链接的Windows硬件抽象层库(hal.dll),并告知服务器是否在游戏中加载了该库。

 void module::check_hal() { const auto module_handle = GetModuleHandleA("hal.dll" if (module_handle) { hal_report.unknown_1 = 0 hal_report.report_id = 0x46 hal_report.unknown_2 = 2 hal_report.data1 = *(__int64*)(module_handle + 0x1000 hal_report.data2 = *(__int64*)(module_handle + 0x1008 battleye::report(&hal_report, sizeof(hal_report), 0 } } 

图像检查


BattlEye还检查加载到游戏中的各种图像。这些模块被认为是经过签名的图像,它们以某种方式被操纵,将其行为改变为恶意,但是我们不能再谈论它们了,而仅是关于它们的检测:

nvToolsExt64_1


 void module::check_nvtoolsext64_1 { const auto module_handle = GetModuleHandleA("nvToolsExt64_1.dll" if (module_handle) { nvtools_report.unknown = 0 nvtools_report.report_id = 0x48 nvtools_report.module_id = 0x5A8 nvtools_report.size_of_image = (PE_HEADER*)(module_handle + (DOS_HEADER*)(module_handle)->e_lfanew))->SizeOfImage battleye::report(&nvtools_report, sizeof(nvtools_report), 0 } } 

ws2detour_x96


 void module::check_ws2detour_x96 { const auto module_handle = GetModuleHandleA("ws2detour_x96.dll" if (module_handle) { ws2detour_report.unknown = 0 ws2detour_report.report_id = 0x48 ws2detour_report.module_id = 0x5B5 ws2detour_report.size_of_image = (PE_HEADER*)(module_handle + (DOS_HEADER*)(module_handle)->e_lfanew))->SizeOfImage battleye::report(&ws2detour_report, sizeof(ws2detour_report), 0 } } 

网络dllx64


 void module::check_networkdllx64 { const auto module_handle = GetModuleHandleA("networkdllx64.dll" if (module_handle) { const auto dos_header = (DOS_HEADER*)module_handle const auto pe_header = (PE_HEADER*)(module_handle + dos_header->e_lfanew const auto size_of_image = pe_header->SizeOfImage if (size_of_image < 0x200000 || size_of_image >= 0x400000) { if (pe_header->sections[DEBUG_DIRECTORY].size == 0x1B20) { networkdll64_report.unknown = 0 networkdll64_report.report_id = 0x48 networkdll64_report.module_id = 0x5B7 networkdll64_report.data = pe_header->TimeDatestamp battleye::report(&networkdll64_report, sizeof(networkdll64_report), 0 } } else { networkdll64_report.unknown = 0 networkdll64_report.report_id = 0x48 networkdll64_report.module_id = 0x5B7 networkdll64_report.data = pe_header->sections[DEBUG_DIRECTORY].size battleye::report(&networkdll64_report, sizeof(networkdll64_report), 0 } } } 

nxdetours_64


 void module::check_nxdetours_64 { const auto module_handle = GetModuleHandleA("nxdetours_64.dll" if (module_handle) { nxdetours64_report.unknown = 0 nxdetours64_report.report_id = 0x48 nxdetours64_report.module_id = 0x5B8 nxdetours64_report.size_of_image = (PE_HEADER*)(module_handle + (DOS_HEADER*)(module_handle)->e_lfanew))->SizeOfImage battleye::report(&nxdetours64_report, sizeof(nxdetours64_report), 0 } } 

nv编译器


 void module::check_nvcompiler { const auto module_handle = GetModuleHandleA("nvcompiler.dll" if (module_handle) { nvcompiler_report.unknown = 0 nvcompiler_report.report_id = 0x48 nvcompiler_report.module_id = 0x5BC nvcompiler_report.data = *(int*)(module_handle + 0x1000 battleye::report(&nvcompiler_report, sizeof(nvcompiler_report), 0 } } 

wmp


 void module::check_wmp { const auto module_handle = GetModuleHandleA("wmp.dll" if (module_handle) { wmp_report.unknown = 0 wmp_report.report_id = 0x48 wmp_report.module_id = 0x5BE wmp_report.data = *(int*)(module_handle + 0x1000 battleye::report(&wmp_report, sizeof(wmp_report), 0 } } 

模块枚举标识符


作为参考,我们给出了模块的枚举ID:

 enum module_id { nvtoolsext64 = 0x5A8, ws2detour_x96 = 0x5B5, networkdll64 = 0x5B7, nxdetours_64 = 0x5B8, nvcompiler = 0x5BC, wmp = 0x5BE 

扫描TCP表


BattlEye shellcode搜索整个系统的TCP连接列表(称为TCP表),并报告用户是否已连接到属于德国付费网站的Cloudflare网关IP地址中的至少一个-cheat称为xera.ph。此机制已添加到外壳程序代码中,以检测在游戏运行时正在运行启动器的用户,从而使其易于识别。这种机制的唯一问题是Cloudflare网关的IP地址可以稍后更改所有者。并且,如果他们的新所有者分发通过特定端口连接到其服务器的软件,那么毫无疑问会出现误报反欺诈触发。

付费作弊xera.ph服务提供商用户很长一段时间以来,他们一直在报告自己被捕获,开发人员无法以任何方式对其进行处理。我们与xera.ph的开发人员联系,以告知他们他们的愚蠢行为,但是他们误解了我们,并向我们发送了免费副本,却没有想到我们可以黑客并释放它。我们不会这样做,但您可能不应该免费向参与逆向工程的人员免费发送专有许可的二进制文件,并希望它们不会被盗版。

 void network::scan_tcp_table { memset(local_port_buffer, 0, sizeof(local_port_buffer for (iteration_index = 0 iteration_index < 500 ++iteration_index) { // GET NECESSARY SIZE OF TCP TABLE auto table_size = 0 GetExtendedTcpTable(0, &table_size, false, AF_INET, TCP_TABLE_OWNER_MODULE_ALL, 0 // ALLOCATE BUFFER OF PROPER SIZE FOR TCP TABLE auto allocated_ip_table = (MIB_TCPTABLE_OWNER_MODULE*)malloc(table_size if (GetExtendedTcpTable(allocated_ip_table, &table_size, false, AF_INET, TCP_TABLE_OWNER_MODULE_ALL, 0) != NO_ERROR) goto cleanup for (entry_index = 0 entry_index < allocated_ip_table->dwNumEntries ++entry_index) { const auto ip_address_match_1 = allocated_ip_table->table[entry_index].dwRemoteAddr == 0x656B1468 // 104.20.107.101 const auto ip_address_match_2 = allocated_ip_table->table[entry_index].dwRemoteAddr == 0x656C1468 // 104.20.108.101 const auto port_match = allocated_ip_table->table[entry_index].dwRemotePort == 20480 if ( (!ip_address_match_1 && !ip_address_match_2) || !port_match) continue for (port_index = 0 port_index < 10 && allocated_ip_table->table[entry_index].dwLocalPort != local_port_buffer[port_index ++port_index) { if (local_port_buffer[port_index]) continue tcp_table_report.unknown = 0 tcp_table_report.report_id = 0x48 tcp_table_report.module_id = 0x5B9 tcp_table_report.data = BYTE1(allocated_ip_table->table[entry_index].dwLocalPort) | (LOBYTE(allocated_ip_table->table[entry_index.dwLocalPort) << 8 battleye::report(&tcp_table_report, sizeof(tcp_table_report), 0 local_port_buffer[port_index] = allocated_ip_table->table[entry_index].dwLocalPort break } } cleanup: // FREE TABLE AND SLEEP free(allocated_ip_table Sleep(10 } } 

通知类型


以下是Shell代码中所有已知通知类型的参考:

 enum BATTLEYE_REPORT_ID { MEMORY_GUARD = 0x21, MEMORY_SUSPICIOUS = 0x2F, WINDOW_TITLE = 0x33, MEMORY = 0x35, PROCESS_ANOMALY = 0x38, DRIVER_BEEP_PRESENCE = 0x3E, DRIVER_NULL_PRESENCE = 0x3F, MISCELLANEOUS_ANOMALY = 0x3B, PROCESS_SUSPICIOUS = 0x40, LSASS_MEMORY = 0x42, SLEEP_ANOMALY = 0x45, MEMORY_MODULE_SPECIFIC = 0x46, GENERIC_ANOMALY = 0x48, MEMORY_MODULE_SPECIFIC2 = 0x5B, } 

Source: https://habr.com/ru/post/zh-CN483068/


All Articles