某Excel全栈解决方案 逆向分析【后端组件篇】

查看 142|回复 11
作者:pjy612   
前情提要
收到个消息推送,说是高性能Excel组件。然后一搜软文还不少,不知道是否真材实料。
然后 貌似还有配套的全栈解决方案...
那么应该可以拿来研究研究... 先从本职后端组件入手吧~
嘉宾介绍
aHR0cHM6Ly93d3cuZ3JhcGVjaXR5LmNvbS9kb2N1bWVudHMtYXBpLWV4Y2Vs
准备工作
aHR0cHM6Ly93d3cuZ3JhcGVjaXR5LmNvbS9kb2N1bWVudHMtYXBpLWV4Y2VsL2RvY3Mvb25saW5lL0xpY2Vuc2VJbmZvcm1hdGlvbi5odG1s
一般库 需要先看文档 看看要怎么注册 看看有啥限制。
总之 多看文档少走弯路  
Workbook.SetLicenseKey(" Your License Key");
var workbook = new Workbook("Your License Key");
写个demo 跑跑
private static void GCExcelTest()
{
    string license = $"";
    try
    {
        Workbook workbook = new GrapeCity.Documents.Excel.Workbook();
        IWorksheet worksheet = workbook.Worksheets[0];
        worksheet.Cells[0].Value = "Hello word";
        workbook.Save(@"gcexcel.xlsx");
        workbook = new GrapeCity.Documents.Excel.Workbook(license);
        worksheet = workbook.Worksheets[0];
        worksheet.Cells[0].Value = "Hello word";
        workbook.Save(@"gcexcel2.xlsx");
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }
}
没License 会提示异常。


1.png (80.59 KB, 下载次数: 0)
下载附件
2023-4-10 13:41 上传

然后 未授权会有明显的水印。


2.png (97.28 KB, 下载次数: 0)
下载附件
2023-4-10 13:41 上传

