关于Battle Eye (上)
因为业余的时候,我都会做一些游戏的辅助工具,所以,难免会和游戏保护工具打交道。最近一段时间,都忙于一款游戏《逃离塔科夫》的辅助研究,在版本做好以后,也收集了不少关于BE的保护文章。
在我看来,在逃离塔科夫这个游戏中BE,保护级别还是很高的,所以难度挺大的。感觉BE是一款很不错的游戏保护,经过PUBG的锤炼,估计已经算是世界一线的游戏保护了。当然,国内TP更是。懂的人,自然懂。
接下来,我将翻译来自国外对BE保护的分析:
首先,Battle Eye官网:https://www.battleye.com/
BattlEye 算是世界一线的反游戏作弊的引擎,主要由32岁的创始人Bastian Heiko Suter开发,这个团队来自德国。他们使用通用的保护机制和特定于游戏的检测为游戏发行商提供了易于使用的反作弊解决方案,以提供最佳的安全性。正如他们的网站所说,他们总是紧跟最先进的技术,并利用创新的保护和检测方法,正如他们所在国家的工业制造一样,同样彰显了德国制造的质量。 BattlEye 由多个组件组成,这些组件协同工作以捕获并防止游戏中作弊的作弊者。
- BEService(与BattlEye服务器BEServer通信的Windows系统服务,该服务器提供BEDaisy和BEClient服务器-客户端通信功能。)
- BEDaisy(Windows内核驱动程序,用于注册预防性回调和微型过滤器,以防止作弊者非法修改游戏。)
- BEClient(Windows动态链接库,负责大多数检测,包括本文中的检测。 初始化后将其注入到游戏过程中。)
- BEServer (独立后端服务器,负责收集信息并针对作弊者采取具体行动。)
Shellcode : 最近,BattlEye的shellcode 被揭晓并发布到了互联网中,我们决定对BattlEye的当前迭内容进行记录。 虽然现在流出的Shell Code可能已经过时,但是根据研究表明,BattlEye 仅追加到新的Shellcode上,而不会删除以前的检测。
如何做到呢?BattlEye 可能会将其Shellcode从其服务器通过数据传输到Windows服务(称为BEService)。该服务与位于游戏进程内部的Battleye模块通信,称为BEClient。通过命名管道\ .namedpipeBattleye进行通信,直到去年也没有加密。现在,所有通信都通过异或进行加密,将shellcode传输到客户端后,将在任何已知模块之外分配并执行该shellcode,从而使区分变得容易。要转储Shellcode,您可以HOOK Windows API函数(例如CreateFile,ReadFile等),并转储任何已知模块外部的任何调用者各自的内存部分(查询返回地址上的内存信息),或定期扫描游戏的虚拟内存空间,用于存储任何已知模块之外的可执行内存,并将其转储到磁盘。确保跟踪已转储的部分,以免最终出现数千个相同的转储。
免责:本文章仅仅作为研究和翻译,不附带任何BE的攻击和BE攻击代码,如果您自己有实现的欲望而借与参考,乃巧合所至:)以下乃文章核心,请妥善仔细斟酌:)
内存枚举(Memory enumeration)
反作弊解决方案最常用的检测机制是内存枚举和内存扫描,以检测已知的作弊图像。 Battleye 枚举游戏进程的整个地址空间,并在页面可执行且位于相应Shellcode内存空间之外时运行各种检查。
这是它们的实现:
// 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
}
内存异常(Memory anomaly)
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
}
}
}
模式扫描(Pattern scans)
如前所述,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
}
}
}
}
}
Module specific (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
}
}
Module specific (Unknown)
更为精确的模块检查,如果您加载的模块符合以下任何条件,它将向您报告服务器:
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
}
内存守卫(Memory guard)
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
}
}
}
}
窗口枚举(Window enumeration)
BattlEye 的 shellcode枚举了游戏运行时当前可见的每个窗口,该过程通过从上至下(Z值)迭代窗口来实现。 游戏进程内部的窗口句柄不包括在上述枚举中,由GetWindowThreadProcessId 调用确定。 因此,您可以HOOK各自的功能以欺骗窗口的所有权,然后防止BattlEye枚举窗口。
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
}
}
枚举异常(Anomaly in enumeration)
如果枚举的窗口少于两个,则服务器将收到通知。 这样做可能是为了防止各个功能被Patch,防止BattlEye的shellcode查看任何窗口:
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
}
进程枚举(Process enumeration)
BattlEye会使用CreateToolhelp32Snapshot调用枚举所有正在运行的进程,但不会处理任何错误,因此很容易PATCH并防止检测。
Path check
如果image在至少两个子目录中(从磁盘根目录开始),则相应的Image路径中至少包含以下字符串之一,它将被标记:
Desktop
Temp
FileRec
Documents
Downloads
Roaming
tmp.ex
notepad.
...\.
cmd.ex
如果您的可执行路径与这些字符串之一匹配,则服务器将收到有关您的可执行路径的通知,以及有关父进程是否为以下之一的信息(包含发送到服务器的相应标志位):
steam.exe [0x01]
explorer.exe [0x02]
lsass.exe [0x08]
cmd.exe [0x10]
如果客户端无法使用相应的QueryLimitedInformation权限打开句柄,则如果OpenProcess调用失败的错误原因不等于ERROR_ACCESS_DENIED,它将设置标志位0x04,这将为我们提供相应标志值的最终枚举列:
enum BATTLEYE_PROCESS_FLAG
{
STEAM = 0x1,
EXPLORER = 0x2,
ERROR = 0x4,
LSASS = 0x8,
CMD = 0x10
}
如果Steam是父进程,您将立即被标记并以报告ID 0x40报告给服务器。
下一篇我们继续:)
Categories: Garfield's Diary