INTENT2022--一道包含12个反调试反虚拟机操作的ctf题解

发布者:极安御信
发布于:2022-12-28 16:10

作者:selph

从一道Re题学习12种反调试反虚拟技术

题目:AntiDebuggingEmporium

来源:INTENT CTF 2022 Re

这个题目很有意思,里面出现了总共12个反调试反虚拟机的操作,本文内容分两部分,前部分是题解,后部分是这12个反调试反虚拟机手法分析

题解

程序逻辑分析

文件信息:是个64位的Windows控制台程序,VS2022编译的



主函数:



可以看到,逻辑很简单,

1.首先是调用一个函数等待一个对象执行完成

2.然后提示输入flag,

3.对一个数组的值进行判断,如果所有的值都是0,则处理输入字符串输出flag

这里去看看这个数组的值来自哪里:

通过交叉引用,发现这个数组在多个地方被赋值,基本上均在StartAddress这个函数里



经分析,这里的这个数组保存的就是检测虚拟机和调试器的情况,检测到了则会有值被赋值为1:




这个函数首先从当前文件的资源里读取了二进制数据,保存起来,然后进行了累计12个反虚拟机和反调试的函数,这部分内容我们在后文进行详细分析

随便点开一个:



可以看到,这里对一个数组的某个位置进行赋值了,这个赋值后面会用到

现在看一下StartAddress这个函数是在什么时候被调用的:



通过交叉引用可以看到,在TLS回调函数中调用,TLS回调函数会在主函数执行前先执行,这里的hHandle就是主函数里等待的对象

也就是说,这个程序会先进行反调试反虚拟机的操作,然后检测完之后,获取用户输入,进行处理

接下来看用户输入是被如何处理的:



这里首先是用资源二进制数据和一个数组的对应值进行相加(这个数组的值就是反调试函数里检测成功后赋值的)

然后进行校验,校验是通过异或进行的,这里用到一个哈希值,这个哈希值是对资源数据进行校验得到的,不去修改资源,则这里是固定值,所以可以直接动态调试得到这个值,然后异或用户输入,结果是资源数据计算后的值

逻辑理清楚了,现在要做的事情就是:

1.拿到资源二进制数据

2.拿到反调试数组

3.拿到哈希值

4.计算flag

拿到资源数据

直接从die中就能得到:


拿到反调试数组和哈希值

把程序在物理机上跑起来,让程序卡在scanf里,这个时候调试器和虚拟机的检测已经结束了,反调试数组应该也计算好了

这个时候直接调试器附加,查看数组的地址:14000DBE0




拿到64字节的数据

哈希值也是保存在全局变量里的,可以直接拿到地址000000014000DB88,去查看即可:


计算flag

该有的都有了,写代码生成flag即可:

// antidebuggingemporium.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
#include 


unsigned char RescourceData[68] = {
    0x72, 0x6C, 0xCA, 0x03, 0x75, 0x76, 0xE5, 0x00, 0x00, 0x43, 0x00, 0x00, 0x55, 0x16, 0xEA, 0x77,
    0x0B, 0x4C, 0xC1, 0x77, 0x48, 0x7D, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x00, 0x5B, 0xC1, 0x31,
    0x08, 0x43, 0xEE, 0x76, 0x55, 0x7D, 0xAF, 0x28, 0x64, 0x15, 0xF6, 0x75, 0x64, 0x00, 0x00, 0x00,
    0x64, 0x00, 0x00, 0x00, 0x52, 0x4C, 0xED, 0x71, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEA, 0x3F,
    0x46, 0x22, 0x3F, 0x46
};


unsigned char antiDbgNum[68] = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0x56, 0x00, 0xF9, 0x77, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA9, 0x2E, 0x08, 0x00, 0xAE, 0x28, 0x57, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xAA, 0x34,
    0x00, 0x16, 0xF9, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xAD, 0x27, 0x57, 0x13, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00
};

unsigned char flag[68] = { 0 };

unsigned int hashsum = 0x469E223B;

int main()
{
    for (int i = 0; i < 68; ++i)
    {
        RescourceData[i] += antiDbgNum[i];
    }

    for (int j = 0; j < 68; j +=4)
    {
        *(DWORD*)&flag[j] = (hashsum ^ *(DWORD*)&RescourceData[j]);// 00000000469E223B
    }
    printf("%s", flag);
}

// INTENT{1mag1n4t10n_1s_7h3_0nly_w3ap0n_1n_7h3_w4r_4gains7_r3al1ty}

反调试反虚拟机手法分析

这里依次分析那12个反调试反虚拟机的函数

0x0-反虚拟机-检测CPU核心数



这里通过GetSystemInfo API获取系统信息,这里判断系统的处理器核心数量,现在的用户电脑CPU核心都是4核往上的,一般只有在虚拟机里可能会遇到只分配了1核的情况

0x1-反调试器-检测PEB标志位1




看到这个+2偏移就很容易想到PEB里的BeingDebugged标志位

不过这里是使用ZwQueryInformationProcess获取PEB的:


0x2-反调试器-检测PEB标志位2



这个写的更直接,这个NtCurrentPeb()本质上就是从gs[0x60]处取值,得到的就是peb地址,这里检测的依然是BeingDebugged标志位

0x3-反调试器-检测PEB标志位3




这里使用了PEB的另一个标志位NtGlobalFlag,位置是偏移0xBC的地方,这里IDA的F5显示有问题,在反汇编里可以到是:add     rax, 0BCh

0x4-反调试器-检测PEB标志位4





通过API的方式检测PEB偏移2位置的BeingDebugged的值

0x5-反虚拟机-cpuid 1



cpuid指令,通过rax传递功能号,将返回值保存在eax,ebx,ecx,edx里

当功能号是1的时候,ecx的最高位表示当前是否在虚拟机里

0x6-反虚拟机-cpuid 0x40000000



当功能号是0x40000000时,rbx rcx rdx里返回的是一个cpu名称



然后接下来检测是否是常见的虚拟机的cpu名称

0x7-反虚拟机-cpuid 0



功能号是0时候,是另一种显示cpu相关信息的方法,依然是检测是否出现虚拟机常见字符

0x8-反调试器-rdtsc



通过rdtsc指令获取时间,当两次获取时间间隔过大,可以认为有调试器干扰了程序的正常执行

0x9-反调试器-窗口检测



检测是否存在调试器的窗口,如果存在,则认为有调试行为

0xA-反调试器-异常处理



这里使用SetUnhandledExceptionFilter API设置无法处理的异常的处理函数

通常情况下,当异常无法处理的时候会进入该函数去处理,但是有调试器存在,则会直接由调试器接管,不进入该函数

处理的内容是:



效果是跳过某些指令往下执行:



这里跳过了这个jmp,以至于下面的0x57能正常赋值到数组里

0xB-反虚拟机-设备检测



这里通过API:SetupDiGetClassDevsExW 获取设备集

然后通过API:SetupDiEnumDeviceInfo 枚举设备

使用API:SetupDiGetDeviceRegistryPropertyW 对每一个设备获取其属性



对获取到的信息,去判断是否包含这几个虚拟机相关的特征字符串,来判断是否位于虚拟机内部



声明:该文观点仅代表作者本人,转载请注明来自看雪