吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2304|回复: 54
上一主题 下一主题
收起左侧

[.NET逆向] 某低代码平台 逆向分析(二)【客户端注册/发布/插件】

  [复制链接]
跳转到指定楼层
楼主
pjy612 发表于 2023-4-21 18:43 回帖奖励
本帖最后由 pjy612 于 2025-11-13 22:23 编辑

某低代码平台 逆向分析(二)【客户端注册/发布/插件】

前情提要

昨天不小心把VS给卸了。。。导致就算摸鱼想码文也不好贴图了 囧。。。
还好今儿看着 QA 没报啥 BUG 就继续码文吧~
书接上文
某低代码平台 逆向分析(一)【验证逻辑分析和实践】
(出处: 吾爱破解论坛)  

通过手动的方式把 服务端 授权先给弄了,离线注册也对上了。
那么现在就要来看看客户端相关功能是否正常了  

_(:3」∠)_ 这波弄完基本就真掏空了...  

嘉宾介绍

见上期 某低代码平台 逆向分析(一)【验证逻辑分析和实践】

准备工作

先进 客户端 目录看两眼
Forguncy 8\Website\designerBin
主程序是 Forguncy.exe 然后也有眼熟的 CommonUtilities.dll 先都备份一下,然后老样子扔 de4dot 里面去混淆。
加上 --dont-rename 免得有啥名称依赖。

处理完改个名 然后 跑跑看。


看来功能正常 开工。

客户端 注册码分析

先弄个假码试试

本能歪路子找触发

emmm 弹了个消息框,关键词 C# WinFrom,埋藏N年的血脉觉醒了。 无脑搜个 MessageBox


看来有好多 找个显眼的 比如 ShowMessageBox 然后 往里面看 看到
调用了 系统的 MessageBox.Show 在最靠内的 底层函数上 打个断点,然后 附加进程调试!

再点一下!

断下来了 是想要的内容,那就去看看堆栈。

先向上(工具是向下)找几个明显属于主程序的函数。
发现进的好像都是系统函数的,那就个挂断点,然后再重试一下假码。
好像 还是没有就只能往上多找找了。

这个 VerifyCodeValidatorWindow 看名称挺像 验证码 注册框的。
里面也有验证,不过 也许是 附加调试 调试器权限和控制有限
咱们再直接拿 dnspy 调试运行看看。

果然 这样就能明显定位到上一层了。
  

正常找文案

因为多语言的 所以肯定有语言包。
Forguncy 8\Website\designerBin\zh-CN
下面的 资源 DLL 都拖 dnspy 里面搜一下。


然后在用关键词 VerifyCode_Failed 搜一下。

就找到位置了。

找到验证码触发后就可以看逻辑了。

public static bool a(string A_0)
{
        string text;
        string text2;
        if (!Forguncy.q.a.a(A_0, out text, out text2)) //验证码的有效性
        {
                return false;
        }
        string text3 = string.Format("\"{0}\"", A_0.Replace("\"", "\\\""));
        bool flag = false;
        bool flag2;
        try
        {
                FileUtilities.CreateHiddeUACProcess("VerifyCodeActive.exe", text3);//写激活码
                flag = true;
                if (ResourceHelper.IsChinese())
                {
                        Forguncy.q.a(text, text2); //上传记录
                        ae.Instance.y().ActiveDesignerRequest(text);//GA Google那个埋点还是收集什么的。
                }
                if (ResourceHelper.IsChinese() || ResourceHelper.IsEnglish())
                {
                        CollaborationManager.Instance.InitGlobalSignature();
                }
                flag2 = flag;
        }
        catch (Exception ex)
        {
                TraceHelper.Write("active cn captcha failed.", true);
                TraceHelper.TraceException(ex, null, "WriteVerifyInfo");
                flag2 = flag;
        }
        return flag2;
}

