2025腾讯游戏安全技术竞赛-PC端决赛个人题解

强度很高的比赛,三天基本打满了。
写点关键词给自己引引流:腾讯游戏安全|2025腾讯游戏安全大赛|2025腾讯游戏安全决赛|腾讯游戏安全竞赛决赛

在intel CPU/64位Windows10系统上运行sys,成功加载驱动(0.5分)& 能在双机环境运行驱动并调试(1分)

双机调试

查壳VMP,首先得想办法能双机调试

利用工具查看驱动运行中调用的API:

ec7329355c822f27dc12e26fbe18787a

此工具项目地址:https://github.com/Oxygen1a1/DrvMon

一些API应该是用来判断当前环境是否启用了Intel VT虚拟化。另外检测到双机环境直接蓝屏,bug_checkcode:0x414345(ACE

选择一个较靠后执行的API,下断后dump驱动程序,考虑windbg的搜索功能搜PE头:

s -b fffff806`e3000000 fffff806`e302fa4c 4d 5a

一页一页的往前搜,搜到的地址如果恰好是内存对齐的,那么大概率就是PE头

Image_1047972905778612

然后就是dump驱动,SizeOfImage是大小,dump下来修SizeOfHeader、AddressOfEntryPoint、RawAddress后拖入IDA分析。

IDA中有很多类似这样的函数:

image-20250412002807063

这是通过哈希值来寻找导出函数的地址。还有一种是这样的形式的:

image-20250412130911444

逐一下断后看一下具体是什么函数:

ModuleBase + 31A8 PsGetProcessId
ModuleBase + 3268 PsGetProcessImageFileName
ModuleBase + 3328 ExAllocatePoolWithTag // PsGetCurrentThreadProcess
ModuleBase + 33E8 MmGetPhysicalAddress
ModuleBase + 34A8 MmGetVirtualForPhysical
ModuleBase + 3568 MmIsAddressValid
ModuleBase + 7A80 KdRefeshDebuggerNotPresent
ModuleBase + 85E4 KeQueryActiceProcessorCount
ModuleBase + 86A4 BgpFwGetCurrentIrql
ModuleBase + 8764 KeSetSystemAffinityThreadEx
ModuleBase + 8824 ExQueueWorkItem
ModuleBase + 88E4 KeBugCheckEx
ModuleBase + 89A4 KeDelayExecutionThread

680e0ab5870d082fb3dca5cb11e04071_720

这里能定位到一个反调函数KdRefreshDebuggerNotPresent

这是一个很经典的反调试函数,在ACE-BASE中早有应用,通常还搭配KdDisableDebugger一起使用,用来剥离调试器

下图源自:某CCC-BASE的逆向_ace-base.sys-CSDN博客

image-20250412133240887

那这里就直接Hook这两个函数(完整代码见附件Tencent-Anti-AntiDebug):

BOOLEAN __stdcall myKdRefreshDebuggerNotPresent()
{
	return true;
}


NTSTATUS __stdcall myKdDisableDebugger()
{
	return STATUS_DEBUGGER_INACTIVE;
}

成功绕过蓝屏:

b21d381d1a9032a0e708bc5b4df029f3

成功加载驱动

在IDA中注意到这样一个函数

image-20250412160319178

看样子是在初始化一个字符串,给这个函数下断看看是什么东西。

7c765080cbdfeab0dd88471aaacd61ef

直接断到返回值,字符串是一个注册表:

\Registry\Machine\System\CurrentControlSet\Services\ACEDriver\2025ACECTF

不清楚注册表的键值结构,考虑HookZwQueryValueKey,打印一下QueryValueName,得到以下输出:

dfd139d0637c342ac47d34f8b0fb1ce2

因此得手动创建这两个键,key指的是问题中耗时算法计算出的key

3cdbcc279daa0441392c83fb198eb1a5_720

驱动启动成功。

优化驱动中的耗时算法,并给出demo能快速计算得出正确的key(1分)

通过字符串定位到耗时函数

image-20250412003504493

逆向算法:

image-20250412003525689

image-20250412003535978

大意是计算了一个类帕斯卡三角形:

f(n,k) = f(n-1,k) + f(n-1,k-1) + n % 5 

未经优化的话这个算法的复杂度是:
$$
O(2^n)
$$
考虑dp优化:

import time

def solve_f(n, k):
    dp = [[0] * (k + 1) for _ in range(n + 1)]

    for i in range(n + 1):
        for j in range(min(i, k) + 1):
            if j == 0 or i == j:
                dp[i][j] = 1
            else:
                dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + i % 5

    return dp[n][k]


n = 0x2C  
k = 0x16  

start_time = time.time()

result = solve_f(n, k)

end_time = time.time()
elapsed_time = end_time - start_time

print(f"f(0x{n:X}, 0x{k:X}) = {result} (hex: 0x{result:X})")
print(f"运行时间: {elapsed_time:.6f} 秒")
# 0x66711265FD2
# 0.000228

这边的输入值来自上层函数:

image-20250412004157824

输出0x66711265FD2,即key

另外,优化后的算法复杂度、运行时间(秒)数量级是:
$$
O(nk)、10^{-4}
$$
优化后的时间远小于原算法。

分析并给出flag的计算执行流程(1.5分),能准确说明其串联逻辑(0.5分)

1.1 EPT Hook

从Key的计算函数能交叉引用找到Flag的计算流程:

image-20250412213511193

动调这一部分的时候,意外发现了一个事情:

输入的flag应为fake_flag,首位为0x66,经过mov指令读出赋值给al后,变成了95

结合到本题的环境,合理猜测:程序对这一内存页挂了Hook,读写内存时会触发EPT Violation由Host接管处理,具体怎么处理的还得逆对应的Handler。

容易在IDA找到一个简易的VM-Exit分发器:

image-20250412224526280

逆向分发器:

AI能根据intel手册对对应的handler命名:

image-20250412233533322

有些不太准确,大概都翻翻能看出来应该是这一个:

image-20250412233828440

这里有两段硬编码:

41 8A F0 :mov cl, byte ptr [r8 - 0x10]
88 45 A0:mov byte ptr [rbp - 0x60], al

说明从这里挂入了Hook,在mov操作时做了一个Encrypt

image-20250412234321287

这玩意我想了下应该是不可逆的,因为一个数对异或后进行接下来的操作,产生一个数,这个过程显然不可能是双射的。又因为是单字节的,所以考虑爆破处理。

uint8_t Encrypt(char a1, char a2)
{
    uint8_t result = 0;
    uint8_t xor_val = a1 ^ a2;
    uint8_t bit_count = 0;
    
    uint8_t tmp = xor_val;
    while (tmp) {
        bit_count += tmp & 1;
        tmp >>= 1;
    }

    uint8_t table[8] = {0, 1, 2, 3, 4, 5, 6, 7};
    uint64_t seed = bit_count;
    int n = 8;

    for (int i = 7; i > 0; i--) {
        seed = seed * 0x3F5713FCCC7C79AA + 0x3A7D9E5B36F498B2;
        int idx = ((uint32_t)(seed >> 32)) % n--;
        uint8_t tmp = table[i];
        table[i] = table[idx];
        table[idx] = tmp;
    }


    for (int i = 0; i < 8; i++) {
        uint8_t bit = (xor_val >> table[i]) & 1;
        result |= (bit << i);
    }

    return result;
}

第一部分单字节加密处理完毕

1.2 HookedTea

保留节目之魔改TEA,没开始做这一问的时候就在IDA里找到一个TEA:

image-20250412235957434

显然这大概率不会是真的加密函数,结合前一小问的EPT Hook,合理猜测:出题人给这一函数挂了Hook魔改某一部分。先看静态逻辑:

image-20250413000713280

这里有个Key F5没显示出来:

image-20250413000811127

key = {0x89,0xFE,0x76,0xA0}

逻辑比较简单,两两TEA后跟密文比较,动调发现这一条指令变动:

mov eax,5464AD1Fh => mov eax,8771109Dh

确认这一个函数也被挂上了Hook,这种赋值类型的汇编还能通过读寄存器判断出来,其他的变化则是windbg不可见的。

现在有两个思路:1.根据函数序言的特征,在内存中搜索实际执行的函数。2.直接在EPT交付Hook函数地址时下断,获得函数地址

尝试第一种,在内存中扫描TEA函数序言部分,EPT中肯定有这么一段字节码:

s -[1] ffff0000`00000000 L?(ffffff00`00000000 - ffff0000`00000000) 48 8b c4 48 89 58 08 48 89 68 10 48 89 70 18 48 89 78 20 41 55 48 83 ec 50 33 c0 89 04 24

但是由于不知道具体的地址范围,这里搜的地址接近1T了,过于庞大直接导致Windbg卡死。

这里我想了下应该可以用一些方法放缩范围,或者写个脚本查询,不过还是很麻烦而且不够优雅。

考虑第二种方法:

image-20250413014253814

一个VT框架理应会通过VMCALL指令直接从Guest通知Host,发送Hook请求。(当然还有其他的通信手段,只不过我在IDA里翻到了vmcall所以比较肯定是这一种)

下断后翻一下地址:

rcx的地址与原地址相近,应该是一个模块,不用管

6293484e724eb6f39537f8a080a9046f_720

rdx的地址则存放着hook后的tea:

1f514076654682617728cf44f8e8ae44_720

这里我之前分析有个误区,应该是直接hook了一整页而非一个函数。

dump下来拿到hook后的tea:

image-20250413021801515

不难给出解密代码:

__int64 __fastcall TeaDecrypt(__int64 a1, __int64 a2, __int64 a3, unsigned int *a4)
{
  __int64 result; // rax
  int v5; // [rsp+0h] [rbp-58h]
  unsigned int v6; // [rsp+4h] [rbp-54h]
  unsigned int v7; // [rsp+8h] [rbp-50h]
  unsigned int v8; // [rsp+Ch] [rbp-4Ch]

  v5 = 0x9DDECF4C; 
  v8 = 0x3C;        
  v6 = *a4;         
  v7 = a4[1];       
  
  do
  {

    v7 = v7 
        - 0x7C77AF7C 
        - ((v5 + v6) ^ v6 ^ (*(_DWORD *)(a3 + 8) + 16 * v6) ^ (*(_DWORD *)(a3 + 12) + (v6 >> 5)) ^ 0x4321) 
        + 1493243931;
    
    v5 += 0x788EEF63;  // 恢复v5的值(加回原来的delta)
    
    v6 -= (v5 + *(_DWORD *)(a3 + 4LL * (v5 & 3))) ^ (v7 + ((v7 >> 5) ^ (16 * v7)));
    
    --v8;
  }
  while (v8 > 0);
  
  result = v6;
  *a4 = v6;
  a4[1] = v7;
  return result;
}

1.3最后的异或

本来以为没了...结果调半天没弄出来,感觉还是有东西没看到。又翻了一遍代码:

image-20250413023322214

因为比较前实在是没啥逻辑了,甚至怀疑了这个最后的校验函数也被Hook了(实际没有),因为被Hook的应该只有那一页。

最后感觉还是这个最不起眼的__readmsr在作妖,翻了一下Handler发现个这玩意:

这个handler恰好对应的就是readmsr

image-20250413024225565

密钥初值:

image-20250413024852312

说明最后一步还做了一个异或,代码抠出来:

void FinalXor(uint8_t* data, int len, const uint8_t* xor_key1, const uint8_t* xor_key2) {
    uint8_t xor_table[8];
    uint8_t* ptr = data;
    int blocks = len / 2;
    while (blocks--) {
        for (int v10 = 0; v10 < 8; v10++) {
            xor_table[v10] = v10 + len + xor_key1[v10 % 5] + xor_key2[v10 % 7];
        }

        for (int i = 0; i < 8; i++) {
            ptr[i] ^= xor_table[i];
        }
        ptr += 8;
    }
}

密文:

image-20250413113034445

完整EXP:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <Windows.h>
#define HIDWORD(x) ((uint32_t)(((uint64_t)(x) >> 32) & 0xFFFFFFFF))
#define _BYTE  uint8_t
#define _DWORD uint32_t



//异或加密中,取数时的EPT Hook Encrypt
uint8_t Encrypt(char a1, char a2)
{
    uint8_t result = 0;
    uint8_t xor_val = a1 ^ a2;
    uint8_t bit_count = 0;

    uint8_t tmp = xor_val;
    while (tmp) {
        bit_count += tmp & 1;
        tmp >>= 1;
    }

    uint8_t table[8] = { 0, 1, 2, 3, 4, 5, 6, 7 };
    uint64_t seed = bit_count;
    int n = 8;

    for (int i = 7; i > 0; i--) {
        seed = seed * 0x3F5713FCCC7C79AA + 0x3A7D9E5B36F498B2;
        int idx = ((uint32_t)(seed >> 32)) % n--;
        uint8_t tmp = table[i];
        table[i] = table[idx];
        table[idx] = tmp;
    }


    for (int i = 0; i < 8; i++) {
        uint8_t bit = (xor_val >> table[i]) & 1;
        result |= (bit << i);
    }

    return result;
}


// 变种TEA加密函数
__int64 __fastcall TeaEncrypt(uint32_t* data, const uint32_t* key) {
    uint32_t v0 = data[0];
    uint32_t v1 = data[1];
    uint32_t sum = 0;
    uint32_t rounds = 0;

    while (rounds < 0x3C) {  // 60轮
        v0 += (sum + key[sum & 3]) ^ (v1 + ((v1 >> 5) ^ (16 * v1)));
        sum -= 0x788EEF63;  
        v1 = v1
            + 0x7C77AF7C
            + ((sum + v0) ^ v0 ^ (key[2] + 16 * v0) ^ (key[3] + (v0 >> 5)) ^ 0x4321)
            - 1493243931;
        ++rounds;
    }

    data[0] = v0;
    data[1] = v1;
    return v0;
}

// 变种TEA解密函数
__int64 __fastcall TeaDecrypt(uint32_t* data, const uint32_t* key) {
    uint32_t v0 = data[0];
    uint32_t v1 = data[1];
    uint32_t rounds = 0x3C;     // 60轮
    uint32_t sum = 0;
    for (int i = 0; i < 60; i++) {
        sum -= 0x788EEF63;
    }

    while (rounds > 0) {
        v1 = v1
            - 0x7C77AF7C
            - ((sum + v0) ^ v0 ^ (key[2] + 16 * v0) ^ (key[3] + (v0 >> 5)) ^ 0x4321)
            + 1493243931;

        sum += 0x788EEF63;

        v0 -= (sum + key[sum & 3]) ^ (v1 + ((v1 >> 5) ^ (16 * v1)));

        --rounds;
    }

    data[0] = v0;
    data[1] = v1;
    return v0;
}
//最后一轮,readmsr指令时handler做的异或
void FinalXor(uint8_t* data, int len, const uint8_t* xor_key1, const uint8_t* xor_key2) {


    uint8_t xor_table[8];
    uint8_t* ptr = data;
    int blocks = len / 2;
    while (blocks--) {
        for (int v10 = 0; v10 < 8; v10++) {
            xor_table[v10] = v10 + len + xor_key1[v10 % 5] + xor_key2[v10 % 7];
        }

        for (int i = 0; i < 8; i++) {
            ptr[i] ^= xor_table[i];
        }
        ptr += 8;
    }
}

int main() {
    //密文
    uint32_t encrypted[] = {
    0x199354C3, 0xB1FD7BE6, 0x73205B55, 0xDE5C4D43, 0xA4EF9954, 0xA97651D4,
    0xEFBA6B6A, 0xC6E221DE, 0x8FA342FE,0x4C1C63BE, 0xD0AEE4C6, 0xC6F63D4B, 
    0x3807EBDA, 0x2ADF5814, 0x7A83C42E,0x9E348D33, 0x782779E4, 0xC4A55FC0,
    0x0DC0B64D0, 0x7EE36C5D, 0xE43BE42C, 0xD5E405CA, 0xB772C9A7, 0x30CDC7,
    0x2F09B31C, 0xFA839DD7, 0x57547B88,0x0F754B5AE, 0x231F7B75, 0x13160770, 
    0x6EB71579, 0xFA28BBD, 0x6103E890,0xEF604E1D
    };

    uint32_t key[] = { 0x89, 0xFE, 0x76, 0xA0 };//TeaKey
    uint8_t xor_key1[] = { 0x89, 0xAD, 0xCD, 0x12, 0x56 };//MSR Xor
    uint8_t xor_key2[] = { 0x58, 0x69, 0x58, 0xEF, 0xB3, 0x46, 0x23 };
    FinalXor((uint8_t*)encrypted, 34,xor_key1, xor_key2);
    for (int i = 0; i < 34; i += 2) {
        TeaDecrypt(&encrypted[i], key);
    }

    uint8_t xor_key3[6] = { 0xD2, 0x5F, 0x26, 0x11, 0x67, 0x06 };// 第三问计算出来的Key
    char ans[35]{};
    for (int i = 0; i < 34; i++) {
        for (int j = 32; j < 128; j++) {
            uint8_t res = Encrypt(j, i) ^ xor_key3[i % 6];
            if (res == encrypted[i]) {
                ans[i] = j;
                break;
            }
        }
    }
    ans[34] = '\0';
    printf("%s\n", ans);

}
// ACE_C0n9raTs0nPA55TheZ02S9AmeScTf#

成功解出Flag,输入进注册表中验证是正确的。

串联逻辑说明

最后按照题目要求串讲一遍Flag的加密逻辑:

1.取出注册表中明文后,取数时挂Hook触发一个单字节不可逆的加密,之后与密钥循环异或

2.进入一个魔改的Tea函数(也被挂上了Hook)

3.通过__readmsr指令进行最后一层异或加密

4.与预计算好的密文两两相比(因为Tea是两个两个加密的)

正确解出flag(1分)

在分析中已经给出EXP,解出FlagACE_C0n9raTs0nPA55TheZ02S9AmeScTf#

该题目使用了一种外挂常用的隐藏手段,请给出多种检测方法,要求demo程序能在题目驱动运行的环境下进行精确检测,方法越多分数越高(3分)

在做完上面的题目以后,已经明白赛题使用的外挂隐藏手段是VT,VT能够给函数挂上“无痕”Hook,若用于Hook系统函数,比如RtlWalkChain、MmIsAddressValid,能够在规避PG的同时规避反作弊的检测。

本题可以理解为,给出多种检测当前是否处于虚拟化环境的手段;更深一层地说,给出检测EPT Hook的手段。

注:由于调试需要,本次的检测环境都是在虚拟机下进行,同时赛题驱动也运行在虚拟机环境中,这导致了嵌套虚拟化。因此所给代码实质上是在检测嵌套虚拟化环境。对于利用时序检测的方式,若运行在物理机上,可能需要适当调小阈值。不过方法是通用的。

方法一:CPUID检测(时序检测)(3环)

原理:CPUID是一条特权指令,在虚拟机中,其会触发VM-EXIT,由Hypervisor接管执行,这样VM EXIT以及重新进入VM会引入额外的延迟。因此可以考虑故意去多次执行CPUID取执行时间,转化成一个指数来判断当前运行环境。在Win10 22H2上得到如下的效果:

void CheckByCPUID() {
    const auto originalPriority = SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);

    // 计算1000ms的CPU周期数
    const auto cyclesStart = __rdtsc();
    Sleep(1000);
    const auto cyclesPerSecond = __rdtsc() - cyclesStart;

    // 测量CPUID指令时间
    uint64_t cpuidTotalCycles = 0;
    for (size_t i = 0; i < 0x6ACE; i++) {
        const auto beforeCpuid = __rdtsc();

        int cpuidResult[4] = {};
        __cpuid(cpuidResult, 0);

        cpuidTotalCycles += __rdtsc() - beforeCpuid;
    }

    // 算出一个结果指数,其代数大小表明CPUID指令运行了多久
    double vtDetectionScore = 10000000.0 * cpuidTotalCycles / cyclesPerSecond / 0x65;

    // 还原线程优先级
    SetThreadPriority(GetCurrentThread(), originalPriority);

    // 输出结果
    printf("CPUID执行时间指数 :%.2f\n", vtDetectionScore);
    if (vtDetectionScore < 200.0) {
        printf("运行在物理机上\n");
        return;
    }
    else if (vtDetectionScore > 5000.0) {
        printf("检测到嵌套虚拟化环境\n");
        return;
    }
    printf("运行在虚拟机环境上\n");
}

在开启赛题驱动前后分别打开demo程序,看看效果:

bf4ef1b8895e249893c76818e2048a47

对于不同的环境这个时间指数差异会较大,不过可以肯定的是,开启了虚拟化后执行指数在未开启的十倍以上。

方法二:同步线程计数(3环)

在虚拟化环境中,CPUID指令的执行时间会远大于普通指令的执行时间,可以考虑开两个同步线程,执行相同次数后测量时间比率,以此为基准判断当前环境。

void CheckByThread() {
    if (std::thread::hardware_concurrency() < 2) {
        throw std::runtime_error("至少需要2个CPU核心");
    }

    // 执行足够次数以平均开销
    constexpr auto executionCount = 100000;

    // 测量普通指令执行时间
    const auto normalTime = MeasureExecutionTime([&]() {
        ExecuteNormalInstructions(executionCount);
        });

    // 测量CPUID指令执行时间
    const auto cpuidTime = MeasureExecutionTime([&]() {
        ExecuteCpuidInstructions(executionCount);
        });

    // 计算时间比率
    double ratio = static_cast<double>(cpuidTime) / normalTime;
    printf("比率:%.2f", ratio);
    if (ratio > 1.0) {
        printf("检测到嵌套虚拟化环境\n");
        return;
    }
    printf("运行在物理机上\n");
}

这个方法使用了比值处理,对于不同机器的移植性优于第一种。效果:

6922c52b2c608284bc58bee55be6765a

在物理机上,这个比率的数量级是
$$
10^{-3}
$$

方法三:主动进入VMX(0环)

Intel规定,每个处理器核心只能同时进入一次VMX root mode,也就是说,在赛题驱动正常运行时,其他驱动无法进入VMX。基于这一点,可以设计一个驱动程序,主动调用VMXON尝试进入VMX,若VMXON成功,则代表当前不在VT环境下;若失败,则可以判断已经有其他驱动开启了VT。

NTSTATUS TryVmxOn()
{
    // 检查 CPUID 是否支持 VMX
    int cpuInfo[4];
    __cpuid(cpuInfo, 1);
    if (!(cpuInfo[2] & (1 << 5))) {
        DbgPrintEx(77,0,"VMX not supported by CPU.\n");
        return STATUS_NOT_SUPPORTED;
    }

    // 设置 CR4.VMXE = 1
    __writecr4(__readcr4() | (1 << 13));
    
    // 分配一页物理对齐内存用于 VMXON 区域
    PVOID vmxonRegion = ExAllocatePoolWithTag(NonPagedPoolNx, VMXON_REGION_SIZE, 'vmxT');
    if (!vmxonRegion) {
        DbgPrintEx(77, 0, "Failed to allocate VMXON region.\n");
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    RtlZeroMemory(vmxonRegion, VMXON_REGION_SIZE);

    // IA32_VMX_BASIC MSR [0]–[30] 是 revision ID,需要放到 VMXON 区域前4字节
    UINT64 vmxBasic = __readmsr(0x480); // IA32_VMX_BASIC
    *(ULONG*)vmxonRegion = (ULONG)(vmxBasic & 0xFFFFFFFF);

    // 获取物理地址
    PHYSICAL_ADDRESS vmxonPhys = MmGetPhysicalAddress(vmxonRegion);

    // 尝试执行 VMXON
    ULONG64 result = 0;
    __try {
        __vmx_on(&vmxonPhys.QuadPart);
        DbgPrintEx(77, 0, "VMXON succeeded.\n");
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DbgPrintEx(77, 0, "VMXON failed with exception.\n");
        result = GetExceptionCode();
    }

    // 清理
    ExFreePoolWithTag(vmxonRegion, 'vmxT');
    return result ? STATUS_UNSUCCESSFUL : STATUS_SUCCESS;
}

效果如下:当赛题驱动运行时,返回Failed;卸载赛题驱动后,调用VMXON成功

e732ad13e2a6a5b142a222ac399cb2b6

方法四:监控CR4.VMXE(0环)

驱动程序在调用VMXON进入VT-x前,首先要设置CR4的VMXE位为1,因此,可以持续监控这一位,一旦发现VMXE位被设为1,则可以判断有驱动程序正在开启VT。

VOID CheckCR4VMXE()
{

    ULONG64 cr4 = __readcr4();  // 读取CR4寄存器

    if (cr4 & (1ULL << 13)) {
        DbgPrintEx(77,0,"有驱动程序正在开启VT.\n");
    }

}

// 线程函数,每秒执行一次 CheckCR4VMXE
VOID CheckVmxThread(PVOID StartContext)
{
    UNREFERENCED_PARAMETER(StartContext);

    LARGE_INTEGER interval;
    interval.QuadPart = SLEEP_INTERVAL; // 1秒

    while (g_ShouldRun)
    {
        CheckCR4VMXE();
        KeDelayExecutionThread(KernelMode, FALSE, &interval);
    }

    PsTerminateSystemThread(STATUS_SUCCESS);
}

先加载监控驱动,然后加载赛题驱动,赛题驱动在加载过程中将VMXE位置为1,会被监控到,效果:

f51653baa9e9960b7c9794c36aa5355c

方法五:invd指令触发VM-Exit(0环)

invd指令触发时,会引起VM-Exit进入Host处理,在规范的虚拟机比如Vmware中,正常模拟了这个指令就不会报错。但一些虚拟化框架如果没有处理这个指令,就会抛出异常(比如赛题)。因此,可以设计一个驱动程序,主动触发VM-Exit,若抛出异常,则认为在VT环境下。

#include <ntddk.h>
#include <intrin.h>
// 声明外部裸函数(汇编实现)
extern void Test_invd();


void DetectInstruction(const char* name, void (*func)())
{
    __try {
        func();
        DbgPrintEx(77,0,"[+] %-8s 执行成功,未引发异常。\n", name);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        NTSTATUS code = GetExceptionCode();
        DbgPrintEx(77, 0, "[-] %-8s 引发异常:0x%08X\n", name, code);
        DbgPrintEx(77, 0, "检测到虚拟化环境\n");
    }
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrintEx(77, 0, "驱动卸载完成。\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
    DriverObject->DriverUnload = DriverUnload;


    DetectInstruction("INVD", Test_invd);


    return STATUS_SUCCESS;
}
Test_invd PROC
    invd
    ret
Test_invd ENDP

赛题未加载前,加载本驱动,invd成功执行;赛题驱动加载后,由于赛题没有模拟invd指令,所以会抛出异常,此时则检测到虚拟化环境:

73b1b754feb58ea1c6c207260eb3022f

方法六:未定义的vmcall触发VM-Exit(0环)

类似方法五,传一个空调用vmcall指令,vmware能正常处理,但赛题不行。

#include <ntddk.h>

// 汇编函数声明
extern void Test_invd();
extern void Test_xsetbv();
extern void Test_getsec();
extern void Test_vmcall();
extern void Test_vmread();
extern void Test_vmwrite();
extern void Test_vmlaunch();
extern void Test_rdmsr();
extern void Test_wrmsr();

typedef struct {
    const char* Name;
    void (*Func)();
} InstructionTest;

void DetectInstruction(const char* name, void (*func)())
{
    __try {
        func();
        DbgPrintEx(77,0,"[+] %-10s 执行成功,未引发异常。\n", name);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        NTSTATUS code = GetExceptionCode();
        DbgPrintEx(77,0,"[-] %-10s 引发异常:0x%08X\n", name, code);
    }
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    DbgPrintEx(77,0,"驱动卸载完成。\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);
    DriverObject->DriverUnload = DriverUnload;


    InstructionTest tests[] = {
        {"INVD",      Test_invd},
        {"VMCALL",    Test_vmcall},

    };

    for (int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) {
        DetectInstruction(tests[i].Name, tests[i].Func);
    }



    return STATUS_SUCCESS;
}

Test_vmcall PROC
    vmcall
    ret
Test_vmcall ENDP

效果:

b8ed52e673f5b3fe76768c4022ee8e60

EPT Hook检测

这里给出一种与检测VT方法一相同原理的EPT Hook检测手段,即时序检测。

因为EPT Hook页在执行时需要通过VM-Exit进入进出,所以执行时间必然大;而由于只读页面时不会触发VM-Exit,所以读页面时的耗时应该是正常的。

同时又知道,赛题的Hook点是自身Tea函数,那么可以设计一个驱动函数,主动去读和执行Tea函数,若执行时间远大于读取时间,则判断为被EPT Hook了。当然,如果去读和执行正常函数,会发现相差无几,可以判断没有被Hook。

核心代码如下:

ULONGLONG MeasureExecuteTime(PVOID Address) {
    g_TeaEncrypt = (fpTeaEncrypt)Address;
    ULONGLONG t1 = __rdtsc();
    DbgPrintEx(77, 0, "t1:%llu\n", t1);
    __try {
        // 随便传入参数,执行一次来获取执行时间
        unsigned int data[2] = { 0x12345678, 0x9ABCDEF0 }; // 明文
        unsigned int key[5] = { 0xA1B2C3D4, 0xB2C3D4E5, 0xC3D4E5F6, 0xD4E5F607, 0 }; // 密钥和附加值
        ULONGLONG res = g_TeaEncrypt(data,(__int64)key);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        return 0; 
    }

    ULONGLONG t2 = __rdtsc();
    DbgPrintEx(77, 0, "t2:%llu\n", t2);
    // 返回执行时间差
    return t2 - t1;
}

BOOLEAN GetModuleBaseFromDriver(PDRIVER_OBJECT drv, PCWSTR targetName, PVOID* baseOut, SIZE_T* sizeOut)
{
    PLDR_DATA_TABLE_ENTRY ldr = (PLDR_DATA_TABLE_ENTRY)drv->DriverSection;
    PLIST_ENTRY head = &ldr->InLoadOrderLinks;
    PLIST_ENTRY current = head;

    do {
        ldr = CONTAINING_RECORD(current, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

        if (ldr->BaseDllName.Buffer && _wcsicmp(ldr->BaseDllName.Buffer, targetName) == 0)
        {
            *baseOut = ldr->DllBase;
            *sizeOut = ldr->SizeOfImage;
            return TRUE;
        }

        current = current->Flink;
    } while (current != head);

    return FALSE;
}

VOID DetectEPT_ReadLatency(PVOID ModuleBase, SIZE_T ModuleSize)
{

    ULONGLONG readtime = MeasureReadPageTime((ULONGLONG)ModuleBase + TEA_OFFSET);
    ULONGLONG executetime = MeasureExecuteTime((ULONGLONG)ModuleBase + TEA_OFFSET);
    DbgPrintEx(77, 0, "[EPT DETECT]readtime:%d,executetime:%d\n",readtime,executetime);
    //经验阈值
    if (executetime > readtime * 10) {
        DbgPrintEx(77, 0, "[EPT DETECT]%p EPT Hook detected!\n",(ULONGLONG)ModuleBase + TEA_OFFSET);
    }
}

效果如下:

bb1fe36fa0957f9606a7de0cac56a5b4

另外,还有一种检测方式是,给模块中每页的unuse部分(即0xCC填充部分)修改为C3,即ret。如果没有EPT Hook的话,执行新的指令不会产生异常;如果某一页产生了异常,则说明实际执行的并非我们所修改的那一页,即这一页被挂上了EPT Hook,从而检测到。因比赛时间有限,暂时没有完成代码。

posted @ 2025-04-14 13:31  凉猹  阅读(659)  评论(1)    收藏  举报