solly 发表于 2022-8-15 10:11

详细逆向分析一款非常零类的全平台应用开发工具的授权和激活过程

本帖最后由 solly 于 2022-8-15 10:19 编辑

很久没有发贴了,但签到还是非常多的。{:1_918:}
这个全平台开发工具是一个由 RealBasic 演化来的开发套装,其IDE支持Windows、Linux和MacOS,可以开发桌面、移动和数据库应用,功能还是比较齐全,更新也算比较勤快。


软件的启动Flash屏如下:

最新版本是2022release 2了,我分析的是上面这个次新版。

IDE没有授权时,会在工具栏显示“Buy Xxxx”的按钮,并且没有授权时,只能在 IDE 运行调试程序,不能编译成二进制的程序来发布。如下图所示,如果点“Build”,会弹出一个需要购买授权的对话框:


该IDE授权是以授权文件的方式进行授权认证的,主要有两种方式导入授权文件:
1、通过打开工程文件的方式打开授权文件进行认证;
2、通过 License Keys 菜单打开授权管理对话框,再添加授权文件。

下面是打开工程文件的入:


通过菜单 License Keys 的方式如下图:


打开的授权文件管理对话框如下图所示:

现在还没有导入授权文件。

我们可以点“Add...”,打开一个授权文件

(我已生成好了一个授权文件,这个文件是一个加密后并编码的XML文件,在后面讲如何生成该文件)。
我们用 x64dbg 打开其主程序。进行授权过程的调试,在项目界面打开授权文件(第1种模式)后,中断在如下位置(先下好断点,再打开授权文件):

可以看到,首先对文件头的前15个字符进行检查,如果是“605B6B0E6BA1BA1”开头的话,就是表示这个文件是授权文件,会进入授权验证过程,否则就是项目或其它文件,不会进入授权文件验证过程。
”605B6B0E6BA1BA1“是一个固定的字符串,保存在资源区,具体的比较位置在上面函数内的位置如下图所示,调用其内置的字符串比较函数进行比较,如下图所示:

上面用到了字符串常量结构,该结构如下所示:
string_const_struct {
    dword ref_count;
    dword align1;
    qword address;      //// 保存字符串的缓冲区
    qword unknown1;
    dword length;
    dword properties;   /// 0x08000100
    qword unknown2;
}
上面数据区大的红色框是字符串常量结构的地址指针,指向具体字符串,如下图数据区所示的字符串,另一个小的红色框是字符串的长度。

上图是字符串内容。
比较的结果在下面位置检查,如果检查是授权文件,开始生成前那个授权管理对话框,这个对话框有4个TAB页签,第2个页签就是"Licenses",进行授权管理的。


就是在 "AboutWindow"中来处理授权文件,首先取得该对话框的实例,然后显示该界面:

在初始化和显示的过程中,会初始化口另外一个类对象,这个类对象为:"Registration.StudioKeyValidator",是对授权文件进行验证的类,如下图所示,是该类初始化的代码:

初始化完类后,就是取得其实例,如下所示:

类实例成对象后,然后调用其 constructor 方法,对对象实例进行初始化:

其 contructor 的参数就是授权文件的内容,如上图数据区所示,又是一个 string struct,因为不是常量,字符串内容就紧接在结构的后面,中间只隔了一个字节。
另外,字符串内容的第1个字节是“FF”,这个值也是长度,当实际长度小于 255时,存的是实际长度,当长度大于或等于255时,都是存的“FF”,可以看看前面的“605B6B0E6BA1BA1”,第1个字节是“0F”,表示长度是15字节。


构造函数首先解码授权文件内容,将其转换为16进制的字符串,如下图所示:

转换前如上图数据区所示,转换后如下图数据区所示,变成了16进制格式的字符串了:

然后,会再次生成一个内存块对象(MemoryBlock)这个是进行内存数据操作的对象类,相当于C语言的指针操作了,执行完一大段该对象初始化的代码后,来到这里:

这里就是调用该对象的方法,将数据写入其缓冲区,MemoryBlock 对象结构如下所示,红框中是实际保存数据的指针:

MemoryBlock 对象结构如下所示:
memory_block_object_struct {
    pointer methods[];
    pointer properties[];
    pointer events[];
    qword ref_count;
    qword unknown;
    qword unknown;
    qword size;
    pointer data_buffer;          //// 保存数据的缓冲区的指针
    byte isInit;
    byte isAllowDirectAccessBuffer;
    byte mem_align;
}
下图更详细的显示其结构:

在MemoryBlock 的属性表中,保存有对象的类名,如下图所示的数据区中是 memoryBlock的属性和成员表:

第3个指针就是指向类名,如下图所示:

这种结构是固定的,所以我们可以查询所有内置对象的类名,便于我们跟踪调试,知道现在运行的类对象代码是哪个类的。

回到主题,执行完前面的函数据,memoryBlock对象的缓冲区保存的解码后的授权文件,准备下一步的解密操作了:


现在解码后的数据还是加密的,加密算法是 Blowfish,并且很好确定算法,因为这是一个Blowfish对象,通过前面讲的查类名的方法就知道了。

首先是取得 class,后面又是一大段实例化和初始化的代码,下面就是调用初始化函数的位置:

初始化函数的参数是解密密码,这个密码是一个常量字符串,如上图数据区所示,看起来象是前面我们讲过的函数的返回值和参数描述,只是第1个字符是“+”号,“+”前面的“2C”是长度。

