吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 7064|回复: 64
收起左侧

[原创] 一种新的辨认VMP3的方法

  [复制链接]
DNLINYJ 发表于 2023-12-16 16:52
本帖最后由 DNLINYJ 于 2023-12-16 17:08 编辑

一种新的辨认VMP3的方法

最近,VMP 3.5.1 的源码被完整泄露了 等不及看见国产虚拟机了

朋友在读源码的时候发现了 VMP 编译器水印的位置,我只是个调试菜鸡, 写这个文章来记录一下过程

思路 [来自朋友友情赞助]

cores/processors.cc 的第 2070 行,出现了一个函数 BaseFunction::AddWatermark, 内容如下

uint8_t *version_watermark = NULL;
uint8_t *owner_watermark = NULL;

void BaseFunction::AddWatermark(Watermark *watermark, int copy_count)
{
        Watermark secure_watermark(NULL);
        std::string value;
        uint8_t *internal_watermarks[] = {version_watermark, owner_watermark};

        for (size_t k = 0; k < 1 + _countof(internal_watermarks); k++) {
                if (k == 0) {
                        if (!watermark)
                                continue;
                } else {
                        uint8_t *ptr = internal_watermarks[k - 1];
                        if (!ptr)
                                continue;

                        uint32_t key = *reinterpret_cast<uint32_t *>(ptr);
                        uint16_t len = *reinterpret_cast<uint16_t *>(ptr + 4);
                        value.resize(len);
                        for (size_t i = 0; i < value.size(); i++) {
                                value[i] = ptr[6 + i] ^ static_cast<uint8_t>(_rotl32(key, (int)i) + i);
                        }
                        secure_watermark.set_value(value);
                        watermark = &secure_watermark;
                }

                for (int i = 0; i < copy_count; i++) {
                        watermark->Compile();
                        ICommand *command = AddCommand(Data(watermark->dump()));
                        command->include_option(roCreateNewBlock);
                }
        }
}

这段代码定义了一个名为 BaseFunction::AddWatermark 的方法,该方法将特定的“水印”添加到一个对象或数据中。以下是关于这个函数以及代码中每行的简单解释。

首先,定义了两个指向uint8_t 类型的全局指针,version_watermarkowner_watermark, 既版本水印和用户水印。

uint8_t *version_watermark = NULL;
uint8_t *owner_watermark = NULL;

BaseFunction::AddWatermark 函数接收两个参数: 一个 Watermark 对象指针和一个表示要复制的次数的整数。

void BaseFunction::AddWatermark(Watermark *watermark, int copy_count)

然后,创建了一个新的 Watermark 对象 secure_watermark。并创建了一个字符串 value 用于存放从内部 watermark 中提取和处理的数据。

Watermark secure_watermark(NULL);
std::string value;

接下来创建了一个内部 watermark 数组 internal_watermarks,它包含了之前定义的全局 watermark 指针。

uint8_t *internal_watermarks[] = {version_watermark, owner_watermark};

这里的代码穿过所有 watermark(包括输入的 watermark 和内部的 watermark)。对于每个 watermark,如果它是 NULL,那么就直接跳过当前迭代。