//Forguncy.q.a.a(A_0, out text, out text2)
public static bool a(string A_0, out string A_1, out string A_2)
{
        A_1 = string.Empty;
        A_2 = string.Empty;
        if (string.IsNullOrEmpty(A_0))//非空
        {
                TraceHelper.Write("VerifyCode failed, value is null.", true);
                return false;
        }
        string[] array = null;
        bool flag;
        try
        {
                array = JsonUtilities.FromJsonString<string[]>(A_0); //注册码是个json数组
                goto IL_51;
        }
        catch (Exception ex)
        {
                TraceHelper.Write("VerifyCode failed, get json crashed.", true);
                TraceHelper.Write(A_0, true);
                TraceHelper.TraceException(ex, null, "VerifyCode");
                flag = false;
        }
        return flag;
        IL_51:
        if (array != null && array.Length == 2)
        {
                return Forguncy.q.a.a(array[0], array[1], out A_1, out A_2);//数组长度2
        }
        TraceHelper.Write("VerifyCode failed, json result invalid.", true);
        TraceHelper.Write(A_0, true);
        return false;
}

//Forguncy.q.a.a(array[0], array[1], out A_1, out A_2)
//A_0 
//A_1 
public static bool a(string A_0, string A_1, out string A_2, out string A_3)
{
        A_2 = string.Empty;
        A_3 = string.Empty;
        string[] array = A_0.Split(new char[] { '#' }, 2); //数组0部分 用 #  分隔
        if (array != null)
        {
                if (array.Length == 2)// # 分隔长度为2
                {
                        int num = 0;
                        if (!int.TryParse(array[1], out num) || !EmailValidator.EmailIsValid(array[0])) // 前部分为 email 之后为一个数字
                        {
                                return false;
                        }
                        if (!Forguncy.q.a.a())  // 这个 是判断 MD5 功能是否能用,不能用就直接返回了。
                        {
                                A_2 = array[0];
                                A_3 = array[1];
                                return true;
                        }
                        string text = null;
                        if (!Forguncy.q.a.a(A_0, ref text)) // 这里是 将 email#num 部分 Md5 成 Base64
                        {
                                return false;
                        }
                        bool flag = Forguncy.q.a.a(Forguncy.q.a.a, text, A_1); // RSA 验证
                        if (flag)
                        {
                                A_2 = array[0];
                                A_3 = array[1];
                        }
                        return flag;
                }
        }
        return false;
}
//Forguncy.q.a.a(A_0, ref text)
private static bool a(string A_0, ref string A_1)
{
        bool flag;
        try
        {
                HashAlgorithm hashAlgorithm = HashAlgorithm.Create("MD5");
                byte[] bytes = Encoding.UTF8.GetBytes(A_0);
                byte[] array = hashAlgorithm.ComputeHash(bytes);
                A_1 = Convert.ToBase64String(array);
                flag = true;
        }
        catch (Exception ex)
        {
                TraceHelper.Write("GetHash crashed", true);
                TraceHelper.Write("strSource:", true);
                TraceHelper.Write(A_0, true);
                TraceHelper.Write("strHashData:", true);
                TraceHelper.Write(A_1, true);
                TraceHelper.TraceException(ex, null, "GetHash");
                flag = false;
        }
        return flag;
}
//Forguncy.q.a.a(Forguncy.q.a.a, text, A_1)
private static bool a(string A_0, string A_1, string A_2)
{
        bool flag;
        try
        {
                byte[] array = Convert.FromBase64String(A_1);
                RSACryptoServiceProvider rsacryptoServiceProvider = new RSACryptoServiceProvider();
                rsacryptoServiceProvider.FromXmlString(A_0);  //这里也弄了一个公钥
                RSAPKCS1SignatureDeformatter rsapkcs1SignatureDeformatter = new RSAPKCS1SignatureDeformatter(rsacryptoServiceProvider);
                rsapkcs1SignatureDeformatter.SetHashAlgorithm("MD5");
                byte[] array2 = Convert.FromBase64String(A_2);
                if (rsapkcs1SignatureDeformatter.VerifySignature(array, array2)) //这里有RSA鉴权   为了和服务端统一 我们直接替换公钥
                {
                        flag = true;
                }
                else
                {
                        flag = false;
                }
        }
        catch (Exception ex)
        {
                TraceHelper.Write("SignatureDeformatter crashed", true);
                TraceHelper.Write("strKeyPublic:", true);
                TraceHelper.Write(A_0, true);
                TraceHelper.Write("strHashbyteDeformatter:", true);
                TraceHelper.Write(A_1, true);
                TraceHelper.Write("strDeformatterData:", true);
                TraceHelper.Write(A_2, true);
                TraceHelper.TraceException(ex, null, "SignatureDeformatter");
                flag = false;
        }
        return flag;
}