初始化好密码后,就开始循环对 memoryBlock的缓冲区数据进行解密了,如下图所示:

每次循环会解密8个字节的数据,如下图所示,已完成前面8字节的解密:

全部解密完成后,就是一个XML文件,如下图所示,已经完成整个授权文件地解密了。

可以看到,这是一个 XML 文件,第1步工作已经完成了,下一步是对授权内容进行处理了。

先把解密后的 XML 文件内容从 memoryBlock 中取出来,生成一个字符串,如上图所示。

首先,验证解密后的内容是不是一个XML格式的内容,如下图所示,检查前6个字符是否"<?xml "(最后一个字符是空格):

就是简单的取出前6字节进行较,如下图所示:

如果检查是 XML 格式的字符串,则初始化一个 XMLDocument 对象类,并通过其构造函数来解析XML,生成XMLDocument对象:


XMLDocument 对象生成成功后,就是循环取出其节点值,进行一下步的解析处理:

主要是解析3个标签的值:
1、address
2、signature
3、serial
其它的标签是不解析的,如上面的授权文件中我加了一个email标签,就不会取出来。如下图所示,是检查3个标签的位置:

取出来的标签的值,会保存到 KeyValidator 对象的属性中,如下图所示,保存的是 serial 属性:

下面保存是 address:

以及 signature:

接下就是执行 KeyValidator 的构造函数,对刚才取出来的三个XML标签值进行下一步的验证,检查授权是否合法:


因为后面还有解密过程,并且是用的 email 作为密码解密,程序会从授权信息数据库查询以前的email,这样就可以不用用户来输入解密密码了,如下所示,查询授权文件数据库(是一个全文加密的SQLite数据):

该文件保存在目录"c:\Users\用户名\AppData\Roaming\Xojo\Xojo"中,文件名是“License Keys”。如下图所示,是查询密码的SQL:

如果没有查到密码,也就是没有以前的授权文件,就会显示一个对话框,由用户输入邮箱名作为后的解密密码:

显示密码对话框:

该对话框如下图所示(2个图,1个没有输入,一个已输入email):

如果输入的邮箱地不正解,后面的解密数据会不对,在解密后的校验中,会弹出以下错误:

如果邮箱没有错,可以继续下一步的解密过程了。

刚才弹出的密码对话框,又会再次生成和实例化 Registration.StudioKeyValidator 对象,并再次在 constructor 中重复一遍前的解码、解密、XML解析过程,很是无聊,这就是面向对象编程的问题,每次 new Object 后,要重复 constructor 一次。


把前过程重复一次后,我们来到了这里:

这里将输入的email地址转换成全小写的字符串。

接下来就是用这个小写的 email 为密码,执行一个解密函数,生成另外一个对象:StudioKeyData 对象,保存具体的授权数据。

我们进入这个函数,跟踪处理过程(按 F7 进入函数)

首先处理 StudioKeyValidator.signature 属性:

取出 signature 属性的字符串,这是一个 1024 个字符的字符串,包含有两个部分,每部分都是 512 个字符,后面会讲到其作用。

如下图所示,先截取其前面 512 个字符:

再取剩下的一半字符,如下图位置代码来完成:


取出后半部分后,首先对其进行解码,变成16进制的字符串,长度也变成了 256 字节了:

这个 256 字节的字符串,实际上是一个加密后的 2048 bits 的 RSA 签名。

接下来是对这个加密的签名进行解密。

这次解密的密码是动态生成的,不是常量字符串了,并且跟 signatrure 的前半部分的 512字符相关。

这次密码有 64 字节,由 memoryBlock 保存,所以先初始化一个 memoryBlock 对象:


首先计算前 16 字节的密码,这16字节是我们输入的 email 地址的 MD5 码,如下图所示,计算 MD5 码:

将 16 进制的 MD5 码存入 memoryBlock 的缓冲区,如下图所示:


接下来处理序列号,生成与序列号相关的一段密码,先将序列号中的“-”去掉,如下图所示:

同样进行16制解码,变成16进制的字符串:

serial 变成 24 字节长的 16 进制字符后,填充到密码缓冲区:


第3步就是还有24字节的密码需要生成,而这24字节的密码的生成与 signature 前512个字符相关了,24个字节密码的前16个是一个字节一个节生成的,最后8字节是一次生成的(是一个8字节的双精度浮点数)。

这个步骤的代码相当长且臭,就不一一説了,就是一些查字符串位置,统计某些字符的个数等,并把得到的这些数据填充到密码表中。

查找和统计的字符串有“CAF”, "BAD", "11", "8", "A", "3", "7"等等,还有取最后一个还是2个字符的操作。如下图,是统计字符“A”的个数:

然后将个数据写入密码表:

就这样生成16字节的密码,存入密码表:


最后8字节的密码是一个幂计算的结果(8字节双精度浮点数),如下图所示:

这样,就生成了 64 字节 512 bits 的密码。

(这里讲一个问题,这个密码是512bits的,但 blowfish算法的密码为32bits到448bits,正常情况下用 512bits密码会报错,但这个IDE的Blowfish对象不报错,试过其它空码实现,都不行,会报错,所以,后面的注册机也只能用这个对象来加密,并且还要共它处理,不过,这个 Blowfish对象是 undocument 的,怎么用要自己试。。。。。。)

