本帖最后由 pjy612 于 2025-11-13 22:23 编辑
某低代码平台 逆向分析(一)【验证逻辑分析和实践】
前情提要
虽然之前一直有看到 低代码 低代码的,但是具体是个啥还不太清楚...
这不巧了么...前两期的嘉宾的其他产品里也有个低代码...
但是这羊毛不能只逮着一只薅啊...
所以咱还是在水区发了个帖子,看看大伙儿对这类玩意儿的兴趣程度...
万一很感兴趣,那咱就别发了...
但是看样子大伙儿好像并没啥兴趣...
这样拿出来分析就没啥压力了呢~
然后 网上搜了下 发现 破解版还不少
既然别人都能破解了,那咱们应该也可以试着分析分析吧...
这下 不算是nuget包了,也不是 "简单的js"(论坛web逆向大牛太多,咱的分析帖感觉都上不了台面)
而是实打实的.Net应用了!
希望大伙儿能学到点别的思路...
_(:3」∠)_ 这波弄完基本就真掏空了...
Ps.这次图片高达35张又创新高... 难道以后要出视频?!
嘉宾介绍
|
链接 |
| 国外官网 |
aHR0cHM6Ly93d3cuZm9yZ3VuY3kubmV0Lw== |
| 国内官方 |
aHR0cHM6Ly93d3cuZ3JhcGVjaXR5LmNvbS5jbi9zb2x1dGlvbnMvaHVvemlnZQ== |
| 国内文档 |
aHR0cHM6Ly9oZWxwLmdyYXBlY2l0eS5jb20uY24vZGlzcGxheS9IdW9aaUdlOA== |
准备工作
先去国内官方下载程序吧,分客户端和服务端...
然后简单登陆看看
没啥毛病 开始分析
开始分析
随便写个假码试试
emmm 有在线验证啊,但是 文档里面有写离线验证,那咱们研究下怎么触发吧。
看着像个MVC的程序,那么看看启动程序吧。
默认情况启动了两个进程。
再看看服务
那么 实际 后台项目 就是 ForguncyUserServiceConsole.dll 了。
扔到 dnspy 里面瞧瞧
有控制流混淆啊... 先试试运气吧,扔 de4dot 里面看看处理完还能不能跑。
另外因为如果要经常改程序集,就不用服务启动了 停掉服务,手动运行 ForguncyWorkerService.exe 吧。
全默认参数 走你┏ (゜ω゜)=☞
然后 把处理完的文件名改回 dll,重跑一下看看,能启动最好,不能启动就只能带混淆分析了。。。
处理完就清晰多了,但是能跑起来吗? 试试先
看来没问题!那之后的DLL遇到要分析的都可以先这么处理一下了!
但是这个dll明显不是 MVC的架构,
所以实际启动还在别的里面...
这里面有明显的标记 Forguncy.UserService2
然后实际经过一番定位后 也确定是 它。
那么 也先给它去混淆一下 然后继续定位。
其中 构建请求数据 和 在线验证 又都在 CommonUtilities 里面
那么我们也处理一下dll吧...
结果处理后无法启动 提示 从 CommonUtilities 中 字段找不到。
看来不能用默认选项处理 有的名称需要保持不变 所以加上 --dont-rename 重新处理下。
这次就正常了。
继续分析。
//构建注册请求对象
internal static Dictionary<string, object> a(string A_0, string A_1, string A_2 = null)
{
A_0 = A_0.Replace("-", "");
ForguncySystemInfo forguncySystemInfo = Injectors.GetForguncySystemInfo();
string[] array = new string[]
{
forguncySystemInfo.ComputerId,
A_0,
Environment.MachineName,
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
A_1,
forguncySystemInfo.OSInfo,
forguncySystemInfo.HDInfo,
forguncySystemInfo.CPUInfo,
A_2
};
string text = string.Join("\r\n", array);
List<string> list = ac.d(aa.a);
string text2 = string.Format("<{0}><{1}>{2}</{1}><{3}>{4}</{3}></{0}>", new object[]
{
list[0],
list[1],
ac.a(aa.c),
list[2],
ac.c(aa.b)
});
text = EncryptDecryptHelper.EncryptByXmlPublicKey(text, text2); //这里有用公钥对数据加密,所以这里得动
Dictionary<string, object> dictionary = new Dictionary<string, object>();
dictionary.Add("data", text);
if (ResourceHelper.IsChinese())
{
dictionary.Add("versionLang", "cn");
}
else if (ResourceHelper.IsKorean())
{
dictionary.Add("versionLang", "kr");
}
else if (ResourceHelper.IsJapanese())
{
dictionary.Add("versionLang", "ja");
}
else
{
dictionary.Add("versionLang", "en");
}
return dictionary;
}
//在线验证部分
internal class ab : z
{
// Token: 0x060006C6 RID: 1734 RVA: 0x00005CBD File Offset: 0x00003EBD
internal static string a()
{
if (ResourceHelper.IsJapanese())
{
return "https://forguncylicenseonlineapp.azurewebsites.net/";
}
if (ResourceHelper.IsKorean())
{
return "http://211.171.250.147:808/";
}
if (ResourceHelper.IsChinese())
{
return "http://forguncyactiveservice.grapecity.com.cn/";
}
return "https://activation.forguncy.net/";
}
// Token: 0x060006C7 RID: 1735 RVA: 0x00005CEB File Offset: 0x00003EEB
ActiveResult z.a(Dictionary<string, object> A_0, ProxySettings A_1)
{
if (!ResourceHelper.IsChinese() && !ResourceHelper.IsEnglish())
{
return this.a("Active", A_0, A_1);
}
return this.a("ActiveV8", A_0, A_1);
}
// Token: 0x060006C8 RID: 1736 RVA: 0x00005D16 File Offset: 0x00003F16
ActiveResult z.b(Dictionary<string, object> A_0, ProxySettings A_1)
{
if (!ResourceHelper.IsChinese() && !ResourceHelper.IsEnglish())
{
return this.a("Deactive", A_0, A_1);
}
return this.a("DeactiveV8", A_0, A_1);
}
// Token: 0x060006C9 RID: 1737 RVA: 0x00013A94 File Offset: 0x00011C94
ActiveResult z.c(Dictionary<string, object> A_0, ProxySettings A_1)
{
ActiveResult activeResult;
try
{
string text = EncryptDecryptHelper.DecryptByPublicKey(A_0["data"].ToString(), SystemConfigDef.PlugInAndIPPPublicKey);
activeResult = new ActiveResult
{
Success = true,
Result = EncryptDecryptHelper.EncryptByPublicKey(text, SystemConfigDef.PlugInAndIPPPublicKey)
};
}
catch
{
activeResult = this.a("CheckLicense", A_0, A_1);
}
return activeResult;
}
// Token: 0x060006CA RID: 1738 RVA: 0x00005D41 File Offset: 0x00003F41
private ActiveResult a(string A_0, Dictionary<string, object> A_1, ProxySettings A_2)
{
return JsonConvert.DeserializeObject<ActiveResult>(ServiceVisitor.CreateServiceVisitor(ab.a()).CallMethodToGetResultString(A_0, A_1, A_2));
}
// Token: 0x060006CB RID: 1739 RVA: 0x000024DE File Offset: 0x000006DE
public ab()
{
}
}
如果我们想不走在线流程 根据 注册部分的代码 是不是 我们直接 抛异常就行了???
internal static ActiveResult smethod_25(string string_1, string string_2, ProxySettings proxySettings_0)
{
Dictionary<string, object> dictionary = aa.a(string_1, string_2, "8.0.105.0");
ActiveResult activeResult;
try
{
//throw new Execption(); //这里直接扔异常
activeResult = aa.a().d(dictionary, proxySettings_0);
}
catch (Exception)
{
activeResult = Class229.smethod_30(dictionary["data"] as string);
}
return activeResult;
}
我们实践一下。
emmmm ... 是不是太简单了?
万一遇到 不能直接改和编译的情况呢???
得上IL修改 咱们撤销一下再来一次。
我们得在call之前给他 扔一个异常。
然后保存。
和我们想的有点不一样。。。
我们重新打开 IL 切到异常处理模块。
我们加的代码是 5 ,但是 异常处理 开始位置是 7,所以我们改改。

