Menu

关于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

Comments