密码生成好了,就开始再次进行解密了,先取出密码,生成密码字符串:

一样也是 blowfish 解密,一样的循环解密:

解密完成,得到一个 RSA 签名:


那接下来就是签名验证了,是对 StudioKeyValitor.serial + StudioKeyValitor.address 进行签名验证,这个 serial 和 address 也是从 XML 授权文件中取来的
先从 MemoryBlock 中取出签名字符串:


然后就是生成签名验证的 msg 文本了,是一个 serial 和 address 合并的字符串,分隔符是0x0A,如下图所示:

这样 RSA 签名所需的 sign 和 msg 都准备好了。

接下来就 RSA 签名验证过程了,不过这里要替换 RSA 公钥了。
先生成 RSA 对象:

读取公钥(这个公钥我已经换过了):

运行签名函数,看 al 的值,为 1 表示签名正确:

检查签名校验是否正确:

正确就生成 StudiokeyData 对象,保存相关授权信息。
相关授权信息存于 serial 中,所以 StudioKeyData 的构造函数的参数就是 serial,如下图所示:

授权都有一个过期日期,该构造函数默认会认为是 365 天,所以先将属性 expiration = 365,如下图所示:

再次将序列号的”-“去掉,方便后面hex 解码:

再次解码为 16 进制字符串:

然后又放入到 memoryBlock 中,干什么呢?当然是还要解密了。
先构造一个 memoryBlock 对象:

写入 16 进制的 serial:

这次用小写的email地址,初始化一个 Blowfish 对象:

又是一个循环解密:

完成解密,得到一个24字节的授权信息,如下图的数据区:

得到授权信息后,首先检查是否有效,就是检查 serial == 0x2C,如果相等,表示有效,上图是取出该值,下图是检查是否为0x2C:

然后取出 serial ,如上图所示,对这个值也要检查。

如果这个值大于1,也表示授权信息无效,会抛出不支持的异常,退出验证过程。

接下来是一个日期验证,看看授权文件的生成时间是不是比当前系统日期还晚,如果还晚就表示无效。

先取出日期,就是 serial*256 + serial,一个 word 值。

将年份置入 Date 对象的 year 域。

接下来取 serial, 这是一个WeekOfYear = 0x20,只有一个字节,并赋给 Date 对象。

然后来取 serial, 这是一个DayOfWeek = 0x05,也只有一个字节,也赋给Date对象:

这样通过 year,weekOfYear, DayOfWeek生成一个日期,然后取其总秒数,用于后面的时间比较:

取得秒数后,与另一个表示当前系统时间的日期总秒数比较:

如果 shippedDate > currentSystemDate , 则授权文件无效,退出验证过程。

如果 shippedDate 有效,则进行过期日期的检查,先取出过期日期,是一个相对的 Days (天数),存于 serial和 serial,其值不得大于 0x7FFF:

上图是取出了 expiration Days。

经过一些处理,就是计算还有多少天(去掉 currentSysyDaye - shippedDate),并保存到 StudioKeyData 对象中:


然后就是读取订阅号: subscription_id:

保存 subscription_id:


接下来就是读取 serial_index 了:

保存 serial_index:


最后取出来的是 Feature,版本特性,表示授权文件包括哪些版本特性:

版本特性是一个4字节值,是多个特性 bits 合成后的值,特性 bits 如下所示:
1 - Not for resale
2 - Desktop
4 - Web
8 - Database Servers
16 - iOS
32 - Console
64 - Pro
128 - Lite Mac
256 - Lite Windows
512 - Lite Linux
1024 - Pro Plus
2048 - Single Board Computer
other - Unknown
每一个 bit 代表一个版本。
保存 feature 到 keyData 对象:

另外,在保存 feature 时,会检查是否包括 console 和 pro 版本,如果包括这两个版本之一,则加上 Single board computer 版本特性,如下图所示。

这样,StudioKeyData 的 constructor 就执行完了,我们按 "CTRL + F9" 退出函数,来到下面:


我们再次按 "CTRL+F9"退出当前函数,回到下面的位置:


接下来是一个黑名单检查,如果进入黑名单,一样也是无效:

目前黑名单只有一个,检查结果在下面位置处理:


接下来就是过期日期检查了:

一样也是通过总秒数对比大小来检查是否过期:

取软件发布日期,是一个常量,不过是经过简单计算得来的:

就是 2001-01-01 加上 670896000 秒,计算后得到日期:2022-04-06
进行比较:

如果没有过期,就开始授权数据库,检查是否有老的授权文件,如果查询到了老的授权记录,就检查 serial_index 的大小:

如果数据库中的授权记录的 serial_index 大于或等于当前的 serial_index 就会报错,如果没有老的授权,取得的 serial_index = 0,肯定小于当前的 serial_index:

按“CTRL+F9”退出函数,返回到如上图的位置。

所有验证都通过了后,就是网络验证。

上图就是网络验证的位置,按“F7”进入该函数。

来到这信位置:

按“F7”进入该函数。

如下图所示位置:

再按“F7”进入函数。

来到如下图位置:

又是按"F7"进入函数。

循环写入XML节点的值。

将生成的 XMLDocument 转换成字符串:

XML字符串生成好了,按 "CTRL+F9"退出 BuildXML() 函数。