好了 对了。
重跑看看。
尴尬 报错了。。。
咱们来附加调试看看。。。
再点下试试。。。
断下来了。。。 但是咱码呢???码没了?!
看看堆栈
得。。。又是去混淆的锅,看来 Forguncy.UserService2 处理时 也要加上 --dont-rename 。
上面几步重新弄一下。稍等。。。
看吧。。。就说没那么简单吧。。。得上IL大法。。。
再附加调试一下。
咱又有码了!放行!
能稳定触发离线注册了。
开始分析离线流程。
[HttpPost]
public ResultData OffLineActive([FromBody] OffLineActiveParam param)
{
string content = param.content;
ManagementPageService managementPageService = new ManagementPageService(base.DBContext, this.User);
ResultData resultData = managementPageService.a(Privileges.ActiveBaseServerLicense);
if (resultData != null)
{
return resultData;
}
ActiveResult activeResult = Forguncy.UserService2.Controllers.f.e(content);
return new ResultData
{
Message = (activeResult.Success ? activeResult.Result : activeResult.ErrorMessage),
Result = activeResult.Success
};
}
//A_0 离线激活码
internal static ActiveResult e(string A_0)
{
ActiveResult activeResult;
try
{
string[] array = A_0.Split("|", StringSplitOptions.None);
string text = array[0];
ag ag = Forguncy.UserService2.Controllers.f.a(text); //解析码转实体
if (ag == null)
{
activeResult = ActiveResult.Error(Resources.LicenseActiveResultError_KeyError);
}
else if (ag.ComputerName != Environment.MachineName)//ComputerName = Environment.MachineName
{
activeResult = ActiveResult.Error(Resources.LicenseActiveResultError_MachineNameError);
}
else if (ag.OSID != ar.b()) OSID = 某个值 可以调试一下
{
activeResult = ActiveResult.Error(Resources.LicenseActiveResultError_ComputerIdError);
}
else
{
//剩下的都是在写码了
Forguncy.UserService2.Controllers.f.a(array[0], Forguncy.UserService2.Controllers.f.g(), ag.SerialKey.Key);
CommonUtilities.aa.a(ag.SerialKey.Key, ag.Credential);
if (array.Length == 3 && ResourceHelper.IsChinese())
{
Forguncy.UserService2.Controllers.f.a(array[1], Forguncy.UserService2.Controllers.f.e(), ag.SerialKey.Key);
Forguncy.UserService2.Controllers.f.a(array[2], Forguncy.UserService2.Controllers.f.f(), ag.SerialKey.Key);
}
activeResult = new ActiveResult(Resources.LicenseActiveResult_ActiveSuccess);
}
}
catch (Exception ex)
{
TraceHelper.TraceException(ex, null, "OfflineActive");
activeResult = ActiveResult.Error(Resources.LicenseActiveResult_ActiveFailed);
}
return activeResult;
}
//那我们着重分析下 解码
//Forguncy.UserService2.Controllers.f.a(text)
private static ag a(string A_0)
{
return af.b(CommonUtilities.aa.a(A_0));
}
//CommonUtilities.aa.a(A_0)
internal static Stream a(string A_0)
{
MemoryStream memoryStream = new MemoryStream();
StreamWriter streamWriter = new StreamWriter(memoryStream);
streamWriter.Write(A_0);
streamWriter.Flush();
memoryStream.Position = 0L;
return memoryStream;
}
//af.b
internal static ag b(Stream A_0)
{
if (SystemConfigDef.IsCloudServer) // 我们不用这个 所以走下面
{
ag ag;
using (StreamReader streamReader = new StreamReader(A_0))
{
ag = JsonConvert.DeserializeObject<ag>(EncryptDecryptHelper.DecryptByPublicKey(streamReader.ReadToEnd().Trim(), SystemConfigDef.CloudSitesLicRsaPublicKey));
}
return ag;
}
return af.a(A_0);//实际逻辑在这儿
}
//af.a(A_0)
//A_0 离线激活码
private static ag a(Stream A_0)
{
string text = null;
string text2 = null;
af.d = null;
ag ag2;
try
{
using (StreamReader streamReader = new StreamReader(A_0))
{
for (string text3 = streamReader.ReadLine(); text3 != null; text3 = streamReader.ReadLine())
{
if (text3.StartsWith("F1="))
{
text = text3;
}
else if (text3.StartsWith("F2="))
{
text2 = text3;
}
if (text != null && text2 != null)
{
break;
}
}
//由上面逻辑分析,码由两部分构成 是字符串 两行
//F1=xxx
//F2=xxx
if (text != null && text2 != null)
{
byte[] array = EncryptDecryptHelper.AESDecrypt(Convert.FromBase64String(text.Substring("F1=".Length)), ae.c, ae.d, CipherMode.CBC, PaddingMode.PKCS7);
ag ag = ag.a(array);//这里直接是 JSON 反序列化 所以 原始 F1 应该为 ASE(鉴权对象的JSON)
if (ag == null)
{
return null;
}
//这个是解密出一个公钥
string @String = Encoding.ASCII.GetString(EncryptDecryptHelper.AESDecrypt(ae.e, ae.c, ae.d, CipherMode.CBC, PaddingMode.PKCS7));
if (string.IsNullOrWhiteSpace(@string))
{
return null;
}
using (RSACryptoServiceProvider rsacryptoServiceProvider = new RSACryptoServiceProvider(2048))
{
rsacryptoServiceProvider.FromXmlString(@string);//导入公钥
byte[] array2 = array;
byte[] array3 = Convert.FromBase64String(text2.Substring("F2=".Length));
//对 鉴权对象的JSON 进行鉴权,所以 F2=sing(鉴权对象的JSON)
if (rsacryptoServiceProvider.VerifyData(array2, CryptoConfig.MapNameToOID("SHA1"), array3))
{
af.d = new bool?(true);
return ag;
}
af.d = new bool?(false);
return null;
}
}
ag2 = null;
}
}
catch (Exception)
{
ag2 = null;
}
return ag2;
}
那么答案就有了。
我们 把这个AES提取一下,然后 RSA 就用自己生成一套。能对上就行。
ag 对象 我们也抄一下。
开工 (替换密钥+离线激活码实现)
由于是首次处理,所以我们直接走爆破,毕竟爆破成功的话,后续才好根据特征写自动补丁。
那么通过我们上面的分析,为了能正常通信,那么至少有两点。
离线请求码的构建部分 和 解析离线码的密钥 我们都要改。
大方点我们直接塞私钥。
然后 我们生成一个离线码解析看看。
解码后的信息
90855*********************58d9
6666666666666666
机器名
2023-04-17 13:24:47
server
Microsoft Windows 10 Enterprise
\\.\PHYSICALDRIVE0
11th Gen Intel(R) Core(TM) i5-1145G7 @ 2.60GHz,BFEBFBFF000806C1
8.0.105.0
然后 激活码实体结构为
(一些更细节的分析,比如哪个key要填什么值,为什么要那么填,具体怎么找的。
由于篇幅限制 这里就直接给答案了,有兴趣的可以自己去分析具体的检测)
class LicenseInfo
{
public string SerialKeyID { get; set; } //没太多意义 52pojie
public KeyInfo SerialKey { get; set; } //子对象
public string OSID { get; set; } //line[0]
public string Credential { get; set; } //没太多意义 52pojie
public string ComputerName { get; set; } //line[2]
public string VersionString { get; set; }//line[8]
}
class KeyInfo
{
public string Key { get; set; } //激活时填的码 line[1];
public string DevelopmentKey => this.Key;
public int KeyType { get; set; } //-3 CommonUtilities.SerialKeyType.CNServerAndUser
public DateTime Duration { get; set; } = TimeZoneInfo.ConvertTimeToUtc(DateTime.MaxValue); //期限
public bool IsTerm { get; set; } = false; //是否有期限
public bool Enabled { get; set; } = true;
public bool isComputerNameVerify { get; set; } = false;//是否验证机器名
public int Version { get; set; } // -7779 ForguncyVersion.LicenseVersionNumber_CN
public int PageNumber { get; set; } //页面数什么的 不填最大
public string ExtendInfos { get; set; } //人数什么的 不填最大
public bool ConcurrentUser { get; set; } = true; //是否并发
public bool IncludeReport { get; set; } = true; //是否报表
}
{"SerialKeyID":"52pojie","SerialKey":{"Key":"666666666666666666666666","DevelopmentKey":"666666666666666666666666","KeyType":-3,"Duration":"\/Date(253402271999999)\/","IsTerm":false,"Enabled":true,"isComputerNameVerify":false,"Version":-7779,"PageNumber":0,"ExtendInfos":null,"ConcurrentUser":true,"IncludeReport":true},"OSID":"*********","Credential":"52pojie","ComputerName":"机器名","VersionString":"8.0.105.0"}
见证奇迹的时刻!!!