那么注册码的逻辑就知道了

["email#num",RSASign("email#num","MD5",PKCS1填充)]

开始码


static string GetClientCode(string email="52@pojie.com#666")
{
    string tmp = email;
    byte[] bytes = Encoding.UTF8.GetBytes(tmp);
    byte[] signData = rsa.SignData(bytes, HashAlgorithmName.MD5, RSASignaturePadding.Pkcs1);
    return JsonConvert.SerializeObject(new[] {tmp, Convert.ToBase64String(signData)});
}


激活成功! 可惜成功没啥提示。

发布功能 分析

简单用用之后,咱们来看看发布吧。
噔噔咚。。。发布失败。。。还触发了之前下的错误信息框断点。


刚好拿来定位问题。

//请求验证
private static void a(string A_0, string A_1, string A_2, bool A_3, bool A_4)
{
        if (!ResourceHelper.IsChinese())
        {
                return;
        }
        Dictionary<string, object> dictionary = new Dictionary<string, object>();
        dictionary["userName"] = A_1;
        dictionary["password"] = A_2;
        dictionary["isGetServerData"] = A_3;
        dictionary["publishUser"] = A_4;
        Forguncy.d.b(ServiceVisitor.CreateServiceVisitor(A_0).CallMethodToGetResultStream("Publish/GetLS", dictionary, null, true));
}


眼熟吧,这儿也得改。。。
改完再试试~

欧耶 发布成功!

然后 页面打开了!


这提示是什么鬼。。。
然后 在后端死找没找到相关内容,突发奇想会不会在前端。。。
然后就


   

数据是接口返回的 那么得去 发布端 查问题了   

通过新出现的 进程去查

  

然后查 代码

  

确认 发布后站点代码在 Forguncy.Server2
有兴趣同学的话可以追下接口(这里因为用了异步所以追码流程太长了就略过了)
这里直接说结果吧
因为 既然出现了鉴权失效 那结合之前的尿性 肯定就是哪儿 RSA 漏了,直接搜 F1=  

果然有。。。改呗。。。

成了

收费插件分析

插件市场里面有好多的插件,简单拖几个玩玩吧。


结果发布的时候出问题了

服务端控制台也有提示

咱们瞧瞧去

根据错误信息定位到

private static void c(string A_0, string A_1)
{
        FileUtilities.DeleteFolder(A_0);
        throw new Exception(A_1);
}
//然后 查引用查到
private static void a(UserServiceDBContext A_0, string A_1, Version A_2)
{
        string text = Forguncy.UserService2.Controllers.f.d(A_0).ServerLicenseInfoList[0].RealIssuingKey;
        w w = new w
        {
                ServerKey = text,
                AppPath = A_1,
                version = A_2.ToString()
        };
        byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(w));
        string text2;
        //看来 ForguncyServerConsole 这个应用的 AppCheck 逻辑里面有 验证啊,而且返回值必须不在 198-200 才能成功。
        switch (ProcessHelper.ExecuteProcessWithError("ForguncyServerConsole", "AppCheck" + " " + Convert.ToBase64String(bytes), out text2))
        {
        case 197:
                break;
        case 198:
                DeployApp.c(A_1, Resources.NoPermissionMsg);
                return;
        case 199:
                DeployApp.c(A_1, Resources.PlugInPaymentMsg + text2);
                return;
        case 200:
                DeployApp.c(A_1, Resources.PublishFailed);
                break;
        default:
                return;
        }
}

