这里浅谈一下 VMP、Safengine 和 Themida 的反虚拟机的分析过程,关于反调试下面会说下值得注意的地方。

VMP 的反调试请参考 https://bbs.pediy.com/thread-226455.htm,Safengine 和 Themida 的反调试与 VMP 差不多。

分析的 VMP 版本为 3.0.9,Themida 版本为 2.4.6.30,Safengine 不知道是什么版本了,程序是 32bit 。

关于 64bit 程序,反调试和反虚拟机都更简单。其中反调试只包含 IsDebuggerPresent、NtGlobalFlag、CheckRemoteDebuggerPresent。不过需注意的是程序在 OEP 后,IsDebuggerPresent 之前保存 NtGlobalFlag 标志,在 IsDebuggerPresent 之后同时检测 BeingDebugged 和 NtGlobalFlag 标志,最后调用 CheckRemoteDebuggerPresent。反虚拟机则只有注册表检测,没有 in 指令检测,因此只需修改注册表,就可在虚拟机运行。

# 反调试

# VMP 反调试

VMP 的某些版本有 tls 保护,该 tls 用于检测调试器,具体方法不明。目前排除了 tls 对软件断点、硬件断点、SEH、NtGlobalFlag、BeingDebugged、Heap 结构的 ForceFlags 和 Flags 检测,包括堆中的调试记号(0xbababa,badf00d 等)。虽然不明检测方法,但要过掉还是挺简单的,直接跳过该 tls。跳过的步骤是用调试器启动程序,在加载 ntdll 后,把 TLS 的 DataDirectory 清零,让系统读不到 tls callback,当走到 OEP 时,再还原。当然,如果程序没有文件完整性检测,可以直接修改文件中 TLS 的 DataDirectory 项。

# 反虚拟机

# VMP 反虚拟机

从结果来说,VMP 反虚拟机只使用了一个方法,特殊指令,该特殊指令是 cpuid。检测原理是赋 eax 为 1,执行 cpuid 后,如果 ecx 的 31st 位为 0,表示真机,否则为虚拟机。

从分析过程来说,主要分为 3 步。 一是分析虚拟机框架,二是逐步接近特殊指令,三是定位特殊指令,如下图:

img

  • 虚拟机框架:

    根据初步 VMP 的分析,该 VMP 无 dispatcher,全程是用 push edi,ret 来调整执行流程,其中的 edi 由 mov …,dword ptr [esi] 转换而来。VMP 的单元步骤为一对,如下:

c
...
lea esi, [esi-4]
mov eax, dword ptr [esi]
decode eax
add edi, eax
jmp edi      // 跳转到 handler
...
dec esi
movzx eax, byte ptr [esi]
handler      // 具体执行内容
...

其中值得注意的是字节码的表示,esi 和 edx 轮换表示字节码的当前获取地址,这些地址是一段一段的,且可以重复利用,如下图:

img

  • 逐步接近特殊指令:

    找到存放字节码段首地址的栈地址。因为字节码段是乱序且繁多的,手动跟踪非常缓慢。之后注意到在 ebp 的一个相对偏移处存有 esi 或 edx 的字节码段首地址,且 ebp 相对一段时间是固定不变的,因此可在这个相对偏移下写断点。果然,这个断点被触发了一千多次,由于堆栈的成长,ebp 会超过 VMP 自己的虚拟栈,因此需要调整 ebp,这时存放字节码段首地址的相对偏移会稍有变化,因此需要重复几次找这个 “相对偏移” 的步骤。最后越来越接近特殊指令。

    注:这部分本应更具体一点的,无奈是以前的文章,大家将就看一下吧,感觉下大概。

  • 定位特殊指令:

    通过程序检测到虚拟机的附近代码寻找特殊指令。虽然第二步的结果很接近特殊指令了,但在 VMP 中,这个距离还是非常长的,因此需要另找方法。具体方法是定位程序检测到虚拟机的错误点,然后一步一步逼近,最后用 x32dbg 的 trace 功能扫描代码,找到特殊指令。其中,定位错误点根据 “Sorry, this application cannot run under a Virtual Machine.” 提示语句寻找,因为这段文字被存在栈中的一个固定位置,所以只需在此处下断点就能判断何时触发的错误点,然后向上回溯,最终找到特殊指令。

  • 快速定位:

    以上是定位的过程,这里提供一个快速的定位方法。由于程序对 VM 化的代码不会再做加密,因此可以在 vm 段直接内存搜索,找到 cpuid,注意 VMP 一般会用一次或两次该指令来检测。

  • 过掉 cpuid:

    一般执行 cpuid 时,ecx 的 31st 位为 0,所以 cpuid 直接用两个 nop 替换即可。

