BattlEye的主要Shellcode更新
时间的流逝,反作弊的变化,以及为了提高产品的有效性,功能在其中出现和消失。 一年前,我在我的
博客 [Habré上的
翻译 ]中准备了BattlEye shellcode的详细描述,本文的这一部分将简单地反映对shellcode所做的更改。
列入黑名单的时间戳
在最近的BattlEye分析中,影子禁止列表中只有两个编译时间戳记,并且看起来开发人员决定添加更多:
0x5B12C900 (action_x64.dll)
0x5A180C35 (TerSafe.dll, Epic Games)
0xFC9B9325 (?)
0x456CED13 (d3dx9_32.dll)
0x46495AD9 (d3dx9_34.dll)
0x47CDEE2B (d3dx9_32.dll)
0x469FF22E (d3dx9_35.dll)
0x48EC3AD7 (D3DCompiler_40.dll)
0x5A8E6020 (?)
0x55C85371 (d3dx9_32.dll)
0x456CED13 (?)
0x46495AD9 (D3DCompiler_40.dll)
0x47CDEE2B (D3DX9_37.dll)
0x469FF22E (?)
0x48EC3AD7 (?)
0xFC9B9325 (?)
0x5A8E6020 (?)
0x55C85371 (?)
我无法确定剩余的时间戳,并且两个
0xF *******是Visual Studio确定性程序集创建的哈希。 感谢@mottikraus和T0B1标识了一些时间戳。
模块检查
如主要分析所示,BattlEye的关键功能是模块枚举,从上一次分析起,便在列表中添加了另一个模块:
void battleye::misc::module_unknown1() { if (!GetProcAddress(current_module, "NSPStartup")) return; if (optional_header.data_directory[4].size == 0x1B20 || optional_header.data_directory[4].size == 0xE70 || optional_header.data_directory[4].size == 0x1A38 || timestamp >= 0x5C600000 && timestamp < 0x5C700000) { report_module_unknown report = {}; report.unknown = 0; report.report_id = 0x35; report.val1 = 0x5C0; report.timestamp = timestamp; report.image_size = optional_header.size_of_image; report.entrypoint = optional_header.address_of_entry_point; report.directory_size = optional_header.data_directory[4].size; battleye::report(&report, sizeof(report), false); } }
这可能是对某些代理dll的检测,因为在此检查了重定向表的大小。
窗口标题
在先前的分析中,各种作弊提供者都被标记了窗口名称,但是从那时起,shellcode便停止检查这些窗口标题。 窗口标题列表已完全替换为:
Chod's
Satan5
图片名称
BattlEye因使用非常原始的检测方法而臭名昭著,其中之一是图像名称黑名单。 每年,禁止使用的图片名称列表越来越长,在过去的11个月中,添加了五个新图片:
frAQBc8W.dll
C:\\Windows\\mscorlib.ni.dll
DxtoryMM_x64.dll
Project1.dll
OWClient.dll
值得注意的是,模块名称与列表中任何项目相对应的存在并不意味着您将被立即禁止。 报告引擎还传达基本的模块信息,最有可能用于区分作弊行为与BattlEye服务器上的冲突。
7拉链
7-Zip被广泛使用,并且在作弊场景中一直被参与者用作代码空缺(代码空洞)的记忆填充物。 BattlEye尝试通过执行
非常差的完整性检查来解决此问题,自从我上一篇文章以来,这种检查已发生更改:
void module::check_7zip() { const auto module_handle = GetModuleHandleA("..\\..\\Plugins\\ZipUtility\\ThirdParty\\7zpp\\dll\\Win64\\7z.dll");
看来BattlEye开发人员已经猜到我的上一篇文章已经导致许多用户通过简单地将所需的字节复制到BattlEye检查的位置来绕过此检查。 他们如何解决这种情况? 我们将验证移位了八个字节,并继续使用相同的不良方法来检查完整性。 只读可执行分区,而您需要做的就是从磁盘下载7-Zip并将相互移动的分区进行比较。 如果有任何差异,那是错误的。 认真地说,伙计们,执行完整性检查并不难。
网络检查
枚举TCP表仍然有效,但是在我发布先前的分析批评开发人员标记Cloudflare IP地址之后,他们仍然删除了此检查。 Anti-cheat仍报告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) {
谢谢IChooseYou和摘要
BattlEye堆栈绕过
黑客游戏是猫和老鼠的不变游戏,因此有关新花样的谣言如火如荼地传播。 在这一部分中,我们将研究由大型反作弊者BattlEye提供的最新启发式技术。 通常,这些技术称为堆栈遍历。 通常,它们是通过处理一个函数并遍历堆栈以找出专门调用此函数的人来实现的。 为什么需要这样做? 像任何其他程序一样,视频游戏黑客具有一组众所周知的功能,可用于从键盘获取信息,将其输出到控制台或计算某些数学表达式。 另外,视频游戏黑客喜欢隐藏它们的存在,无论是在内存中还是在磁盘上,以便防作弊软件无法找到它们。 但是作弊程序忘记的是它们会定期从其他库中调用函数,这可用于启发式检测未知作弊。 通过为
std::print
函数实现堆栈遍历引擎,即使这些作弊被屏蔽,我们也可以找到它们。
BattlEye
实施了 “堆栈绕过”,尽管这一事实尚未公开宣布,并且在本文发表时只有谣言。 注意引号-您将在这里看到的不是真正的堆栈巡回,而是检查返回地址和调用程序的转储的组合。 真正的堆栈遍历实现将遍历整个堆栈并生成一个真正的调用堆栈。
正如我在上一篇有关BattlEye的文章中所解释的那样,防作弊系统在运行时将shellcode动态地流到游戏中。 这些外壳代码具有不同的大小和任务,并且不会同时发送。 这种系统的显着特性是研究人员需要在多人比赛中动态分析反作弊,这使得确定这种反作弊的特性变得复杂。 它还允许反作弊对不同的用户采取各种措施,例如,仅将更深入的入侵模块转移给谋杀和死亡等比例异常高的人。
这些shell代码之一BattlEye负责执行此堆栈分析。 我们将其称为
shellcode8kb,因为它比我
在此处记录的
shellcodemain略小。 这个使用
AddVectoredExceptionHandler函数的小外壳代码准备了一个矢量化的异常处理程序,然后在以下函数上设置了中断陷阱:
GetAsyncKeyState
GetCursorPos
IsBadReadPtr
NtUserGetAsyncKeyState
GetForegroundWindow
CallWindowProcW
NtUserPeekMessage
NtSetEvent
sqrtf
__stdio_common_vsprintf_s
CDXGIFactory::TakeLock
TppTimerpExecuteCallback
为此,它仅遍历标准使用的函数列表,将相应函数的第一条指令设置为
int3 ,这将用作断点。 设置断点后,对相应函数的所有调用都会通过异常处理程序,该处理程序可以完全访问寄存器和堆栈。 具有此访问权限后,异常处理程序将从堆栈的顶部转储调用程序的地址,如果满足启发式条件之一,则将转储32个字节的调用函数,并将其发送到报告标识符为
0x31的BattlEye服务器:
__int64 battleye::exception_handler(_EXCEPTION_POINTERS *exception) { if (exception->ExceptionRecord->ExceptionCode != STATUS_BREAKPOINT) return 0; const auto caller_function = *(__int64 **)exception->ContextRecord->Rsp; MEMORY_BASIC_INFORMATION caller_memory_information = {}; auto desired_size = 0;
如我们所见,异常处理程序会在内存页发生不经意的更改时或在该函数不属于已知的处理模块时(手动映射程序未设置内存页类型MEM_IMAGE)转储所有调用函数。 当无法调用
NtQueryVirtualMemory时,它还会转储调用函数,以使作弊程序不会绑定到此系统调用,也不会在堆栈转储中隐藏其模块。 最后一个条件实际上非常有趣,它标记了所有使用
jmp qword ptr [rbx]小工具的调用函数-用于“欺骗返回地址”的方法。 它是
由我的联合秘书成员昵称namazso发布的。 BattlEye的开发人员似乎看到人们在他们的游戏中使用这种欺骗方法,因此决定直接针对它。 这里值得一提的是,namazsos描述的方法效果很好,只是使用一个不同的小工具,或者完全不同,或者只是一个不同的寄存器-没关系。
BattlEye开发人员提示:
CDXGIFactory::TakeLock
在内存
CDXGIFactory::TakeLock
的
CDXGIFactory::TakeLock
是错误的,因为您(偶然或有意地)启用了CC填充,每次填充都大不相同。 为了获得最大的兼容性,您需要删除填充(签名中的第一个字节),因此您很可能会抓到更多作弊者:)
发送到BattlEye服务器的完整结构如下所示:
struct __unaligned battleye_stack_report { __int8 unknown; __int8 report_id; __int8 val0; __int64 caller; __int64 function_dump[4]; __int64 allocation_base; __int64 base_address; __int32 region_size; __int32 type_protect_state; };
BattlEye中的管理程序识别
黑客游戏领域的猫和老鼠游戏仍然是漏洞利用和作弊斗争的创新之源。 在诸如
DdiMon Satoshi Tanda和
hvpp Peter Benes这样的易于使用的虚拟机管理程序出现之后,就开始积极地在黑客游戏中使用虚拟化技术。 由于入门门槛低和详细的文档记录,这两个项目被地下黑客界的大多数有偿作弊者所使用。 这些发行版可能会加速虚拟机管理程序领域的军备竞赛,而虚拟机管理程序领域现在已经开始在游戏黑客社区中崭露头角。 这是昵称
wlan的最大游戏黑客社区之一的管理员对这种情况的评价:
随着用于黑客游戏的即用型虚拟机管理程序系统的出现,像BattlEye这样的反作弊不可避免地将重点放在对虚拟化的普遍认可上。
系统管理程序的广泛使用归因于反欺诈方面的最新进展,这使黑客几乎没有机会以传统方式修改游戏。 可以通过避免反欺诈的简单性来解释虚拟机管理程序的普及,因为虚拟化使用诸如
系统调用挂钩和
MMU虚拟化之类的机制简化了信息隐藏。
最近,BattlEye使用基于时间的检测实现了对常见虚拟机管理程序的识别,例如上述平台(DdiMon,hvpp)。 此识别尝试检测非标准的CPUID指令时间值。 CPUID是在实际设备上相对低成本的指令,通常仅需要200个周期,并且在虚拟环境中,由于自省引擎引起的不必要操作,其执行时间可能会延长十倍。 内省引擎与实际设备不同,后者仅以预期的方式执行操作,因为它会基于任意准则跟踪并有条件地更改返回给来宾的数据。
有趣的事实: CPUID在这些临时识别过程中得到了积极使用,因为它是具有无条件输出的指令以及具有非特权序列化的指令。 这意味着将CPUID用作
屏障,并确保遵循它之前和之后的指令; 同时,计时变得与指令的通常重新排序无关。 您还可以使用
XSETBV之类的
指令 ,它们也会执行无条件退出,但是为了确保独立的时序,这将需要某种屏障指令,以便在其前后都不会发生重新排序,从而影响了时序的可靠性。
认可度
以下是BattlEye“ BEClient2”模块的识别过程; 我进行了反向工程,并用伪C重新创建了代码,然后将其发布在
twitter上 。 在我发布推文后的第二天,BattlEye开发人员意外地更改了BEClient2的混淆,显然希望这会阻止我分析模块。 以前的混淆并没有改变一年多,但在我发推文后的第二天发生了改变-令人印象深刻的速度。
void battleye::take_time() {
如上所述,这是使用无条件拦截指令的最常见识别技术。 但是,它很容易受到伪造时间的影响,我们将在下一部分中详细讨论。
识别绕过
该识别方法存在问题。 首先,它容易产生伪造的时间,通常可以通过两种方式完成:通过在VMCS中转移TSC或每次执行CPUID时减少TSC。 还有许多其他方法可以处理基于时间的攻击,但是后者更易于实现,因为您可以保证指令的执行时间将在真实设备上的执行同步的一两个时钟周期内。 发现这种仿冒技术的难度取决于开发人员的经验。 在下一节中,我们将研究时间伪造和改进BattlEye中创建的实现。 此识别方法存在缺陷的第二个原因是,不同工作表中的CPUID延迟(运行时)非常不同,具体取决于工作表的值。 最多可能需要70-300个时钟周期才能完成。 此识别过程的第三个问题是使用SetThreadPriority。 Windows函数用于设置给定流描述符的优先级值,但是OS并不总是侦听请求。 该函数只是提高线程优先级的建议,不能保证会发生。 因此,此方法可能会受到中断或其他过程的影响。
在这种情况下,容易绕过识别,并且所描述的伪造时间技术有效地挫败了该识别方法。 如果BattlEye的开发人员想要改进此方法,则以下部分提供一些建议。
改善
此功能可以通过多种方式进行改进。 首先,可以通过将CR8更改为最高IRQL来有意地禁用中断并强制线程的优先级。 将这一检查隔离在一个CPU内核中也是理想的。 另一个改进:您应该使用不同的计时器,但是其中许多计时器不如TSC准确,但是有一个这样的计时器称为APERF(实际性能时钟)。 我建议使用此计时器,因为用它作弊比较困难,并且仅在逻辑处理器处于电源状态C0时才累积一个计数器。 这是使用TSC的绝佳选择。 您还可以使用ACPI,HPET,PIT计时器,GPU计时器,NTP计时器或PPERF计时器,它们与APERF类似,但计数被视为执行指令的度量。 这样做的缺点是您需要启用HWP,而HWP可以被中间操作员禁用,因此它是无用的。
以下是应该在内核中执行的识别过程的改进版本:
void battleye::take_time() { std::uint32_t cpuid_regs[4] = {}; _disable(); const auto aperf_pre = __readmsr(IA32_APERF_MSR) << 32; __cpuid(&cpuid_regs, 1); const auto aperf_post = __readmsr(IA32_APERF_MSR) << 32; const auto aperf_diff = aperf_post - aperf_pre;
注意: IET代表指令执行时间。
但是,该过程在检测常见的虚拟机管理程序上仍然非常不可靠,因为CPUID运行时可能会有很大差异。
比较两个指令的IET会更好。其中之一的执行延迟应比CPUID长。例如,它可能是FYL2XP1,这是一种比CPUID指令的平均IET要更长的时间才能完成的算术指令。此外,它不会在管理程序中引起任何陷阱,并且可以可靠地测量其时间。使用这两个功能,性能分析功能可以创建一个数组,用于存储IET指令CPUID和FYL2XP1。使用APERF计时器,可以获得算术指令的初始时钟,执行该指令并为其计算时钟增量。可以将结果存储在IET数组中N个分析周期,获取平均值,并对CPUID重复该过程。如果CPUID指令的执行时间比算术指令长,那么这是一个可靠的信号,表明该系统是虚拟的,因为在任何情况下,一条算术指令都不会比执行CPUID花费更多的时间来获取有关制造商或版本的信息。该识别程序还将能够检测使用TSC偏移/缩放的那些。我再说一遍,开发人员必须强制启用绑定到计算内核才能在一个内核上执行此检查,禁用中断并强制IRQL设置最大值以确保数据一致且可靠。如果BattlEye的开发人员决定实施此功能,那将是令人惊讶的,因为它需要付出更多的努力。在内核驱动程序中,BattlEye吃了另外两个虚拟机识别例程,但这是另一篇文章的主题。