internal enum v
{
        // Token: 0x04000317 RID: 791
        a = 197,
        // Token: 0x04000318 RID: 792
        b,
        // Token: 0x04000319 RID: 793
        c,
        // Token: 0x0400031A RID: 794
        d
}
private static v a(string[] A_0, out string A_1)
{
        v v;
        try
        {
                A_1 = string.Empty;
                int num = A_0.Length;
                byte[] array = Convert.FromBase64String(A_0[num - 1]);
                w w = JsonConvert.DeserializeObject<w>(Encoding.UTF8.GetString(array));
                if (new Version(w.version) < new Version("7.0.0.0"))
                {
                        v = v.a;
                }
                else
                {
                        string text = u.b(Path.Combine(w.AppPath, "Metadata"), w.ServerKey);
                        if (!string.IsNullOrWhiteSpace(text))
                        {
                                if (SystemConfigDef.IsCloudServer)
                                {
                                        TraceHelper.Write("IPProtectionCheckState CheckLicense CheckTemplateString checkResult=" + text, true);
                                }
                                A_1 = text;
                                v = v.b;
                        }
                        else
                        {
                                string text2 = u.a(Path.Combine(w.AppPath, "Plugins"), w.ServerKey);//也就是 text2 必须没有值
                                if (!string.IsNullOrWhiteSpace(text2))
                                {
                                        if (SystemConfigDef.IsCloudServer)
                                        {
                                                TraceHelper.Write("IPProtectionCheckState CheckLicense CheckPluginProtection pluginCheckResult=" + text2, true);
                                        }
                                        A_1 = text2;
                                        v = v.c;
                                }
                                else
                                {
                                        v = v.a; //所以要解排除错误得走这儿
                                }
                        }
                }
        }
        catch (Exception ex)
        {
                TraceHelper.Write(ex.Message, true);
                A_1 = ex.Message;
                v = v.d;
        }
        return v;
}
//u.a(Path.Combine(w.AppPath, "Plugins"), w.ServerKey)
//A_1 ServerKey
internal static string a(string A_0, string A_1)
{
        if (!Directory.Exists(A_0))
        {
                return null;
        }
        List<string> list = new List<string>();
        foreach (string text in Directory.GetFiles(A_0, "PluginConfig.json", SearchOption.AllDirectories))
        {
                u.b b = new u.b();
                aq aq = JsonConvert.DeserializeObject<aq>(File.ReadAllText(text));
                b.a = Path.GetFullPath(Path.Combine(text, ".."));
                try
                {
                        IEnumerable<string> enumerable = aq.assembly.Where(new Func<string, bool>(b.b));//这里是找不在插件目录内的 dll
                        if (enumerable.Any<string>())
                        {
                                foreach (string text2 in enumerable)
                                {
                                        if (!File.Exists(Path.Combine(b.a, Path.GetFileNameWithoutExtension(text2) + ".edll")))
                                        {
                                                throw new Exception("Could not find assembly " + text2 + "."); //如果 dll 不在 目录内 并且 没有匹配的 .edll 就直接弹错
                                        }
                                        m m = u.a(b.a, aq);//清单内容解密 但是也是 RSA 解密的是官方的授权。不好改 所以不动它
                                        if (string.IsNullOrEmpty((m != null) ? m.ServerLicenseKey : null) || ((m != null) ? m.ServerLicenseKey : null) != A_1)
                                        {
                                                //关键点在这儿,如果那个插件 授权 的 ServerLicenseKey 和我们注册的Key不一致就添加到list 里面。
                                                //那么我们实际上得爆破这里 不添加就行了。或者最后强制返回 空
                                                list.Add(u.a(aq));
                                        }
                                }
                        }
                }
                catch (Exception ex)
                {
                        TraceHelper.WriteLicenseException(ex.Message);
                        throw new Exception("Plugin " + u.a(aq) + " license validation failed.");
                }
        }
        return string.Join(",", list);
}

将最后改成