后面就是发送激活信息,我们将建立一个模拟激活服务器来处理激活过程,首先修改 Hosts 将激活服务器指向本机:


因激活服务器是 https 的,我们还要搭建一个 https 服务,最简单的 https 由 python 来搞定,创建好https需要的ssl证书就可以,另外 python 那个内置的 https 服务没有处理 POST,所以我们还要写一个定制的post 处理的 handler。
代码如下图所示,大部分来自网络,我只是合并修改了一下:

启动上面的服务。如下图所示:


返回 x64dbg,提前下好处理返回信息的断点后(后面会讲到),直接按“F9”运行程序:


激活服务会收到 POST 过来的激活XML内容:


然后 x64dbg 断在我们提前下好的接收处理函数内的断点上:

前面説过,我们要先在激活回应处理的函数(都是事件处理函数),这个事件是网络事件,是对象 RealID_Message 的事件,他是另一个类的继承,这个类是 _wrapper_ServerComm,_wrapper_ServerComm又是StudioLicenseKeyPasswordDialog对象的一个类模块,这个 StudioLicenseKeyPasswordDialog 就是那个询问密码(邮箱地址)的对话框类。
激活服务器返回的是一个 XML 格式的文本内容,格式如下所示:
<?xml version="1.0" ?>
<methodResponse>
<params>
<param>
<value><int>8888</int></value>
</param>
</params>
</methodResponse>
其中只有一个有用的值,就是<int>8888</int>,表示返回的是整数,值为 8888,而这个 8888 表示的是剩余可激活的次数(同一台机器多次激活也只算1次,也可以説是剩余可激活的设备数)。

F8往下走,来到这里,对收到的XML内容进行处理:

按"F7"进入该函数。

还是老办法,生成一个 XMLDocument 对象来处理 XML 文档,如下图所示:

继续往下走一段代码,来到如下位置,就是解析并取得那个最终的响应值了:

这个函数返回的是一个 Variant<Double> 类型的值,其实就是 8888。
Variant 也可看作一个对象,结构如下:
variant_struct {
   pointer methods[];      /// methods = NULL
   pointer properties[];   ///
   pointer events[];       /// events = NULL
   qword   ref_count;   /// reference count
   qword   unknown1;
   qword   unknown2;
   qword   value_or_address;   //// 当时基本类型值时,保存的是实际值;当是字符串或对象时,保存的是地址指针
}

取得这个值后,接下传递给下一个事件,是以 Delegate 调用的方式来处理的,如下图所示:

这样,我们进入另一个对象(ServerComm)的事件处理,这是前面讲过的 StudioLicenseKeyPasswordDialog._wrapper_ServerComm 的一个实例。

这个事件处理函数的参数就是前面的 Variant<Double>类型的变量。

在处理这个数据前,先恢复成整数值,如上图所示。

然后就是对这个数据进行对比,根据返回值进行对应的处理和提示:


如果 int >= 1,表示激活正确,还有剩余激活次数。
如果 int = -1,表示激活异常,激活失败了。
如果 int = -2, 表示激活次过多,不能再激活了。
上面会根据不同的返回值,取得不同的提示信息字符串。

接着往下走,如果激活成功,就会保存当前授权文件的内容到数据库了,下面就是调用保数据的位置:

这个函数内有保存到数据库的代码,相当长,生成SQL的位置如下:

接下来就提示激活成功了:

上面还剩余多次的情况,如果只剩一次了,提示如下(其实就是处理了一下英文名词单数和复数的显示问题)


如果失败,就不会保存授权数据到数据库,同时会提示出错:

这个就是前面判断<int>-1</int>的情况。


这里是前面判断<int>-2</int>的情况,表示激活次数(设备)过多,超过次数,只能迁移以前的授权过来了。

还説明一下,前面那个判断是有 BUG 的,当值为 0 或 -3 及以及下的负数时,也是激活成功的:



前面我们説过 serial_index 的问题,就是新加的授权文件中的 serial_index 一定要大于数据库中的授权记录中的 serial_index,就是:
xml_serial_index > db_serial_index 要满足,否则就会报以下错误:

只要将同一个授权文件添加两次,第2次就会报这个错误了。

这样,整个授权检查和激活过程就结束了。

下面,我説一下如何快速查看一个对象的类名吧,一个对象类的基本结构如下:
object_struct {
    pointer events[];
    pointer properties[];
    pointer metheds[];
    qword ref_count;
    qword inherited_count;//// 不确定
    dword unknown1;
    pointer ref_object;   //// 最后引用的对象,没有则是指向对象本身
    dword unknown3;
    dword unknown4;
    dword unknown5;
}
下面是上面结束 properties[] 的前面几个属性:
properties_struct {
    pointer   base_class;
    qword   unknown1;
    char *    class_name;
    dword   unknown2;
    dword   unknown3;
    .......   properties_list
}
可以看到,第3个属性就是类名了,下面以图説明:

上图中,rcx 指向一个对象, 就是属性和成员表了。

在数据区按CTRL+G,输入定位到属性和成员表,如上图所示位置,其中第3个属性是一个地址,指向类名,也就是指向 [ + 0x10]。

再在数据区按CTRL+G,输入[ + 0x10] 就可以看到类名,这样只要知道 rcx 是一个对象,直接 [ + 0x10]就会显示类名了。