for (size_t k = 0; k < 1 + _countof(internal_watermarks); k++) {
        if (k == 0) {
                if (!watermark)
                        continue;
        } else {
                uint8_t *ptr = internal_watermarks[k - 1];
                if (!ptr)
                        continue;

取出内部 watermark,将它的数据作为 32 位和 16 位的块重新解释(也就是把原始的数据直接视作这些数值)。然后,它用一个特殊的算法(对每个字节都执行异或,然后添加到索引上),在 value 中生成新的字符。

        uint32_t key = *reinterpret_cast<uint32_t *>(ptr);
        uint16_t len = *reinterpret_cast<uint16_t *>(ptr + 4);
        value.resize(len);
        for (size_t i = 0; i < value.size(); i++) {
                value[i] = ptr[6 + i] ^ static_cast<uint8_t>(_rotl32(key, (int)i) + i);
        }
        secure_watermark.set_value(value);
        watermark = &secure_watermark;

最后,它为每个 watermark 创建 copy_count 个复制项,编译每个复制项,并将其添加到 ICommand 中,随后设置创建新块的数据选项标志。

        for (int i = 0; i < copy_count; i++) {
                watermark->Compile();
                ICommand *command = AddCommand(Data(watermark->dump()));
                command->include_option(roCreateNewBlock);
        }
}

这段代码的主要目的是为了处理和添加 watermark 到数据中。它首先处理传入的 watermark,然后处理定义的内部 watermark。处理的方式包括从中提取数据,以特定方式编辑这些数据,并设置 secure_watermark 或新增 watermark 的值。然后为这些 watermark 创建指定数量的复制项,并添加到命令中。

其中的 secure_watermark.set_value(value); 设定水印内容,之后使用这个实例进行水印的添加 (见上方),我们可以通过获取这个实例的 this 指针来获取水印内容

然而在后期测试的时候,BaseFunction::AddWatermark 实际上是被虚拟机保护了,但 Watermark 类并没有被虚拟机保护,其中的 Watermark::ReadFromIni 函数特征及其鲜明,这意味着我们可以通过 dump 下来的程序,抓住函数地址并直接读取水印内容,见下文

注:必须要用dump后的程序分析,因为VMP似乎只有在运行时才会解密字符串

// 在 IDA 中可以通过搜索dump后的程序内字符串,快速锁定这个函数,如 Name%d
// 快捷键: Shift + F12
void Watermark::ReadFromIni(IniFile &file, size_t id)
{
        id_ = id;
        name_ = file.ReadString("Watermarks", string_format("Name%d", id).c_str());
        value_ = file.ReadString("Watermarks", string_format("Value%d", id).c_str());
        use_count_ = file.ReadInt("Watermarks", string_format("UseCount%d", id).c_str());
        enabled_ = file.ReadBool("Watermarks", string_format("Enabled%d", id).c_str(), true);
        Compile(); // Watermark::Compile()
}
void Watermark::Compile()
{
        dump_.clear();
        mask_.clear();

        if (value_.size() == 0)
                return;

        for (size_t i = 0; i < x.size(); i++) {
                size_t p = i / 2;
                if (p >= dump_.size()) {
                        dump_.push_back(0);
                        mask_.push_back(0);
                }

                uint8_t m = 0xff;
                uint8_t b;
                char c = value_[i]; // 可以从这里直接抓取水印的内存地址,然后直接读取
                if ((c >= '0') && (c <= '9')) {
                        b = c - '0';
                } else if ((c >= 'A') && (c <= 'F')) {
                        b = c - 'A' + 0x0a;
                } else if ((c >= 'a') && (c <= 'f')) {
                        b = c - 'a' + 0x0a;
                } else {
                        m = 0;
                        b = rand();
                }

                if ((i & 1) == 0) {
                        dump_[p] = (dump_[p] & 0x0f) | (b << 4);
                        mask_[p] = (mask_[p] & 0x0f) | (m << 4);
                } else {
                        dump_[p] = (dump_[p] & 0xf0) | (b & 0x0f);
                        mask_[p] = (mask_[p] & 0xf0) | (m & 0x0f);
                }
        }
}

实际调试

这里就是我干活的地方了(

掏出带有反反调试器的x64dbg,直接 逆向,启动!()

image.png

使用自带的 Scylla 把 VMP 主程序 dump 出来

image-1.png

在 IDA 内搜索特征字符串,找到 Watermark::Compile() 函数

image-2.png

image-3.png

解释后的伪代码 Watermark::Compile() 函数如下

char __fastcall Watermark::Compile(_QWORD *this)
{
  _QWORD *mask_; // rdi
  __int64 size_value_; // rax (value_.size())
  unsigned __int64 i; // rsi (iteration variable)
  unsigned __int64 p; // r15 (i / 2, index for dump_ and mask_)
  unsigned __int64 new_size; // rax (used during vector expansion)
  unsigned __int64 dump_condition; // rcx (condition for growth check)
  char *value_pointer; // r14
  _BYTE *dump_bytes; // rdx
  _BYTE *mask_bytes; // rax
  unsigned __int64 mask_new_size; // rax (used during vector expansion)
  char *mask_pointer; // r14
  _BYTE *mask_data; // rdx
  _BYTE *dump_expansion; // rax
  char mask_byte; // bp (temporary bit mask)
  _QWORD *value_data; // rax (pointer to value_ data)
  char c_value; // al (current character in value_)
  char hex_value; // al (hexadecimal representation for current character)
  __int64 dump_offset; // rdx (offset into dump_)
  char current_byte; // cl (current byte in dump_)
  char v22[8]; // [rsp+50h] [rbp+8h] BYREF (used for temporary growth checking)

  mask_ = this + 16;
  this[14] = this[13];
  this[17] = this[16];
  size_value_ = this[9];
  if ( size_value_ )
  {
    i = 0i64;
    do
    {
      p = i >> 1;
      if ( p >= this[14] - this[13] )
      {
        new_size = this[14];
        v22[0] = 0;
        if ( (unsigned __int64)v22 >= new_size || (dump_condition = this[13], dump_condition > (unsigned __int64)v22) )
        {
          if ( new_size == this[15] )
            ResizeDump(this + 13, 1i64);
          mask_bytes = (_BYTE *)this[14];
          if ( mask_bytes )
            *mask_bytes = 0;
        }
        else
        {
          value_pointer = &v22[-dump_condition];
          if ( new_size == this[15] )
            ResizeDump(this + 13, 1i64);
          dump_bytes = (_BYTE *)this[14];
          if ( dump_bytes )
            *dump_bytes = value_pointer[this[13]];
        }
        ++this[14];
        mask_new_size = mask_[1];
        v22[0] = 0;
        if ( (unsigned __int64)v22 >= mask_new_size || *mask_ > (unsigned __int64)v22 )
        {
          if ( mask_new_size == mask_[2] )
            ResizeMask(mask_, 1i64);
          dump_expansion = (_BYTE *)mask_[1];
          if ( dump_expansion )
            *dump_expansion = 0;
        }
        else
        {
          mask_pointer = &v22[-*mask_];
          if ( mask_new_size == mask_[2] )
            ResizeMask(mask_, 1i64);
          mask_data = (_BYTE *)mask_[1];
          if ( mask_data )
            *mask_data = mask_pointer[*mask_];
        }
        ++mask_[1];
      }
      mask_byte = -1;
      if ( this[10] < 0x10ui64 )
        value_data = this + 7;
      else
        value_data = (_QWORD *)this[7];
      c_value = *((_BYTE *)value_data + i);
      if ( (unsigned __int8)(c_value - 48) > 9u )
      {
        if ( (unsigned __int8)(c_value - 65) > 5u )
        {
          if ( (unsigned __int8)(c_value - 97) > 5u )
          {
            mask_byte = 0;
            hex_value = RandomFunction();
          }
          else
          {
            hex_value = c_value - 87;
          }
        }
        else
        {
          hex_value = c_value - 55;
        }
      }
      else
      {
        hex_value = c_value - 48;
      }
      dump_offset = this[13];
      current_byte = *(_BYTE *)(dump_offset + p);
      if ( (i & 1) != 0 )
      {
        *(_BYTE *)(dump_offset + p) ^= (hex_value ^ current_byte) & 0xF;
        LOBYTE(size_value_) = (mask_byte ^ *(_BYTE *)(*mask_ + p)) & 0xF;
        *(_BYTE *)(*mask_ + p) ^= size_value_;
      }
      else
      {
        *(_BYTE *)(dump_offset + p) = (16 * hex_value) | current_byte & 0xF;
        LOBYTE(size_value_) = (16 * mask_byte) | *(_BYTE *)(*mask_ + p) & 0xF;
        *(_BYTE *)(*mask_ + p) = size_value_;
      }
      ++i;
    }
    while ( i < this[9] );
  }
  return size_value_;
}

其中的 value_data 就是我们想找的水印字符串的指针,对应汇编为

.text:00007FF74DE06F61 loc_7FF74DE06F61:                       ; CODE XREF: Watermark__Compile+71↑j
.text:00007FF74DE06F61                 cmp     qword ptr [r13+50h], 10h
.text:00007FF74DE06F66                 mov     bpl, 0FFh
.text:00007FF74DE06F69                 jb      short loc_7FF74DE06F71
.text:00007FF74DE06F6B                 mov     rax, [r13+38h]
.text:00007FF74DE06F6F                 jmp     short loc_7FF74DE06F75
.text:00007FF74DE06F71 ; ---------------------------------------------------------------------------
.text:00007FF74DE06F71
.text:00007FF74DE06F71 loc_7FF74DE06F71:                       ; CODE XREF: Watermark__Compile+169↑j
.text:00007FF74DE06F71                 lea     rax, [r13+38h]

其中 [r13+0x38] 为从 实例指针(this)+偏移量 中取出 水印字符串指针 赋值给 rax, 既 value_data

进一步追寻 r13 的来源,在函数开头发现了这一行

.text:00007FF74DE06E18                 mov     r13, rcx

根据 Windows 平台 x64 下的 fastcall 调用约定,第一个传入参数为 rcx , 既 this 指针,因此我们在 Watermark::Compile 函数开头直接使用 [rcx+0x38] 就可以获取水印字符串指针

注: 关于 x64 调用约定,可参考微软官方文档 https://learn.microsoft.com/zh-cn/cpp/build/x64-calling-convention?view=msvc-170#calling-convention-defaults

在调试器内下断点,获取水印字符串指针,直接看内存获得水印具体内容

image-5.png

0000028A4FC35E60  35 30 46 30 31 46 46 44 46 44 38 3F 3F 37 39 32  50F01FFDFD8??792  
0000028A4FC35E70  36 3F 3F 3F 42 34 3F 3F 43 32 3F 3F 3F 30 37 3F  6???B4??C2???07?  
0000028A4FC35E80  34 3F 3F 3F 3F 3F 43 3F 43 3F 3F 46 3F 44 32 3F  4?????C?C??F?D2?  
0000028A4FC35E90  36 3F 3F 31 39 43 42 46 30 3F 39 39 31 32 3F 37  6??19CBF0?9912?7  
0000028A4FC35EA0  31 37 3F 3F 33 36 33 35 43 41 38 41 3F 37 3F 30  17??3635CA8A?7?0  
0000028A4FC35EB0  3F 3F 3F 46 3F 43 3F 44 37 44 37 3F 3F 39 45 35  ???F?C?D7D7??9E5  
0000028A4FC35EC0  3F 31 3F 38 34 45 34 3F 3F 3F 32 34 3F 3F 44 34  ?1?84E4???24??D4  
0000028A4FC35ED0  35 3F 35 3F 43 3F 30 34 42 39 45 3F 44 3F 32 3F  5?5?C?04B9E?D?2?  
0000028A4FC35EE0  31 35 3F 38 39 3F 3F 36 3F 37 38 34 3F 3F 3F 3F  15?89??6?784????  
0000028A4FC35EF0  3F 44 39 3F 3F 31 3F 31 3F 45 3F 3F 30 33 3F 3F  ?D9??1?1?E??03??  
0000028A4FC35F00  3F 3F 3F 34 34 36 3F 36 3F 3F 3F 33 45 43 39 34  ???446?6???3EC94  
0000028A4FC35F10  31 45 3F 36 41 3F 3F 34 3F 35 3F 3F 3F 3F 3F 3F  1E?6A??4?5??????  
0000028A4FC35F20  3F 3F 38 3F 43 3F 3F 38 3F 3F 3F 32 3F 3F 3F 30  ??8?C??8???2???0  
0000028A4FC35F30  43 38 45 42 3F 43 31 3F 44 3F 34 3F 00           C8EB?C1?D?4?.     

其中的 ? 为随机内容,疑似为保留随机性的行为,同时,这里的内容其实为十六进制表示的二进制内容,所以我们在搜索的时候需要将程序转化为 hex 形式的字符串再进行搜索,概念验证代码如下

import re

def read_file_as_hex(filepath):
    with open(filepath, 'rb') as file:
        content = file.read()
    return content.hex().upper()

def search_hex_pattern(hex_pattern, hex_string):
    hex_pattern = hex_pattern.replace("?", "[0-9a-f]")
    return re.findall(hex_pattern, hex_string, flags=re.IGNORECASE)

filepath = "VMProtect.exe"
watermark = "50F01FFDFD8??7926???B4??C2???07?4?????C?C??F?D2?6??19CBF0?9912?717??3635CA8A?7?0???F?C?D7D7??9E5?1?84E4???24??D45?5?C?04B9E?D?2?15?89??6?784?????D9??1?1?E??03?????446?6???3EC941E?6A??4?5????????8?C??8???2???0C8EB?C1?D?4?"

hex_file_content = read_file_as_hex(filepath)
match_results = search_hex_pattern(watermark, hex_file_content)

print(match_results)

可以看到成功识别了 VMP 的水印

image-6.png

换用某厂加了 VMP 壳的 UnityPlayer 进行测试,一样成功检测,并且 ExeinfoPe 没有检测到 VMP 的存在 (已使用 ExeinfoPe 最新版本)

image-7.png

image-8.png

总结

朋友赏饭吃,我只是个蹭饭的 ()

顺便给个 Star 呗 () : https://github.com/DNLINYJ/DetectVMP3

免费评分

参与人数 24吾爱币 +27 热心值 +21 收起 理由
笙若 + 1 + 1 谢谢@Thanks!
longgui0318 + 1 我很赞同!
MIAIONE + 1 + 1 用心讨论,共获提升!
ytfh1131 + 1 + 1 谢谢@Thanks!
triangle111 + 1 + 1 谢谢@Thanks!
qiaoyong + 1 + 1 热心回复!
Camus2021 + 1 + 1 用心讨论,共获提升!
aatrox、 + 1 + 1 谢谢@Thanks!
merky + 1 我很赞同!
Light紫星 + 3 + 1 用心讨论,共获提升!
macdym + 1 谢谢@Thanks!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
timeni + 1 + 1 用心讨论,共获提升!
allspark + 1 + 1 用心讨论,共获提升!
ekobe1796 + 1 我很赞同!
LZC13807614037 + 1 + 1 谢谢@Thanks!
gr520 + 1 + 1 我很赞同!
akyra + 1 + 1 谢谢@Thanks!
Tonyha7 + 2 + 1 谢谢@Thanks!
alexxi0571 + 1 谢谢@Thanks!
kelvar + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
3yu3 + 1 + 1 用心讨论,共获提升!
only998 + 1 + 1 我很赞同!
忆魂丶天雷 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| DNLINYJ 发表于 2023-12-17 00:49
本帖最后由 DNLINYJ 于 2023-12-17 01:03 编辑
iaoedsz2018 发表于 2023-12-17 00:38
最后解析出来的数据似乎也不包含什么有用的信息啊

这个主要是应对某些时候无法检测出目标程序是否含VMP3壳的另类解决方案 如果检测到水印 可以视为这个程序加了VMP3的壳
chmod755 发表于 2023-12-27 20:47
测试 vmp 3.0.9 ,水印检测有效
直接用yara 把楼主的规则粘进去,能识别
rule vmp_3
{
meta:
    description="check vmp 3.x"
strings:
   $vmp={ 50F01FFDFD8??7926???B4??C2???07?4?????C?C??F?D2?6??19CBF0?9912?717??3635CA8A?7?0???F?C?D7D7??9E5?1?84E4???24??D45?5?C?04B9E?D?2?15?89??6?784?????D9??1?1?E??03?????446?6???3EC941E?6A??4?5????????8?C??8???2???0C8EB?C1?D?4? }
condition:
  all of them
}

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
DNLINYJ + 1 + 1 用心讨论,共获提升!

查看全部评分

AdmiMVP1 发表于 2023-12-16 17:42
文西思密达 发表于 2023-12-16 18:04
有了源码之后 是不是旧版本的vmp壳子都可以破了
FruitBaby 发表于 2023-12-16 18:30
源码都拿到了,爱咋玩咋玩
CXC303 发表于 2023-12-16 19:40
楼主,github链接里没看到源码,或成品啥的
iaoedsz2018 发表于 2023-12-17 00:38
最后解析出来的数据似乎也不包含什么有用的信息啊
Hmily 发表于 2023-12-17 00:40
学习,期待更多源码级的发现分享。
Xistorg 发表于 2023-12-17 07:20
注意到过这个函数,没细看
xuelinghua 发表于 2023-12-17 09:54
期待更多源码级的发现分享,谢谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-22 18:58

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表