120        0160        ldstr        ","
121        0165        ldloc.3
122        0166        call        string [netstandard]System.String::Join(string, class [netstandard]System.Collections.Generic.IEnumerable`1<string>)
123        016B        pop
124        016C        ldnull
125        016D        ret

在发布看看~


成功!

因为篇幅有限 其他没讲的就放到下次最终篇吧!

下期预告

  • 不看不知道 一看吓一跳,应用里竟然有检测白嫖的暗桩?!拿网上破解版来干坏事的小伙伴小心会遭重哟!

  • 虽然这个应用混淆不复杂也能改代码,但是校验的点有些多。。。
    不小心漏了就要继续分析,而且每次都手动改exe或dll是不是很麻烦?
    对这种能改的程序 有没有什么偷懒的方案,这种相同的逻辑能不能通杀?比如 Hook 一下?  

感兴趣的话 给咱点免费评分呗... 咱好有动力抓紧码字啊...

关于 Wyn

看到不少留言说顺手弄一下 Wyn
简单看过,这两个其实验证逻辑基本一样的。。。
只是 鉴权类 有些差别。
其他的就只有 各个 字段对应的值要怎么设了,有没有验证什么的
而且 Wyn 只有一个端 只需要改一个dll
真心推荐看过这两篇帖子的坛友自己上手试呢~

相信这个如果弄会了,Wyn 一定 so easy !
期待大伙儿对 Wyn 的分析贴呢~

免费评分

参与人数 11威望 +1 吾爱币 +27 热心值 +11 收起 理由
ti996633 + 1 + 1 谢谢@Thanks!
笙若 + 1 + 1 谢谢@Thanks!
LYTLYT + 1 谢谢@Thanks!
Yokuri + 1 + 1 谢谢@Thanks!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
用户SVIP + 1 大佬呀!太强了
HAINING + 1 + 1 用心讨论,共获提升!
wg198300 + 1 我很赞同!
Epool + 1 + 1 太强了
skyuce + 1 + 1 用心讨论,共获提升!
3yu3 + 1 + 1 用心讨论,共获提升!

查看全部评分

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

来自 25#
 楼主| pjy612 发表于 2023-4-22 13:17 |楼主
本帖最后由 pjy612 于 2023-4-22 13:20 编辑
HAINING 发表于 2023-4-22 11:27
收费插件只能爆破  吗

没,还可以直接批量解密收费的插件,会放下次一起写出来。
前面 基本就是走流程 把点位都说一遍。之后就 一下子就是写代码通用的就一下子全弄掉。
不通用的就静态补丁弄掉。

咱就是 又把自己的破解思路和流程 重走了一遍 再写出来。。。
这样有谁真去上手的话 不至于偏的太多,上下文基本都有关联。

所以 废话多一些。
来自 27#
 楼主| pjy612 发表于 2023-4-22 13:29 |楼主
lml0126 发表于 2023-4-22 13:23
好强大,期待出来(三),怎么把代码清理得那么干净的,用你的工具还是很多花指令

就是de4dot 啊 。。。要么你用 de4dot reactor 6.7 这个,这个可能稍微好一点。
沙发
 楼主| pjy612 发表于 2023-4-21 18:44 |楼主
3#
lml0126 发表于 2023-4-21 18:56
大佬牛,估计他家其他产品也类似,价格还贵
4#
3yu3 发表于 2023-4-21 18:59
大佬的技术高超,文章写的更加精彩
5#
liduowu 发表于 2023-4-21 19:17
弹窗应该也是那个COM来的,至于出现免费字样,在那JS文件里也可以找到这字样,清一下就好了
6#
koolaa 发表于 2023-4-21 19:25
看看,学习一下,谢谢
7#
spray 发表于 2023-4-21 20:04
来学习啦!
8#
yongyou 发表于 2023-4-21 20:34
谢谢表哥分享的技术贴,学习下
9#
liangang 发表于 2023-4-21 21:04
大佬的技术高超,文章写的更加精彩
10#
bin1421457921 发表于 2023-4-21 21:21
111111111111111111111
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-1-15 00:48

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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