还有就是它的对类的方法和属性的名称,函数的返回值和参数的类型,也都可以查到,如下图所示是 XMLDocument 的一个方法:


============================ 分界线 ======================================

下面我们进入注册机的主题了,生成一个可用的授权文件(当然前提是 RSA 的公钥要换成我们自己的)。

这里还要説明一下,它那个 RSA 签名过程与标准的开源的不一样,我作了对比,有些差异,并且它的 RSA 签名过程是通过 Plugin 来完成的,我们也要通过这种方式来完成,否则签名是通不过的。
还有就是系统自带了 RSA 签名的函数,但这个函数生成的签名也是通不过的,这个函数如下:
RSASign(data As MemoryBlock, privateKey As MemoryBlock) As MemoryBlock
所以我们也只能调用它的插件来实现签名,但是系统没有带这个插件,其插件目录中没有这个 RSA 签名的插件。但是系统的库目录(Xojo Libs)带的 RSA 签名的动态库,有这个就可以了,我们自己来依葫芦画瓢制作一个,
我们查看一下插件目录(Xojo Libs),有系统自带的插件,其实这些插件是 ZIP 格式的文件,我们用压缩软件打开一个插件,就可以看到,其结构很简单,就是包括不同编译目标平台的动态库。

我们只有 x86_64 的库,只要这个就可以,如下图所示,我们自制了一个 OpenSSL 插件:

插件中的DLL来自其 Libs 目录中的库文件。
保存这个文件到其安装目录下的 Plugins 子目录,并重新启动 IDE 就可以用了。具体怎么用可看看其 plugin SDK 的文档,有c++的接口説明,再用 IDA 看看DLL就差不多理解了。

还有就是前面説过的 Blowfish 的密码长度超过正常的长度,所以得用其内置的 _Blowfish 类来处理 Blowfish的加密,不然也是通不过的,而这个 _Blowfish 类是不公开的,也就上没有文档説明,其文档中的两个Blowfish相关函数:BlowFishDecrypt(publicKey As String, data As MemoryBlock, blockMode As Crypto = BlockModes.CBC, initializationVector As MemoryBlock) As MemoryBlock
BlowFishEncrypt(publicKey As String, data As MemoryBlock, blockMode As Crypto = BlockModes.CBC, initializationVector As MemoryBlock) As MemoryBlock
也不是通过这个类来实现的,这两个函数也不能处理超过长度的密码。

以下是注册机的主要代码:
const NID_SHA1 as Integer = 64 ////&H40
var features() as int64 = Array(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 2147479552)
var crlf as String = EndOfLine.UNIX
const xml_cryptoKey as String = "+%%o<Graphics>o<RectControl>o<RBControl>i4i4"

//// email and password
var email as String = txtEmail.Text.Trim.Lowercase

if email = "" then
MessageBox "Please Input a Email Address!!!"
exit sub
end if

//if lstEditions.SelectedRowIndex = ListBox.NoSelection then
//MessageBox "Please select edition features!!!"
//exit sub
//end if

///// ==================================================================================================================
/////生成序列号
var password as string = email

//if password = "" then
//MessageBox "Password(email address) is Null, Input eMail Address First!!!!!!"
//exit sub
//end if

var mem as MemoryBlock = new MemoryBlock(24)

mem.LittleEndian = false

// unused
mem.Int8Value(0) = &H20

// release date
// 2022-04-06
/// shipped_date
var today as date = new Date
var shippedDate as date = dtpShippedDate.SelectedDate
var year as UInt16 = shippedDate.Year
var wks as UInt8 = shippedDate.WeekOfYear
var wDays as UInt8 = shippedDate.DayOfWeek

/// shipped date
mem.UInt16Value(1) = Year
mem.UInt8Value(3) = wks// yestoday
mem.UInt8Value(4) = wDays

/// expiration = valid_days - today - 1
var expiredDays as Integer = CType((dtpLicensedDate.SelectedDate.SecondsFrom1970 - dtpShippedDate.SelectedDate.SecondsFrom1970) / 86400, Integer)
if expiredDays < 0 then
MessageBox "Licensed Date Must be more than Shipped Date!!!"
Exit sub
end if
if expiredDays > (&H7FFE - today.Day) then
MessageBox "Licensed Date too bigger!!!"
Exit sub
end if
//mem.Int16Value(5) = expiredDays//// key的有效天数,默认 365 天,生成的授权文件在 365 天内加入都 有效,否则会失效
mem.Int16Value(5) = &H7FFF - 365
// loop one, >1 则 raise UnsupportedFormatException
mem.Int8Value(7) = 1// not loop

// unused
mem.Int8Value(8) = &H20
mem.Int8Value(9) = &H20

// SubscriptionID

mem.Int32Value(10) = Integer.FromString(txtSubscriptionID.Text.Trim)//&H88888888
// serial_index
mem.Int16Value(14) = Integer.FromString(txtSerialIndex.Text.Trim) //&H6667

// unused
mem.Int8Value(16) = &H20

//feature
var feature as UInt32 = 0
for i as Integer = 0 to lstEditions.LastRowIndex
if lstEditions.RowAt(i).Selected then
    feature = feature or features(i)
end if
next i
if bitwise.BitAnd(feature, &H36) = 0 then      //// Desktop, Web, iOS, Console 必选
feature = &H476// Desktop, Web, iOS, Console, Pro, Pro Plus
end if
mem.UInt32Value(17) = feature//&H7FFFFFFE