# Safengine 反虚拟机

使用 in 指令和 RegQueryValueExA 检查 SystemManufacturer (在注册表里),in 指令的原理请参考 https://bbs.pediy.com/thread-225735.htm。这里说下如何定位 in 指令和过掉。

in 指令在真机中会产生 0xC0000096 异常,而在虚拟机中不会产生异常。因此定位 in 指令需要在真机的调试器中运行,但前提是你要知道它反虚拟机使用的是 in 指令,这个就需要各种尝试了(因为 safengine 首先是反调试,然后再是反虚拟机,所以分析反调试时很轻松就能发现 in 指令)。

定位到后,就是过掉了。为了说明过掉方法的原理,这里需要再讲下 in 指令。in 在真机中产生异常,执行程序的 SEH,在 SEH 中会将 eip 重新赋值。该过程就像一个跳转,在 in 指令的地址处有一条 jmp 指令的感觉。而在虚拟机中,不会产生异常,那么就会进入检测到虚拟机的分支。

稍微了解了 in 指令后,现在说明两个过掉方法。一是换成 int3,二是重设 eip。关于一,in 和 int3 指令只占用一个字节,且 int3 也可产生异常,因此非常合适。但不排除程序的 SEH 会检测该异常的 ExceptionCode,如果是这样,那就用第二种方法。关于二,调试程序,走到 in 指令后(下硬件断点),手动给 eip 赋值,该值是 SEH 里指定的值。

# Themida 反虚拟机

# 检测方法
  • 注册表检测

    两次使用 in 指令和 RegQueryValueExA 检查注册表内容。其中注册表内容包括以下三点:

    • HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\VideoBiosVersion (一般虚拟机中没有,不过没有该项也不会导致虚拟机被检测到)

    • HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\ SystemBiosVersion

    • HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class{4d36e968-e325-11ce-bfc1-08002be10318}\0000\DriverDesc

  • in 指令检测

    两次 in 指令的使用中 ecx 分别为 0x14 和 0xA,0x14 是检查返回的 eax,0xA 是检查返回的 ebx。其中:

    • ecx 为 0x14 时,VMware® Workstation 14 Pro 返回的 eax 为 0x800,在真机中会跳到 SEH,将 eax 赋值为 0
    • ecx 为 0xA 时,虚拟机中的 ebx 为 'VMXh',在真机中进入 SEH,并在 SEH 中直接修改 eip 跳转
