某低代码平台 逆向分析(一)【验证逻辑分析和实践】
某低代码平台 逆向分析(一)【验证逻辑分析和实践】
前情提要
虽然之前一直有看到 低代码 低代码的,但是具体是个啥还不太清楚...
这不巧了么...前两期的嘉宾的其他产品里也有个低代码...
但是这羊毛不能只逮着一只薅啊...
所以咱还是在水区发了个帖子,看看大伙儿对这类玩意儿的兴趣程度...
万一很感兴趣,那咱就别发了...
但是看样子大伙儿好像并没啥兴趣...
这样拿出来分析就没啥压力了呢~
然后 网上搜了下 发现 破解版还不少
既然别人都能破解了,那咱们应该也可以试着分析分析吧...
这下 不算是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.binternal 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 home.php?mod=space&uid=452487 = 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.aprivate 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 密钥啊。。。 手动改改再重启看看。。。
哈!搞定收工!
结果展示

下期预告
上一篇:VM虚拟机去虚拟化