Menu

BattlEye 虚拟化检测(VT)

引言( Introduction )

游戏外挂开发者和游戏开发商继续上演着猫捉老鼠的游戏,他们的发展持久,却很缓慢。但是, 自从Satoshi Tanda的DdiMon和PetrBeneš的hvpp这2个基于虚拟化技术的产品开源以来,虚拟化技术在游戏辅助中的使用就激增了。 由于进入门槛低和文档丰富,这两个项目被地下黑客经过包装,从而有偿提供给了更多的游戏辅助开发者。这些版本不仅仅拥有强大的隐蔽性,也突破了操作系统本身的瓶颈,从而更针对的进行游戏攻击。 这是全球最大的游戏黑客社区之一的管理员对这种情况的评价:随着现成的用于游戏辅助开发的的虚拟机监控程序解决方案出现,诸如BattlEye之类的反作弊手段不可避免地将重点放在通用虚拟化检测上。

虚拟化技术之所有发展如此之迅速,是因为游戏保护开发商的内核反作弊技术快速发展,使得黑客通过传统方式修改游戏的空间很小。而虚拟化技术的普及,使游戏辅助开发者能够通过诸如syscall hookMMU 虚拟化之类的机制更轻松地从反作弊中隐藏自己的信息。

BattlEye最近使用基于时间的检测实现了对通用管理程序虚拟化的检测,例如前面提到的平台(DdiMon,hvpp)。 此检测旨在发现指令CPUID中的异常时间值。 CPUID是在实际硬件上相对便宜的指令,通常只需要200个周期,而在虚拟化环境中,由于自省引擎的开销,它可能需要多达十倍的时间。 内省引擎不像任何实际硬件那样仅按预期执行操作,它会根据任意条件监视并有条件地更改返回给来宾的数据。

有趣的事:CPUID通常在这些基于时间的检测例程中使用,因为它是无条件退出的指令,也是无特权的序列化指令。 这意味着CPUID充当“栅栏”,并确保指令完成之前或之后,并使时序独立于典型的指令重新排序。 可以使用诸如XSETBV之类的指令,该指令也可以无条件退出,但是要确保独立的时序,则需要使用某种FENCE指令,以便在此之前或之后都不会发生重新排序,这将影响时序可靠性。

检测( Detection )

下面是检测例程,在将其发布之前,我从BattlEye模块“ BEClient2”进行了逆向工程并将其重构为伪代码(C语言)。 在我发推文的第二天,BattlEye开发人员在意料之外的情况下更改了对BEClient2的混淆,这可能是希望它会阻止我分析模块。 以前的混淆在一年多的时间内没有发生变化,但是在我发布推文后的第二天发生了变化,这是一个令人印象深刻的转变。

void battleye::take_time()
{
    // SET THREAD PRIORITY TO THE HIGHEST
    const auto old_priority = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
 
    // CALCULATE CYCLES FOR 1000MS
    const auto timestamp_calibrator = __rdtsc();
    Sleep(1000);
    const auto timestamp_calibration = __rdtsc() - timestamp_calibrator;
 
    // TIME CPUID
    auto total_time = 0;
    for (std::size_t count = 0; count < 0x6694; count++)
    {
        // SAVE PRE CPUID TIME
        const auto timestamp_pre = __rdtsc();
 
        std::uint32_t cpuid_data[4] = {};
        __cpuid(cpuid_data, 0);
 
        // SAVE THE DELTA
        total_time += __rdtsc() - timestamp_pre;
    }
 
    // SAVE THE RESULT IN THE GLOBAL REPORT TABLE
    battleye::report_table[0x1A8] = 10000000 * total_time / timestamp_calibration / 0x65;
 
    // RESTORE THREAD PRIORITY
    SetThreadPriority(GetCurrentThread(), old_priority);
}

如前所述,这是使用无条件拦截指令的最常见检测技术。 但是,这项技术容易受到时间伪造的影响,我们将在下一部分中详细介绍。

规避( Circumvention )

此检测方法存在一些问题。首先是它容易受到时间伪造的影响,通常可以通过以下两种方式之一进行:VMCS中的TSC偏移,或每次执行CPUID时都减小TSC。有很多方法可以克服基于时间的攻击,但是后者可以更轻松地实现,因为您可以确保指令执行时间在实际硬件执行的一两个时钟TICK之内。根据经验,检测这种时间锻造技术可能很困难。在下一部分中,我们将介绍时间伪造的检测以及对BattlEye实施的改进。此检测方法存在缺陷的第二个原因是,CPUID延迟(执行时间)在处理器之间变化很大,并且根据给定的叶子值可能会变得更糟。它需要70-300个周期才能执行。此检测例程的第三个问题是SetThreadPriority的用法。 Windows函数用于为给定的线程句柄指定优先级值,但是,操作系统并不总是侦听请求。此函数仅是提高线程优先级的建议,无法保证会发生此问题,因此使此方法容易受到中断或其他进程的干扰。

在这种情况下,规避很简单,所描述的伪造时间技术有效地击败了这种检测方法。 如果BattlEye想要改进此方法,则在下一节中将提供一些建议。

改善( Improvement )

可以对该功能进行多项改进。 首先是故意禁用中断,并通过将CR8修改为最高IRQL来强制线程优先级。 将测试隔离到单个CPU内核也是理想的。 其他改进将是使用不同的计时器,但是,许多计时器不如TSC准确,但是有一个这样的计时器称为APERF计时器。 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;
     
    // CPUID IET ARRAY STORE
    // BATTLEYE REPORT TABLE STORE
     
    _enable();
}

注意:IET仅表示指令执行时间。

但是,由于CPUID执行可能会发生巨大变化,因此这在检测通用管理程序时可能并不可靠。一个更好的主意是比较两个指令的IET。一种具有比CPUID更长的执行延迟。例如FYL2XP1,这是一条算术指令,它花费的时间比CPUID的平均IET稍长-它也不会导致任何陷阱进入我们的管理程序,并且可以可靠地计时。使用这两个指令,适当的性能分析函数将创建一个数组来存储CPUID和FYL2XP1的IET时间。使用APERF定时器,他们将获得算术指令的起始时钟,执行该指令并为其计算增量时钟计数。然后,他们将结果存储在IET数组中,以获取针对N个性能分析循环的特定指令的平均值,并重复执行CPUID。如果CPUID指令的执行时间长于算术指令,则表明系统已虚拟化,这是可靠的指示,因为在任何情况下,算术指令都不应花费比CPUID执行时间更长的时间来获取供应商或版本信息。此检测还将捕获使用TSC偏移/缩放的对象

再次,他们将需要强制执行该测试的亲和力集中到一个内核,禁用中断,并将IRQL强制设为最大值,以确保数据的一致性和可靠性。 如果BattlEye开发人员决定执行此操作,那将是令人惊讶的,因为它需要付出更大的努力。 BattlEye的内核驱动程序中还有另外两个虚拟机检测例程。

Categories:   Garfield's Diary

Comments

  • 头像

    Posted: 2020年5月3日 17:12

    ZHANG

    请问如何跟你请教一些问题

头像