# in 指令定位过程
  • 检测相关函数

    因为 Themida 没有用 PSAPI.dll、IPHLPAPI.dll、shlwapi.dll,所以能用来反虚拟机的函数比较有限,除了与注册表相关的函数,剩下的是检查固件信息的函数,不过设断点后都没有触发,断点如下:

    ba e1 KERNELBASE!GetSystemFirmwareTable
    ba e1 KERNELBASE!EnumSystemFirmwareTables

    之后我用 APIMonitor 监控 Themida 对 API 的调用,除了注册表相关函数,确实没有其他用于检测虚拟机的函数了。因此我猜测剩下的虚拟机检测方法不是用的函数,而是特殊指令。

  • 搜索 cpuid 指令

    让程序运行,在 Themida 的可执行段搜索 cpuid 指令,在可能的指令处下硬件断点,结果没有命中。因为 Themida 有 SMC,所以在代码解密之后搜索 cpuid,尝试下断点,但仍然没有命中。

  • 跟踪错误提示字符串并尝试分析多线程

    跟踪 MessageBoxExA 引用的提示字符串时,发现多线程 SMC。之后分析多线程时,无法掌控主线程的执行进度。Themida 在检测到虚拟机后,会用 MessageBoxExA 弹框,于是我开始跟踪 “Sorry, this application cannot run under a Virtual Machine” 字符串,发现该字符串是多次解密后产生的结果。之后我回溯该字符串的解密过程,但由于多线程之间频繁的相互切换,我无法持续单步跟踪主线程,导致当线程切换后,主线程已经执行到不可预估的地方了。

    关于跟踪主线程,还有两点需要解释:

    • 第一是无法保持一直跟踪主线程,因为主线程执行到不同代码片段时会进入死循环,等待其他的某个线程修改对应的死循环代码,因此仅仅是挂起除主线程的所有进程是无法继续分析的,必须在某一点唤醒其他解密线程。

    • 第二是线程切换时不能预估主线程的执行进度。当主线程需要执行待解密的代码时,会首先标记一个事件句柄(即告诉其他线程,你们可以执行了),然后调用 Sleep (0),释放掉当前分配给自己的执行时间,以让其他线程被调度。而其他线程有部分是调用 Sleep,正准备被唤醒,有部分是用 WaitForSingleObject 等待某事件句柄,该事件句柄正是主线程标记的事件句柄。

      在这个过程中,如果对主线程 Sleep (0) 的下一条汇编指令下断点,结果就是断点会被命中,但往往被命中的线程不是主线程,而是同样调用 Sleep、等待被唤醒的其他线程。因此,这里不应该在主线程将被唤醒的汇编代码处下断点,即使断点被命中,也很可能是其他线程被命中。因为除主线程,还有大概 25 个线程等待被唤醒,其中调用 Sleep 等待被唤醒的数量可能有 7、8 个。

  • 尝试分析虚拟机框架

    分析虚拟机框架,尝试定位提示字符串产生的过程,找到反虚拟机的特殊指令。因为无法精确跟踪主线程,所以我想分析 Themida 的虚拟机框架,进一步分析细节,找到提示字符串的解密以及产生过程。在看雪搜索相关帖子后,发现分析成本很大,无法快速达到定位特殊指令的目的,因此需要另找思路。

  • 尝试根据虚拟机配置来定位特殊指令

    Themida 的虚拟机检测可以通过设置 VMware 选项过掉 (不过我尝试没成功),比如:

    disable_acceleration = "TRUE"
    monitor_control.restrict_backdoor = "TRUE"

    于是我想弄懂这两句的原理,以此定位特殊指令。通过一段时间的搜索,发现这些语句可能与虚拟机解释执行指令有关,不会与某条特定的汇编指令产生关联(因为找不到官方文档的说明,无法确定该猜想)。因此通过虚拟机配置寻找特殊指令大概是不可行的。

  • 与真机对比并锁定关键条件跳转

    对比真机和虚拟机的执行情况,找到关键代码段;对比这两份代码段,发现判断虚拟机的标志。

    单独分析虚拟机的情况无法找到特殊指令,那在真机中执行相同步骤,以此来寻找异同点,以下为两个关键点:

    • 在真机和虚拟机中,“Sorry, this application cannot run under a Virtual Machine” 字符串都会被解密。在虚拟机的分析中,我设定了一个前提,即解密该字符串意味着 Themida 发现了虚拟机。

      但真机的这一现象,瞬间颠覆了我的想法,看来准备工作没做充分。我开始在虚拟机和真机中交替分析程序,找到了两种环境下程序的分叉点,从该分叉点继续执行大概 23000 条汇编指令,虚拟机会执行到 MessageBoxExA 并报告发现虚拟机,而真机则不会报告。

    • 对比代码段,发现关键条件跳转。使用 x32dbg 的 trace 功能,把两种环境下从分叉点执行的 23000 行代码 dump 下来,然后用 BCompare 进行文本比较,发现大概在 1500 行后,程序判断 eflag 的 ZF 标志,ZF 为 1 对应真机,ZF 为 0 对应虚拟机。从 eflag 的比较处开始回溯,发现一句关键的代码

      cmp [addr], 0

    addr 指向的地址处保存了一个标志,该标志是用来存储某个结果的。如果是虚拟机,该标志为 1,如果是真机,则为 0。

    之后继续回溯,找到了该标志是在哪片代码被设置的。这片代码只是在设置标志,无法得知为何会设置该标志。

    由于 Themida 的跳转很多,不清楚程序是从哪跳转到这片代码的,于是再使用 x32dbg 的 trace 功能,发现在程序调用 GetNativeSystemInfo 和 GetVersion 后不久(大概 200,300 行),就跳到了这片代码,这时已经看到了 in 指令,即成功定位到 in 指令。

  • 两次 in 指令检测

    • 第一次检测

      c
      in eax,dx  //ecx 为 0x14,用于获取 VMware 中 VX 端口的 memory size
      pop dword ptr fs:[0]  // 两种环境下都会达到该指令
      push 3D193AC9
      mov dword ptr ss:[esp],eax  // 将结果赋到栈中,在真机中会进入 SEH,把 eax 设为 0,在虚拟机中返回的 eax 为 0x800。
      xor eax,dword ptr ss:[esp]
      xor dword ptr ss:[esp],eax
      xor eax,dword ptr ss:[esp]
      pop esp
      cmp eax,0  // 若为真机,eax 为 0,若为虚拟机,eax 大于 0
      jbe 5B775D
      cmp dword ptr ss:[ebp+17BA1689],1  //[ebp+17BA1689] 的值在两种环境下均为 1
      jne 5B775D
      mov dword ptr ss:[ebp+17BA2DBA],1  // 只有虚拟机会到达此处,设置标志位
      ...
      5B775D xxxx
    • 第二次检测:

      c
      in eax,dx  //ecx 为 0xA,用于获取 VMware 版本
      cmp ebx,564D5868  // 虚拟机会执行到此处,返回的 ebx 为 0x564D5868
      jne 5B4BE2    
      mov dword ptr ss:[ebp+17BA2DBA],1  // 只有虚拟机会到达此处,设置标志位
      (5B4BE2) pop dword ptr fs:[0]  // 真机会从 in 指令转到 SEH,最后修改 eip 跳到这里
      add esp,4
      cmp dword ptr ss:[ebp+17BA2DBA],0
      jne 5B3EA2  // 虚拟机中,跳转到 5B3EA2,执行虚拟机的分支,否则继续执行

    最后,in 指令的快速定位和 Safengine 一样,在真机中跟踪异常,直到发现 in eax, dx 产生的异常。之前我在真机中没有定位到 in 指令的原因是 0xC0000096 的异常太多了,大多数是由 Themida 的 sti 指令产生的,于是当时我就偷懒没有挨个查看产生异常的指令。