// tag
mem.Int8Value(21) = &H2C    ///逗号 ',',序列号有效性标志

// unused
mem.Int8Value(22) = &H20
mem.Int8Value(23) = &H20

//Crypto.BlowFishEncrypt(key As String, data As MemoryBlock, blockMode As BlockModes = BlockModes.CBC, initializationVector As MemoryBlock) As MemoryBlock
var mem2 as MemoryBlock = Crypto.BlowFishEncrypt(password, mem, Crypto.BlockModes.ECB, nil)
var serial_hex as string = mem2.StringValue(0, mem2.Size)
var serial_str as string = EncodeHex(serial_hex)
var serial_xml as string = serial_str.Middle(0, 8) + "-" + serial_str.Middle(8, 8) + "-" + serial_str.Middle(16, 8) + "-" + serial_str.Middle(24, 8) + "-" + serial_str.Middle(32, 8) + "-" + serial_str.Middle(40, 8)


////// debug
////MessageBox "SerialNo: " + serial_xml

////// ==================================================================================================================
/////生成序列号和地址信息的签名
var privateKey as String = Self.rsa_private_key.ReplaceLineEndings(EndOfLine.UNIX)//// 取得私钥
if privateKey = "" then
MessageBox "RSA private key is null!!!!"
exit sub
end if

////用户名,一般为电脑登陆名
var userName as String = txtName.Text.Trim
if userName = "" then
userName = app.LoggedInUserName   ///
end if

/// 生成地址信息
var address as String = txtName.Text + crlf + txtAddress1.Text + crlf + txtAddress2.Text + crlf + txtCity.Text + "," + txtProvince.Text + "," + txtPostCode.Text + crlf + txtCountry.Text
address = address.ReplaceLineEndings(EndOfLine.UNIX)

//// 生成RSA签名文本
var msg as String = serial_xml + crlf + address
var signRet as String = ""

var private_key_pem as String = "-----BEGIN PRIVATE KEY-----" + EndOfLine.UNIX + privateKey + EndOfLine.UNIX + "-----END PRIVATE KEY-----"
//var public_key_pem as String = "-----BEGIN PUBLIC KEY-----" + EndOfLine.UNIX + publicKey + EndOfLine.UNIX + "-----END LUBLIC KEY-----"

////注意:需要安装插件 OpenSSLPlugin.xojo_plugin,否则不能调用下面4个OpenSSL函数,该插件可自制
////注意:需要安装插件 OpenSSLPlugin.xojo_plugin,否则不能调用下面4个OpenSSL函数,该插件可自制
////注意:需要安装插件 OpenSSLPlugin.xojo_plugin,否则不能调用下面4个OpenSSL函数,该插件可自制
var rsa as Ptr =OpenSSL.RSA_New()

rsa = OpenSSL.PEM_read_RSAPrivateKey(private_key_pem, rsa)

var isRsaSignOK as Boolean = OpenSSL.RSA_Sign(NID_SHA1, msg, signRet, rsa)   ////生成RSA签名: signRet

OpenSSL.RSA_Free(rsa)

var signRet_str as String = ""
if isRsaSignOK then
signRet_str = EncodeHex(signRet)
//// debugInfo
////MessageBox "RSA Sinature return: " + crlf + signRet_str
else
MessageBox "Generate RSA Signature failure!!!!!!"
exit sub
end if

if signRet_str.Length <> 512 then
MessageBox "Encode RSA Signature failure!!!!!!"
exit sub
end if

////// debug
///taLicenseKeyContent.Text = signRet_str


/////==================================================================================================================
///// 生成 XML 文件的 signature 标签内容
var sign_pass As MemoryBlock = new MemoryBlock(64)

sign_pass.LittleEndian = False

//MD5
var md5Value As MemoryBlock = Crypto.MD5(password)
sign_pass.StringValue(0, 16) = md5Value.StringValue(0,16)

//Serial
sign_pass.StringValue(16, 24) = serial_hex

var signature_password_constant as String = password_retrieval_table//// 含一定的字符串:“CAF", "BAD", "11", "8", "B", "3", "7", "D", "F", "C"等

var signature_verify as String = signRet_str

if signature_verify = "" then
MessageBox "RSA Signature First!!!!!"
exit sub
end if

var pass_str as String
if chkRandomPwdBase.Value then
//var randBytes as MemoryBlock = Crypto.GenerateRandomBytes(256)
//var signature_password_random as String = EncodeHex(randBytes.StringValue(0, 256), false)
var signature_password_random as String = getRandomString()//// 生成随机的密码检索表
//// debugInfo
//// MessageBox "random string: " + signature_password_random

pass_str = signature_password_random   //// 随机
else
pass_str = signature_password_constant    //// 固定
end if
var sign_verify_str as String = signature_verify
var sign_verify_str_hex as String = app.MyDecodeHex(sign_verify_str)// 16 进制的签名

if pass_str.Length <> 512 then
MessageBox "pass base string length is not 512 bytes!"
exit sub
end if
if sign_verify_str.Length <> 512 then
MessageBox "signature verity string length is not 512 bytes!"
exit sub
end

//MessageBox "sign_verify_str.length = " + sign_verify_str.Length.ToString
//MessageBox "sign_verify_str_hex.length = " + sign_verify_str_hex.Length.ToString

