某宝石盒子 系列组件 许可证分析

查看 129|回复 9
作者:pjy612   
某宝石盒子 系列组件 许可证分析
前情提要
手头的事儿搞得差不多了,项目从 Win8.1 迁移到 UWP, Webview 升 Webview2 踩了好多坑...
之后 UWP 好像 还得再迁... QAQ ...
于是放松放松 再来水个帖子...
唉... 虽然感觉这个好像也达不到精华的程度 囧...  
嘉宾介绍
还是某个和 Office 相关的系列组件,因为 Nuget 上就这类多  
官网
aHR0cHM6Ly93d3cuZ2VtYm94c29mdHdhcmUuY29tL2J1bmRsZQ==
示例库介绍
aHR0cHM6Ly93d3cuZ2VtYm94c29mdHdhcmUuY29tL2RvY3VtZW50L2V4YW1wbGVzL2dldHRpbmctc3RhcnRlZC84MDE=
示例库文档
aHR0cHM6Ly93d3cuZ2VtYm94c29mdHdhcmUuY29tL2RvY3VtZW50L2RvY3MvZXZhbHVhdGlvbi1hbmQtbGljZW5zaW5nLmh0bWw=
准备工作
先来个能触发鉴权的 Demo。
GemBox.Document.ComponentInfo.SetLicense("FREE-LIMITED-KEY");
var document = new GemBox.Document.DocumentModel();
var section = new GemBox.Document.Section(document);
document.Sections.Add(section);
for (int j = 0; j
会触发 超过免费页面额度的错误。  
开工
先通过引用直接定位到 dll ,看一眼目标函数 SetLicense
// Token: 0x06009BD1 RID: 39889 RVA: 0x002F256C File Offset: 0x002F076C
public static void SetLicense(string serialKey)
{
        try
        {
                ComponentInfo.\u0003.\u000F(Regex.Replace(serialKey, \u0003\u0018\u001A\u0003.\u0005\u0002(2011419674), string.Empty));
        }
        catch (\u0005\u0005\u0002\u0003 u0005_u0005_u0002_u)
        {
                throw new \u0002\u0005\u000F\u0002(u0005_u0005_u0002_u.Message);
        }
}
拿de4dot 去个混淆看看。
发现字符串并没有自动解密,看来要手动解密
解密函数是  
// Token: 0x060025E7 RID: 9703 RVA: 0x000C5800 File Offset: 0x000C3A00
[MethodImpl(MethodImplOptions.NoInlining)]
internal static string \u0005\u0002(int \u0002)
{
        string text;
        if (\u0003\u0018\u001A\u0003.\u0002.TryGetValue(\u0002, out text))
        {
                return text;
        }
        return \u0003\u0018\u001A\u0003.\u0008\u0002(\u0002, true);
}
结果


1.png (35.83 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传

看来这里面 可能有循环调用。
我们可以用反射自己手动解密  
void decodeString(int i)
{
    Type type = AccessTools.GetTypesFromAssembly(typeof(GemBox.Document.ComponentInfo).Assembly).First(t => t.Name == "\u0003\u0018\u001A\u0003");
    string invoke = (string) AccessTools.Method(type,"\u0005\u0002", new[] {typeof(int)}).Invoke(null, new object[] {i});
    Console.WriteLine($"{i}:{invoke}");
}
new[]
{
    2011419674, // \s+
}.ToList().ForEach(decodeString);
也可以试着看看别的环境里的库能不能解,比如这个库默认使用的是netstandard2.0 旁边还有个 net35 和 net6.0-windows7.0 的环境。
咱们解下 net35 吧~
发现成功... 有时候换条路其实也行的 当然 硬啃 就像上面自己慢慢调试了...
咱们 继续看 去混淆后的函数。


10.png (21.77 KB, 下载次数: 0)
下载附件
2023-5-16 17:53 上传

PS. 这里可以使用 dnspy 的功能 修改字段名 方便 阅读代码。
比如 我们可以将 this.string_0 这里名称 改成 _license 反正自己看看 怎么舒服怎么来。
接着老样子,查引用,查读取


2.png (97.55 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传

// Class333
// Token: 0x060009A8 RID: 2472 RVA: 0x000DC678 File Offset: 0x000DA878
// 上层入参
//string_1 DN
//string_2 2023May09
//
public bool method_1(string string_1, string string_2, Action action_0, ref int int_1, ref int int_2, ref int int_3)
{
        if (string.IsNullOrEmpty(this._license))
        {
                return false;
        }
        if (this.method_2()) // 这里 是检查是否是 某种旧格式的 LicenseKey
        {
                throw new Exception65("The serial key is not valid. You are probably using serial key for an older version of the product. Please renew your license to continue using this version, or use an older version.");
        }
        char c = this._license[this._license.Length - 1]; // license 最后一位 是 A 或者 B
        if (c == 'A') // 所以 可用 License 有两种 A或B 结尾
        {
                return this.method_3(string_1, string_2, action_0, ref int_1, ref int_2, ref int_3);
        }
        if (c == 'B')
        {
                return this.method_4(string_1, string_2, action_0, ref int_1, ref int_2, ref int_3);
        }
        if (c != 'Y') //这里为什么是个Y? 因为 默认的 免费Key FREE-LIMITED-KEY 结尾是Y...
        {
                this.method_7();
                return false;
        }
        return this.method_13(action_0, ref int_1, ref int_2, ref int_3);
}
// Class333
// Token: 0x060009A9 RID: 2473 RVA: 0x0003987C File Offset: 0x00037A7C
[MethodImpl((MethodImplOptions)256)]
private bool method_2()
{
        //private static readonly Regex regex_0 = new Regex("[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}");
        return Class333.regex_0.IsMatch(this._license);
}
然后来看 method_3 和 method_4 的逻辑


3.png (35.37 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传

private void method_10(string string_1) // 入参是否过期 所以 我们
{
        DateTime dateTime = DateTime.ParseExact(string_1, "yyyyMMMdd", CultureInfo.InvariantCulture);
        if (DateTime.Now > dateTime)
        {
                throw new Exception65("The serial key has expired.");
        }
}
所以 推测 method_4(B) 是 限时许可。着重看 method_3(A)即可。


4.png (26.58 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传

private bool method_3(string string_1, string string_2, Action action_0, ref int int_1, ref int int_2, ref int int_3)
{
        if (this._license.Length


5.png (12.54 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传



6.png (19.46 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传

PS.根据黑名单 可以看出 有一些 特殊的前缀 SN,DN,PN,AN,EN 可以直观看出 许可证的 前18位 的格式
最后就剩一个
//string_1 前18
//string_2 之后但排除最后一位
private void method_11(string string_1, string string_2, Action action_0, ref int int_1, ref int int_2, ref int int_3)
{
        Class333.Class335 home.php?mod=space&uid=341152 = new Class333.Class335();
        @class.class333_0 = this;
        @class.string_0 = string_1;
        @class.string_1 = string_2;
        action_0(@class.string_1.Length > 20);
        @class.int_1 = Class333.random_0.Next(111, 12567);
        @class.int_2 = this.method_12(@class.int_1);
        @class.int_0 = 0;        
        //然后看 Class335 的 method_0 method_1
        Class2886.smethod_0(new Action(@class.method_0), new Action(@class.method_1));
        int_1 = @class.int_1;
        int_2 = @class.int_2;
        int_3 = @class.int_0;
}
// Class333.Class335
// Token: 0x060009B8 RID: 2488 RVA: 0x000DC9B8 File Offset: 0x000DABB8
internal void method_0(int int_3)
{
        this.class333_0.method_5(this.string_0, this.string_1);
        int num = ((this.string_0.Length > 10) ? 59 : 58);
        this.int_0 = int_3 * this.int_1 + num * this.int_2;
}
// Class333.Class335
// Token: 0x060009B9 RID: 2489 RVA: 0x00039924 File Offset: 0x00037B24
internal void method_1()
{
        this.class333_0.method_5(this.string_0, this.string_1);
}
然后 又回到 Class333
// Class333
// Token: 0x060009AC RID: 2476 RVA: 0x0003988E File Offset: 0x00037A8E
[MethodImpl((MethodImplOptions)256)]
private void method_5(string string_1, string string_2)
{
        //最后就是这儿了
        if (!Class1396.smethod_0(string_1, string_2))
        {
                this.method_7();
        }
}
// Class333
// Token: 0x060009AE RID: 2478 RVA: 0x0003989F File Offset: 0x00037A9F
[MethodImpl((MethodImplOptions)256)]
private void method_7()
{
        throw new Exception65("The serial key is not valid.");
}
// Class1396
// Token: 0x06003D8C RID: 15756 RVA: 0x001B0D68 File Offset: 0x001AEF68
public static bool smethod_0(string string_0, string string_1)
{
        byte[] bytes = Encoding.UTF8.GetBytes(string_0); //前18
        byte[] array;
        try
        {
                array = Convert.FromBase64String(string_1); //之后的
        }
        catch (FormatException)
        {
                return false;
        }
        bool flag;
        try
        {
                //系统内置函数鉴权,看了那么多帖子知道怎么弄了吧?
                ECDsaCng ecdsaCng = new ECDsaCng(Class1396.smethod_1("IGTj5fnJc5wIeoub9ZkjZMpK21mNA1tU5k/ATtPka0lHIGVjywokrgVI3/AKhNmtcsTq+4v/suEbAElXEI2D8EtXAA=="));
                try
                {
                        ecdsaCng.HashAlgorithm = CngAlgorithm.Sha256;
                        flag = ecdsaCng.VerifyData(bytes, array); //ECDsaCng 鉴权 推出格式 19-倒数第二 为 sign,实际Hook的话都不用管
                }
                finally
                {
                        ((IDisposable)ecdsaCng).Dispose();
                }
        }
        catch (Exception ex)
        {
                if (!(ex is PlatformNotSupportedException) && !(ex is NotImplementedException) && !(ex is NotSupportedException))
                {
                        throw;
                }
                //如果系统不支持 ECDsaCng 走另一种鉴权模式 这里就不分析了...
                flag = Class2345.smethod_0(string_0, string_1);
        }
        return flag;
}
// Class1396
// Token: 0x06003D8D RID: 15757 RVA: 0x001B0E10 File Offset: 0x001AF010
private static CngKey smethod_1(string string_0)
{
        MemoryStream memoryStream = new MemoryStream();
        CngKey cngKey;
        try
        {
                memoryStream.Write(Class1396.byte_0, 0, Class1396.byte_0.Length);
                MemoryStream memoryStream2 = new MemoryStream(Convert.FromBase64String(string_0));
                try
                {
                        byte[] array = memoryStream2.smethod_3();
                        byte[] array2 = memoryStream2.smethod_3();
                        memoryStream.Write(array, 0, array.Length);
                        memoryStream.Write(array2, 0, array2.Length);
                }
                finally
                {
                        ((IDisposable)memoryStream2).Dispose();
                }
                cngKey = CngKey.Import(memoryStream.ToArray(), CngKeyBlobFormat.EccPublicBlob);
        }
        finally
        {
                ((IDisposable)memoryStream).Dispose();
        }
        return cngKey;
}
从上面就可以得出 当前Demo的库 许可证格式为
DN-日期-N个字符补到18长度 追加不定长的Base64(sign) 最后一位是 A
生成许可证
public enum GemBoxProduct
{
    [Description("DN")] Document,
    [Description("SN")] Spreadsheet,
    [Description("AN")] Pdf,
    [Description("PN")] Presentation,
    [Description("EN")] Email,
    [Description("IN")] Imaging
}
private static string GetDescriptionByEnum(Enum enumValue)
{
    var value = enumValue.ToString();
    var field = AccessTools.Field(enumValue.GetType(), value);
    var objs = field.GetCustomAttributes(typeof(DescriptionAttribute), false);
    if (objs.Length == 0)
        return value;
    var descriptionAttribute = (DescriptionAttribute) objs[0];
    return descriptionAttribute.Description;
}
internal static string GetLicense(GemBoxProduct code)
{
    var productCode = GetDescriptionByEnum(code);
    string head = $"{productCode}-{new DateTime(2999, 1, 1).ToString("yyyyMMMdd", CultureInfo.InvariantCulture)}-".PadRight(18,'A');
    var sign = Convert.ToBase64String(new byte[50 - head.Length].ToArray());
    var key = head + sign + "A";
    return key;
}


7.png (32.05 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传

Key的长度看来是够了。
之后只需要加点魔法。  
无名小银,如果您要查看本帖隐藏内容请回复
看看效果


9.png (26.99 KB, 下载次数: 0)
下载附件
2023-5-16 17:37 上传

声明
仅供学习交流,请勿用于商业或非法用途
附录
由于 该系列中各个库的 许可前缀 都是硬编码 不好反射,所以也只能自己硬编码了。
具体参考 上面代码中的 枚举 GemBoxProduct  

下载次数, 下载附件

pjy612
OP
  


沙发自己坐~
3yu3   

大佬又出新品了,看看大佬这次的新技能,学习学习!!
三年i   

66666,学习一下
pjy612
OP
  


3yu3 发表于 2023-5-17 11:25
大佬又出新品了,看看大佬这次的新技能,学习学习!!

没啥新技能 都是老东西。。。
icq168   

不错,谢谢分享,~~~~~~~~~
dnn19782001   

太好了又能学到新东西了
move   

easyDe4dot 分享一下
undead   

感谢楼主,学习下!
pangtouniao   

太专业了,必须支持
您需要登录后才可以回帖 登录 | 立即注册