啊咧?!但是Σ(っ °Д °;)っ 提交成功了...为啥还是空白的?!?!
没有弹错误提示啊?
看来还有🕳(坑)
咱们来看 ManagementPage/LicenseList 接口。
[HttpGet]
public IActionResult LicenseList()
{
Forguncy.UserService2.Models.a a = Forguncy.UserService2.Controllers.f.d(base.DBContext);
return this.a("LicenseList", a);
}
//Forguncy.UserService2.Controllers.f.d(base.DBContext);
internal static Forguncy.UserService2.Models.a d(UserServiceDBContext A_0)
{
Forguncy.UserService2.Models.a a = Forguncy.UserService2.Controllers.f.d.Get(Forguncy.UserService2.Controllers.f.e) as Forguncy.UserService2.Models.a;
if (a != null)//读缓存有必要 调试时 手动赋值为null
{
return a;
}
List<Forguncy.UserService2.Controllers.f.a> list = Forguncy.UserService2.Controllers.f.g(null);
Forguncy.UserService2.Models.a a2 = new Forguncy.UserService2.Models.a();
a2.ComputerName = Environment.MachineName;
new Forguncy.UserService2.Controllers.f.i().bk(list.FirstOrDefault<Forguncy.UserService2.Controllers.f.a>(), a2).bl();
new Forguncy.UserService2.Controllers.f.n().bk(list.FirstOrDefault<Forguncy.UserService2.Controllers.f.a>(), a2).bl();
new Forguncy.UserService2.Controllers.f.s().bk(list.FirstOrDefault<Forguncy.UserService2.Controllers.f.a>(), a2).bl();
new Forguncy.UserService2.Controllers.f.x().bk(list.FirstOrDefault<Forguncy.UserService2.Controllers.f.a>(), a2).bl();
if (AutoTestIndicator.IsAutoTest)
{
a2.AllowUserCount = int.MaxValue;
}
Forguncy.UserService2.Controllers.f.d.Add(Forguncy.UserService2.Controllers.f.e, a2, new TimeSpan(1, 0, 0));
return a2;
}
//Forguncy.UserService2.Controllers.f.g(null);
internal static List<Forguncy.UserService2.Controllers.f.a> g(string A_0 = null)
{
string text = Forguncy.UserService2.Controllers.f.f(A_0);//获取License存放的路径
return Forguncy.UserService2.Controllers.f.a(text, typeof(Forguncy.UserService2.Controllers.f.d));
}
//Forguncy.UserService2.Controllers.f.a
private static List<Forguncy.UserService2.Controllers.f.a> a(string A_0, Type A_1)
{
if (!Directory.Exists(A_0))
{
return new List<Forguncy.UserService2.Controllers.f.a>();
}
string[] files = Directory.GetFiles(A_0); //通过路径获取所有的离线key信息
FileInfo[] array = new FileInfo[files.Length];
for (int i = 0; i < files.Length; i++)
{
array[i] = new FileInfo(files[i]);
}
Array.Sort<FileInfo>(array, new Comparison<FileInfo>(Forguncy.UserService2.Controllers.f.<>c.<>9.a));
HashSet<string> hashSet = new HashSet<string>();
List<Forguncy.UserService2.Controllers.f.a> list = new List<Forguncy.UserService2.Controllers.f.a>();
foreach (FileInfo fileInfo in array)
{
a3 a = Forguncy.UserService2.Controllers.f.h(fileInfo.FullName); //实际解析Key
Forguncy.UserService2.Controllers.f.b b = Forguncy.UserService2.Controllers.f.f.a(a, A_1);
//后续一些验证
if (Forguncy.UserService2.Controllers.f.a(b) && (a == null || a.SerialKey == null || a.SerialKey.Key == null || !hashSet.Contains(a.SerialKey.Key)))
{
if (a != null)
{
hashSet.Add(a.SerialKey.Key);
}
Forguncy.UserService2.Controllers.f.a a2 = new Forguncy.UserService2.Controllers.f.a
{
ImportDate = fileInfo.CreationTime,
License = a,
AssociatedLicenseFile = fileInfo.FullName
};
list.Add(a2);
}
}
return list;
}
//Forguncy.UserService2.Controllers.f.h(fileInfo.FullName)
private static a3 h(string A_0)
{
if (!File.Exists(A_0))
{
TraceHelper.WriteLicenseException("license file can't find");
TraceHelper.WriteLicenseException("filePath : " + A_0);
return null;
}
a3 a2;
try
{
using (FileStream fileStream = File.OpenRead(A_0))
{
a3 a = a2.b(fileStream);
if (a == null)
{
TraceHelper.WriteLicenseException("The Readed license info is null");
}
a2 = a;
}
}
catch (Exception ex)
{
TraceHelper.WriteLicenseException("Read license file failed");
TraceHelper.TraceException(ex, null, "GetLicenseInfoFromFile");
a2 = null;
}
return a2;
}
//a2.b(fileStream) 见下图
看来不止一处 需要 修改 RSA 密钥啊。。。 手动改改再重启看看。。。
哈!搞定收工!
结果展示
下期预告
-
客户端虽然是免费的,但是使用前还是会有个激活码验证,咱们也试试🔪掉吧~
-
插件市场好丰富,纳尼?里面竟然还有收费的?而且价格好像都赶超软件本体了?那不得试试能不能白嫖了!🔪了🔪了!
-
发布功能是不是正常,咱们还没测呢!
-
不看不知道 一看吓一跳,应用里竟然有检测白嫖的暗桩?!拿网上破解版来干坏事的小伙伴小心会遭重哟!
-
虽然这个应用混淆不复杂也能改代码,但是校验的点有些多。。。
不小心漏了就要继续分析,而且每次都手动改exe或dll是不是很麻烦?
对这种能改的程序 有没有什么偷懒的方案,这种相同的逻辑能不能通杀?比如 Hook 一下?
感兴趣的话 给咱点免费评分呗... 咱好有动力抓紧码字啊...
|