关于Battle Eye (下)
接着一篇,我们继续来看关于BE的检测手段。不过在此之前,我来Show一下《逃离塔科夫》的辅助截图效果吧:
书归正传,接着讲:)
Image name
如果您的进程符合以下任何其他条件,您将立即被标记并报告给服务器,报告ID为0x38
Image name contains "Loadlibr"
Image name contains "Rng "
Image name contains "A0E7FFFFFF81"
Image name contains "RNG "
Image name contains "90E54355"
Image name contains "2.6.ex"
Image name contains "TempFile.exe"
Steam game overlay
BattlEye 始终关注Steam 游戏窗口叠加过程,Steam游戏叠加层窗口的完整进程名称为gameoverlayui.exe,并且众所周知,该图像集可用于渲染,因为劫持和绘制数据到游戏窗口非常简单。 检查的条件是:
file size != 0 && image name contains (case insensitive) gameoverlayu
特定Steam游戏叠加层窗口的检查几乎与在游戏进程本身上运行的例子相同,因此已从伪代码中省略了它们。
Steam Game Overlay memory scan
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 Game Overlay process protection
如果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的报告。
模块枚举(Module enumeration)
还枚举了Steam游戏叠加过程的模块,特别是寻找vgui2_s.dll和gameoverlayui.dll。 从gameoverlayui.dll开始,已经对这些相应的模块进行了某些检查。
如果此条件匹配:[gameoverlayui.dll + 6C779] == 08BE55DC3CCCCB8 ????? C3CCCCCC,则shellcode将扫描位于字节?????????中的地址的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
}
}
}
}
先前的例程在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 Game Overlay threads
线程也会被枚举:
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
}
}
}
LSASS
枚举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决定该存储操作是否为合法操作。 如果确定该操作是合法的,它将继续进行操作,否则,他们将故意对计算机进行蓝屏显示。
Misc. report
BattlEye 收集杂项信息,并将其发送回报告ID为3C的服务器。 该信息包括:
- Any window with WS_EX_TOPMOST flag or equivalent alternatives:
- Window text (Unicode)
- Window class name (Unicode)
- Window style
- Window extended style
- Window rectangle
- Owner process image path
- Owner process image size
- Any process with an open process handle (VM_WRITE|VM_READ) to the game
- Image name
- Image path
- Image size
- Handle access
- File size of game specific files:
- ….ContentPaksTslGame-WindowsNoEditor_assets_world.pak
- ….ContentPaksTslGame-WindowsNoEditor_ui.pak
- ….ContentPaksTslGame-WindowsNoEditor_sound.pak
- Contents of game specific files:
- ….BLGameCookedContentScriptBLGame.u
- Detour information of NtGetContextThread
- Any jump instructions (E9) are followed and the final address get’s logged
NoEye
BattlEye通过检查由GetFileAttributesExA找到名称为BE_DLL.dll的任何文件,以检测是否存NoEye的bypass file。
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
}
}
Driver presence
检查驱动设备Beep 和 Null,并报告(如果存在)。 这两个系统通常在任何系统上都不可用,这表明有人手动启用了设备,也称为驱动程序设备劫持。 这样做是为了实现与恶意驱动程序的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
}
}
Sleep delta
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
}
}
Hardware abstraction layer
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
}
}
Image checks
BattlEye还检查加载到游戏过程中的各种image。 这些模块大多数都是经过正常签名的。
nvToolsExt64_1
NV显卡额外的辅助插件检测:
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
}
}
networkdllx64
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_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
}
}
Module id enumeration
模块的枚举ID:
enum module_id
{
nvtoolsext64 = 0x5A8,
ws2detour_x96 = 0x5B5,
networkdll64 = 0x5B7,
nxdetours_64 = 0x5B8,
nvcompiler = 0x5BC,
wmp = 0x5BE
}
TCP table scan
BattlEye shellcode还将搜索系统范围内的TCP连接列表(称为TCP表).
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
}
}
Report types
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,
}
Categories: Garfield's Diary