分析开始
先从 dnspy 定位到 GrapeCity.Documents.Excel.Workbook.SetLicenseKey
public static void SetLicenseKey(string key)
{
        Assembly callingAssembly = Assembly.GetCallingAssembly();
        //预处理 但是多了个函数可以先看看
        if (string.IsNullOrEmpty(key) || !Workbook.a(key, callingAssembly))
        {
                throw new ArgumentException(GrapeCity.Documents.Excel.Resources.a.ez());
        }
        bool flag = false;
        try
        {
                //核心验证应该在这里面
                amj.j(key, callingAssembly);
        }
        catch
        {
                flag = true;
        }
        //下面都是异常
        if (amj.c2())
        {
                throw new ArgumentException(GrapeCity.Documents.Excel.Resources.a.ey());
        }
        if (flag || amj.c3() == 0)
        {
                throw new ArgumentException(GrapeCity.Documents.Excel.Resources.a.ez());
        }
}
先看上面那个 预处理函数
internal static bool a(string A_0, Assembly A_1)
{
        return !amj.l(A_0) || amj.j(A_1);
}
//amj.l(A_0)
internal static bool l(string A_0)
{
        return ang.f.a(A_0);
}
//amj.j(A_1)
internal static bool j(object A_0)
{
        return ang.f.a(A_0);
}
好像 预处理 都在 一个 ang.f 里面
//amj.l(A_0)
internal bool a(string A_0)
{
        return A_0 != null && A_0.StartsWith("GrapeCity-Internal-License,", StringComparison.Ordinal);
}
//amj.j(A_1)
internal bool a(object A_0)
{
        Assembly assembly = A_0 as Assembly;
        if (A_0 == null)
        {
                return false;
        }
        byte[] publicKeyToken = assembly.GetName().GetPublicKeyToken();
        ulong num = 0UL;
        if (publicKeyToken == null)
        {
                return false;
        }
        for (int i = 0; i
邪道解法
啊咧咧?
大概意思就是 key 如果是 GrapeCity-Internal-License 开头的话 并且 调用的 Assembly 不是 指定范围内的,则会触发异常...
那么搜一下?


3.png (38.6 KB, 下载次数: 0)
下载附件
2023-4-10 13:41 上传

哦豁?!有内部用的注册码耶~( •̀ ω •́ )y
那我们简单 Hook 下?(当然爆破也行,但是一般 Nuget 的Lib类不推荐直接操刀Dll  


4.png (77.75 KB, 下载次数: 0)
下载附件
2023-4-10 13:41 上传

啊这...直接过了?水印也没了?!
还真就是 解毒剂往往长在毒药旁边
回归正途
//amj.j(key, callingAssembly)
internal static void j(string A_0, object A_1)
{
        ang.f.a(A_0, A_1);
}
//ang.f.a(A_0, A_1);
public void a(string A_0, object A_1)
{
        this.a.a(A_1);
        this.a.at7(A_0);
}
//private readonly ang.a a;
//this.a.a(A_1);
public void a(object A_0)
{
        this.h = A_0; //这里存的是 Assembly 估计是是有什么 程序集 匹配和产品什么的校验
}
//this.a.at7(A_0);
public void at7(string A_0) //这里是 str  所以 注册码是在这里面
{
        if (A_0 != this.a)
        {
                this.a = A_0;
                if (this.g != null) //有个 g 类型是 EventHandler
                {
                        this.g(this, EventArgs.Empty);
                }
        }
}
//private EventHandler g;
我们来查下 g 的调用


5.png (50.85 KB, 下载次数: 0)
下载附件
2023-4-10 13:41 上传

Σ(っ °Д °;)っ  竟然没有被用到么?那肿么办?!  
别急 看到子类上面那个 private class a : sg sg 了么?
internal interface sg
{
        // Token: 0x0600316C RID: 12652
        [CompilerGenerated]
        void at4(EventHandler A_0);
        // Token: 0x0600316D RID: 12653
        [CompilerGenerated]
        void at5(EventHandler A_0);
        // Token: 0x0600316E RID: 12654
        string at8();
        // Token: 0x0600316F RID: 12655
        string at9();
        // Token: 0x06003170 RID: 12656
        string at6();
        // Token: 0x06003171 RID: 12657
        void at7(string A_0);
        // Token: 0x06003172 RID: 12658
        object aua();
}
继承了 接口,那么 要查引用就要通过接口去查。。。


6.png (69.94 KB, 下载次数: 0)
下载附件
2023-4-10 13:42 上传

咿哈!逮到你了!
然后顺便看看 sf 的初始化在哪
//GrapeCity.Documents.Excel.ang.a.a() : void @060061B1
public a()
{
        this.b = new sf(this, new Func(ang.a.c.9.b), new Func(ang.a.c.9.a), new Action>(this.a), new Func(ang.a.c.9.a), new Func(ang.a.c.9.a));
}
里面有不少回调 感觉可以整理替换下 方便之后回顾
public sf(sg A_0, Func A_1, Func A_2, Action> A_3, Func A_4, Func A_5)
{
        this.b = A_0; //GrapeCity.Documents.Excel.ang.a 的实例
        this.b.at4(new EventHandler(this.a));
        this.c = A_1; //return new sm("Sample", "A0");
        this.d = A_2; //return new so("wE+VWE4exHP+ieziZg+Cgf7sJslBhVzJbPXZQwfGUfU27NqODPzCpizjAPz6NnKw8GCiHpug6D+bUxmutcBmUw==", "AQAB");
        this.e = A_3; //外部回调
        this.f = A_4; //内部类 验证 上面出现过
        this.g = A_5; //return A_0 != null && A_0.StartsWith("GrapeCity-Internal-License,", StringComparison.Ordinal);
}
然后我们来看看 之前 那个 g 的 逻辑。
//这逻辑 开始 内外回调 绕起来了~
// this.b.at4(new EventHandler(this.a));
private void a(object A_0, EventArgs A_1)
{
        sf.b b = new sf.b();
        b.a = this;
        if (!this.a(out b.b))
        {
                b.b = this.a(this.b.at6(), this.c('c').a());
                if (this.d('s').a(string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", b.b.e(), b.b.f(), (b.b.a() == null) ? "" : b.b.a().a()), b.b.b()))
                {
                        this.e(this.a(b.b.a()), new Func(b.c));
                }
        }
}
// if (!this.a(out b.b))
private bool a(out si A_0)
{
        return sf.a.TryGetValue(this.b.at8(), out A_0) && A_0 != null && !string.IsNullOrEmpty(A_0.b()) && A_0.a() != null && A_0.a().b().Any(new Func(this.b));
}
// sf.a -> private static readonly ConcurrentDictionary a = new ConcurrentDictionary();
// this.b.at8() ->  GrapeCity.Documents.Excel.ang.a.at8 -> return "WU5D";
// 首次调用的话 肯定返回的是 false 所以可以略过
//然后继续看 b.b = this.a(this.b.at6(), this.c('c').a());
// this.b.at6() ->  GrapeCity.Documents.Excel.ang.a.at6 -> return 注册码;
// this.c('c').a() -> sm("Sample", "A0").a() -> #A0
internal class sm
{
        public sm(string A_0, string A_1 = "A0")
        {
                this.a = A_0;
                this.b = "#" + A_1;
        }
        public string a()
        {
                return this.b;
        }
        //...
}
//b.b = this.a(this.b.at6(), this.c('c').a());
//A_0 注册码
//A_1 #A0
private si a(string A_0, string A_1)
{
        if (string.IsNullOrEmpty(A_0))
        {
                throw new sl(GrapeCity.Documents.Excel.Resources.a.ez());
        }
        int num = A_0.IndexOf(A_1, StringComparison.OrdinalIgnoreCase);
        if (num == -1)
        {
                throw new sl(GrapeCity.Documents.Excel.Resources.a.ez());
        }
        string text = A_0.Substring(0, num);
        string text2 = A_0.Substring(num + A_1.Length);
        //通过 #A0 去分割注册码,所以 码的本体在 #A0 后面
        si si = this.c('c').a(text2).a();
        si.d(A_1);
        si.c(text);
        return si;
}
//this.c('c').a(text2)


7.png (21.43 KB, 下载次数: 0)
下载附件
2023-4-10 13:42 上传



8.png (37.91 KB, 下载次数: 0)
下载附件
2023-4-10 13:42 上传

接着就到了


9.png (27.56 KB, 下载次数: 0)
下载附件
2023-4-10 13:42 上传

这感觉不就来了嘛~
internal static si a(this string A_0)
{
        if (string.IsNullOrEmpty(A_0))
        {
                return null;
        }
        si si = new si(); //鉴权实体类
        qi qi = pr.a(A_0) as qi; //JSON解析器
        if (qi == null)
        {
                return null;
        }
        si.a(qi.a("_s"));
        si.b(qi.a("S"));
        ql ql = qi.a("D");
        if (ql == null)
        {
                si.a(null);
        }
        else
        {
                sj sj = new sj();
                sj.f(ql.a("Id"));
                sj.a(ql.a("Evl"));
                sj.e(ql.a("OId"));
                sj.d(ql.a("CNa"));
                sj.c(ql.a("CId"));
                sj.b(ql.a("Dms"));
                sj.a(ql.a("Ips"));
                string text = ql.a("Exp");
                if (!string.IsNullOrEmpty(text))
                {
                        sj.a(new DateTime?(DateTime.ParseExact(text, "yyyyMMdd", CultureInfo.InvariantCulture)));
                }
                sj.a(DateTime.ParseExact(ql.a("Crt"), "yyyyMMdd hhmmss", CultureInfo.InvariantCulture));
                qe qe = ql.a("Prd");
                object obj = ql.a("Anl");
                if (obj != null)
                {
                        qi qi2 = pr.a(obj.ToString()) as qi;
                        if (qi2 != null && qi2.a("dsr") != null)
                        {
                                sj.a(new bool?(qi2.a("dsr")));
                        }
                }
                sj sj2 = sj;
                sk[] array;
                if (qe != null)
                {
                        array = qe.ToArray().Select(q=>{
                                sk sk = new sk();
                                sk.b(q.a("N"));
                                sk.a(q.a("C"));
                                return sk;
                        }).ToArray();
                }
                else
                {
                        array = null;
                }
                sj2.a(array);
                si.a(sj);
        }
        return si;
}
上面取值部分有兴趣的可以自己分析看看,这里不多阐述,直接说结果和必填项。
最后Json 类似
{
    "S":"sign",
    "D":{
        "Id":"52pojie",
        "Prd":[
            {
                "N":"",
                "C":""
            }
        ],
        "Crt":"日期"
    }
}
注册码本体雏形有了 我们继续看之后的逻辑
// this.d('s').a() -> so("wE+VWE4exHP+ieziZg+Cgf7sJslBhVzJbPXZQwfGUfU27NqODPzCpizjAPz6NnKw8GCiHpug6D+bUxmutcBmUw==", "AQAB").a() -> RSA鉴权
internal class so
{
        // Token: 0x060031AD RID: 12717 RVA: 0x001EE44C File Offset: 0x001EC64C
        public so(string A_0, string A_1)
        {
                this.a.ImportParameters(new RSAParameters
                {
                        Modulus = Convert.FromBase64String(A_0),
                        Exponent = Convert.FromBase64String(A_1 ?? "AQAB")
                });
        }
        // Token: 0x060031AE RID: 12718 RVA: 0x001EE4A1 File Offset: 0x001EC6A1
        public bool a(string A_0, string A_1)
        {
                //使用的 系统函数鉴权 后续直接Hook
                return !string.IsNullOrEmpty(A_1) && this.a.VerifyData(Encoding.UTF8.GetBytes(A_0), "SHA1", Convert.FromBase64String(A_1));
        }
        // Token: 0x04001503 RID: 5379
        private readonly RSACryptoServiceProvider a = new RSACryptoServiceProvider();
}
RSA 解决后就剩 最后一个函数了,但是依旧挺绕的
//this.e(this.a(b.b.a()), new Func(b.c));
//b.b.a() 这个是 JSON中 D 字段对象 sj
//this.a(b.b.a())
private bool a(sj A_0)
{
        if (A_0 == null) //D 不为空
        {
                return false;
        }
        if (A_0.b() == null) // D.Prd 不为空
        {
                return false;
        }
        /*
        //new Func(this.a)
        private bool a(sk A_0)
        {
                return A_0.a() == this.b.at8();//还记得这个 at8 吗 对应 WU5D
        }
        */
        if (A_0.b().Any(new Func(this.a)))// 判断 D.Prd 中 C 是否有对应值,GcExcel 的是 WU5D
        {
                //下面的基本都不用设 默认为true
                return
                (A_0.d() == null || (A_0.d().Value - DateTime.Now.Date).Days > 0) //有没有到期时间,有的话验证
                &&
                !A_0.a().GetValueOrDefault(false) //Anl.dsr 有没有值什么的 默认没有的
                &&
                ((string.IsNullOrEmpty(A_0.f()) && string.IsNullOrEmpty(A_0.e())) //域名什么的验证
                || sf.a(this.b.at9(), A_0.f(), A_0.e(), A_0.a() == null)
                );
        }
        if (!this.g(this.b.at6()))//这里是判断注册码 开头是不是内部码
        {
                return false;
        }
        object obj = this.b.aua();//程序集是不是内部程序集
        return this.f(obj);
}
// this.b.at9() ->  GrapeCity.Documents.Excel.ang.a.at9 -> return "localhost";
晕了没?没晕就继续。
接着是 new Func(b.c)
//b.c
internal sh c()
{
        return this.a.a(this.b);
}
//this.a.a(this.b);
private sh a(si A_0)
{
        string text = A_0.a(this.b.at8()); //还是验证 Prd 里面 是否包含 WU5D
        string text2 = A_0.e(); //注册码 后半段
        //如果 Evl 为true 就是 Evaluation 模式 否则 Licensed,反正咱们没填
        LicenseMode licenseMode = ((A_0.a() == null) ? LicenseMode.Unlicensed : (A_0.a().j() ? LicenseMode.Evaluation : LicenseMode.Licensed));
        bool flag;
        int? num = A_0.a(out flag); //过期时间 没填
        sf.a a = new sf.a();
        a.a(text);
        a.b(text2);
        a.a(licenseMode);
        a.a(flag);
        a.a(num);
        a.a(A_0.d() ?? new string[0]);
        sf.a a2 = a;
        if (A_0.a().a() != null)
        {
                a2.a(A_0.a().a()); //Anl.dsr 反正没填
        }
        return a2;
}
终于又到 之前 e 那个回调了。。。
//上面的 this.e = A_3 那个回调
private void a(bool A_0, Func A_1)
{
        this.c = A_1();
        if (!A_0) //因为 A_0 是 true 所以 不走这些
        {
                if (this.c.y0() == LicenseMode.Unlicensed)
                {
                        this.c = null;
                        return;
                }
                if (string.IsNullOrEmpty(this.c.y3()))
                {
                        this.c = null;
                        this.f = true;
                        return;
                }
                if (this.c.y1())
                {
                        this.e = new bool?(true);
                        return;
                }
                this.c = null;
                return;
        }
        else
        {
                if (this.c.y2() != null) //过期时间 实际咱们没有 所以 跳过
                {
                        this.d = DateTime.Today.AddDays((double)this.c.y2().Value);
                        this.e = null;
                        return;
                }
                this.e = new bool?(false); // 看来这个为 false 反而是真解
                return;
        }
}
开工
分析完了 可以开始动工了
internal static string GetLicense(Type t)
{
    string name = "52pojie";
    var temp = new
    {
        S = Convert.ToBase64String(Encoding.UTF8.GetBytes("0")),
        D = new
        {
            Id = name,
            Prd = new[]
            {
                new {N = "WU5D", C = "WU5D"}
            },
            Crt = DateTime.Now.ToString("yyyyMMdd hhmmss", CultureInfo.InvariantCulture)
        }
    };
    string json = JsonConvert.SerializeObject(temp);
    //还记得之前那个盲猜的 encode吗?调用它试试~
    string lic = (string)t.Assembly.GetType("GrapeCity.Documents.Excel.sp").GetMethod("g").Invoke(null, new object?[] { json }); ;
    return $"{name}#A0{lic}";
}
Hook 部分


10.png (110.04 KB, 下载次数: 0)
下载附件
2023-4-10 13:42 上传

结果


11.png (51.31 KB, 下载次数: 0)
下载附件
2023-4-10 13:42 上传



12.png (59.48 KB, 下载次数: 0)
下载附件
2023-4-10 13:42 上传

完美收工~
注意事项
仅限于学习交流,请勿用于商业或非法用途
附录
有兴趣的 同学可以解析内部注册码 能少走一些弯路  (^U^)ノ~YO

下载次数, 下载附件

pjy612
OP
  


木木小白 发表于 2023-4-13 10:33
我一般都是暴力破解的。。。

如果是 exe 啥的 能爆破咱肯定也爆破啦~一时半会儿也不会去更新它。
lib之类的 后面如果要更新啥的,或者换编译环境或框架。
当然还是写hook通杀方便啊~
pjy612
OP
  


kuoniya 发表于 2023-4-12 19:04
上次看的时候还要求那啥是80,感谢作者

哦 因为 之前 级别低的时候 发帖子 不能存草稿,所以排版啥的 要慢慢调。
就先弄个80权限,改好了 自己看着差不多可以对外了。
就会把 权限和标题 改对。。。
咱这个又不是站务帖子 肯定得放出来给大家看的。。。
pjy612
OP
  

坐个沙发~
既然有后端篇 那肯定还有个前端篇~
免费评分点一下~ 加速出文哟~
文件夹已经建好了!
cfc0699   

谢谢楼主分析这么透彻。
dragon_00   

没晕,从头看到尾,好好学习一下【实际上很晕了】。
haduke   

学习一下。
zhuhuaicheng   

楼主水平可以啊,先学习一下
cheyue380   

感谢分享,学习一下。
wzz2690   

看晕了,多学习一下
您需要登录后才可以回帖 登录 | 立即注册