# Themida 反虚拟机需注意的点
  • 真机和虚拟机中都会解密 “Sorry, this application cannot run under a Virtual Machine” 这段文字,所以不能跟踪这段文字来找到 in 特殊指令。另外 Themida 使用 MessageBoxExA (不管程序是否使用 Unicode) 输出错误信息。

  • Themida 使用多线程 SMC,使得主线程边运行边执行被解密的代码,多线程之间通过事件对象同步。(解密代码一般由其他线程完成,不过主线程也会解密一部分代码)。从结果来说,in 指令是解密代码后才出现的,虽然程序运行后会解密出来,但因为 in eax,dx 只需一个字节,所以即使解密代码后,搜索时结果也会有很多无关信息,直接导致无法判定。

  • Themida 会使用很多 sti 指令,该指令会触发异常,且 ExceptionCode 和 in 指令的 ExceptionCode 是一样的 (因为 sti 指令较多,最初在真机中没有枚举所有的异常,所以没发现 in 指令。当最后发现是 in 指令时,才在真机中证实了 in 指令的使用)。因为 SEH 的不同,sti 的作用可能不一样,不过大多数都是将 eip 加 1。另外,要从 sti 跟踪到 SEH,比如断到 ntdll!KiUserExceptionDispatcher,需要下软件断点,不能下硬件断点。在刚进入 ntdll!KiUserExceptionDispatcher 时,调试寄存器的值为 0,等执行到程序的 SEH 时,调试寄存器的值才会恢复原来的值。

  • 分析主线程时,由于多线程之间会经常跳转,因此当跟踪主线程的关键代码时,要将除主线程的所有线程挂起。

更新于 阅读次数