while (sign_verify_str_hex.length mod 8) <> 0
sign_verify_str_hex = sign_verify_str_hex + " "
wend

var count as Integer= 0
var min_val as Integer = 0
var sum_val as Integer = 0
var position as Integer = 0
var start as Integer = 0
var pow_val as Double = 0.0
var str_tmp1 as string = ""
var str_tmp2 as string = ""
var str_tmp3 as string = ""

/// byte(40)
count = CountOfStr(pass_str, "A")
sign_pass.Int8Value(40) = count

/// byte(41)
str_tmp1 = pass_str.Right(1)
str_tmp2 = MidStr(pass_str, pass_str.Length-2, 1)
position = charAt(pass_str, str_tmp2)// Instr()
count = CountOfStr(pass_str, str_tmp1, position)
sign_pass.Int8Value(41) = count
//messagebox str_tmp1 + ", " + str_tmp2 + "," + position.ToString

/// byte(42)
count = CountOfStr(pass_str, "CAF")
sign_pass.Int8Value(42) = count
//messagebox "count of ""CAF"": " + count.ToString

/// byte(43)
count = CountOfStr(pass_str, "8")
sign_pass.Int8Value(43) = count
//messagebox "count of ""8"": " + count.ToString

/// byte(44)
position = charAt(pass_str, "B")// Instr()
min_val = Min(position * 8, 255)
sign_pass.Int8Value(44) = min_val
//messagebox "min of InStr(""B"")*3 between ""255"": " + min_val.ToString

/// byte(45)
count = CountOfStr(pass_str, "BAD")
sign_pass.Int8Value(45) = count
//messagebox "count of ""BAD"": " + count.ToString

//// byte(46)
position = charAt(pass_str, "3")// Instr()
count = CountOfStr(pass_str, "7", position)
min_val = Min(count, 12)
sign_pass.Int8Value(46) = min_val
//messagebox "min of InStr(""7"", InStr(""3"")) between ""12"": " + min_val.ToString

/// byte(47)
sign_pass.Int8Value(47) = 9   /// '\t'

/// byte(48)
sign_pass.Int8Value(48) = password.Length   //用户密码长度

/// byte(49)
str_tmp1 = MidStr(pass_str, 21, 1)
str_tmp2 = MidStr(pass_str, 203, 1)
str_tmp3 = MidStr(pass_str, 48, 1)
sum_val = str_tmp1.Asc + str_tmp2.Asc + str_tmp3.Asc
sign_pass.Int8Value(49) = sum_val
//MessageBox str_tmp1 + ", " + str_tmp2 + ", " + str_tmp3

/// byte(50)
position = charAt(pass_str, "11")// Instr()
if position > 0 then
sign_pass.Int8Value(50) = 1
else
sign_pass.Int8Value(50) = 0///默认就是0,可跳过
end if

/// byte(51)
sign_pass.Int8Value(51) = 42

/// byte(52)
sign_pass.Int8Value(52) = sign_pass.Int8Value(0)   /// byte(52) = MD5Bytes

/// byte(53)
sign_pass.Int8Value(53) = sign_pass.Int8Value(31)* 7/// byte(53) = serialBytes * 7

/// byte(54)
count = CountOfStr(pass_str, "D", 127)
sign_pass.Int8Value(54) = count

/// byte(55)
str_tmp1 = MidStr(pass_str, pass_str.Length-2, 1)
str_tmp2 = pass_str.Right(1)
position = charAt(pass_str, str_tmp2)
count = CountOfStr(pass_str, str_tmp1, position)
sign_pass.Int8Value(55) = count
//MessageBox str_tmp1 + ", " + str_tmp2 + ", pos = " + position.ToString + ", count = " + count.ToString

/// byte(56) ~ byte(63)
position = charAt(pass_str, "F")
count = CountOfStr(pass_str, "C", 1)
pow_val = Pow(position, count)
sign_pass.DoubleValue(56) = pow_val

var sign_pass_string as String = sign_pass.StringValue(0, 64)

///enrypted signature_verify_part
var sign_verify_data as MemoryBlock = new MemoryBlock(sign_verify_str_hex.Length)
sign_verify_data.StringValue(0, sign_verify_str_hex.Length) = sign_verify_str_hex

////加密
var bf as _Blowfish = new _Blowfish(sign_pass_string)
//MessageBox "sign_verify_str_hex.length = " + sign_verify_str_hex.Length.ToString

var data0 as MemoryBlock = new MemoryBlock(sign_verify_str_hex.Length)
data0.StringValue(0, sign_verify_str_hex.Length) = sign_verify_str_hex
var data as MemoryBlock = new MemoryBlock(sign_verify_str_hex.Length)
data.StringValue(0, sign_verify_str_hex.Length) = sign_verify_str_hex
data.LittleEndian = False
bf.Encipher(data)///加密

//// 256
var xml_signature as String = ""
var encrypted_signature as String
if data <> nil then
//MessageBox encrypt_verify_data.Size.ToString
encrypted_signature = EncodeHex(data.StringValue(0, data.Size), false)
xml_signature = pass_str + encrypted_signature
//MessageBox "encryped signature: "+ crlf + encrypted_signature
//MessageBox "signature ok!!!"
else
MessageBox "XML Signature is null"
exit sub
end if

////// debug
////taLicenseKeyContent.Text = xml_signature


/////==================================================================================================================
///// 生成 XML 文件内容
if encrypted_signature = "" then
MessageBox "XML Signature is empty."
exit sub
end if

if encrypted_signature.Length <> 512 then
MessageBox "Ecrypted Signature String Length not equal 512"
exit sub
end if

//if pass_str.Length <> 512 then
//MessageBox "Password Base String Length not equal 512"
//exit sub
//end if

//// XML 文件
var xml_stras String = "<?xml version=""1.0"" encoding=""UTF-8""?>" + EndOfLine.Unix +"<key>" + EndOfLine.Unix + "</key>"

var xml as XmlDocument = new XmlDocument()
xml.PreserveWhitespace = true
xml.LoadXml(xml_str)
var xml_element as XmlElement = xml.DocumentElement
var n as Integer = xml_element.ChildCount

var serialNode as XmlTextNode = xml.CreateTextNode(serial_xml)
xml_element.AppendChild(xml.CreateElement("serial")).AppendChild(serialNode)

var addressNode as XmlTextNode = xml.CreateTextNode(address)
xml_element.AppendChild(xml.CreateElement("address")).AppendChild(addressNode)

var emailNode as XmlTextNode = xml.CreateTextNode(email)//// password
xml_element.AppendChild(xml.CreateElement("email")).AppendChild(emailNode)

var signatureNode as XmlTextNode = xml.CreateTextNode(xml_signature)
xml_element.AppendChild(xml.CreateElement("signature")).AppendChild(signatureNode)

////// debug
///taLicenseKeyContent.Text = xml.ToString

/////==================================================================================================================
///// 生成 最终加密的 XML 文件内容
//加密
var sData as String = xml.ToString.ReplaceLineEndings(EndOfLine.Unix)

while ((sData.Length mod 8) <> 0)
sData = sData + " "
wend

var mbData as MemoryBlock = new MemoryBlock(sData.Length)

mbData.LittleEndian = False

mbData.StringValue(0, sData.Length) = sData

// BlowFishEncrypt(publicKey As String, data As MemoryBlock, blockMode As Crypto = Crypto, initializationVector As MemoryBlock) As MemoryBlock
var encryptData as MemoryBlock = Crypto.BlowFishEncrypt(xml_cryptoKey, mbData,Crypto.blockModes.ECB, Nil)

var encryptedXML as string = EncodeHex(encryptData.StringValue(0, encryptData.Size-8), False)///// 需要去除 XML 加密后多出来的8个padding字符,否则XMLDocument会出错

////// show license key
taLicenseKeyContent.Text = encryptedXML

////
MessageBox "Generate License Key Success!!!!!"



主界面如下:


下面附上激活服务器和注册机的工程源码:
激活服务器:
注册机源码:

----- game over -----

solly 发表于 2022-8-15 14:43

本帖最后由 solly 于 2022-8-15 14:54 编辑

wangwh27 发表于 2022-8-15 14:23
注册机源码运行报错了
1、你的注册表中生成了这个UUID没有,或以管理员权限运行一次这个IDE看看。


2、还有就是可能有一些杀毒软件什么的,禁止了注册表的写入或读取。

3、最后看看 build settings 中的 architecture 是不是选的 "x86 64-bit"。

solly 发表于 2022-8-15 16:11

本帖最后由 solly 于 2022-8-15 16:26 编辑

wangwh27 发表于 2022-8-15 15:01
以管理员权限运行这个IDE后,注册表依然没有这个UUID。
那这个就不晓得了,可能是IDE 的问题吧,你可以手工创建一个吧,UUID结构如下:
ComputerUUID:
char MacAddress;
Double   dateTotalSeconds;
Double   runtimeMicroseconds;
Double   random;
}

MacAddress 是去掉了“:”号或“-”号的字符串,加上后面3个double,整个UUID是36字节。

然后整个结构置入MemoryBlock后进行 base64 编码。
把编码后的字符串写入 UUID 即可。

UUID的生成是一次性,只要有UUID,IDE就直接读取,不会再次生成了。

IDE 生成 UUID 的位置如下图所示:

longbbyl 发表于 2022-8-15 10:25

奈何如次优秀的逆向,赏心悦目

Hmily 发表于 2022-8-15 10:33

好久没见大佬发帖了,赞!

lszc87 发表于 2022-8-15 10:35

我的妈呀,这也太详细了

KingXL 发表于 2022-8-15 10:39

{:300_938:}太厉害了,已get了,感谢分享经验。

平淡 发表于 2022-8-15 10:45

辛苦了,这么详细!免费评分致敬先!

平淡 发表于 2022-8-15 10:46

今日机会暂无,零点过后再来!

MorichikaRinno 发表于 2022-8-15 10:49

太厉害了,谢谢大佬。

homehome 发表于 2022-8-15 11:29

中断在如下位置(先下好断点,再打开授权文件)

这个中断怎么来的?

solly 发表于 2022-8-15 11:36

homehome 发表于 2022-8-15 11:29
中断在如下位置(先下好断点,再打开授权文件)

这个中断怎么来的?

前期调试多次找到的,后面也説了,class 初始化时,可以看到这些类的方法名,通过名字一看就知道了。
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 详细逆向分析一款非常零类的全平台应用开发工具的授权和激活过程