高中时候写的文章,现在搬过来。
说在前面:本备忘的内容均以电子班牌系统为例进行介绍。
一、插件系统 0. 概论 通软公司的统一开发框架支持且只支持一种基于插件的开发。如此操作似乎是有着解耦合与代码复用方面的考量(例如刷卡组件便是电子班牌与校门口刷卡门岗共用)。
在这种指导思想的引领下,通软的任何一套系统的“主程序”只能被称为是一个插件加载器,而所有的业务逻辑、UI 等等全部被拆散在了一个一个 .Net Framework 类库中。
固然,这种设计模式有着一定的合理性,并的确实现了通软的同志们所希冀的解耦合与复用性,但是也给第三者对其系统的修改带来了极大的便利。实际上,接下来的一切修改,都是基于插件系统进行的。
以下,将介绍通软的插件系统的基本内容。
1. 一套插件的基本组成部分 一般说来,一个插件最少应当包含以下几个组成部分:
至少一个 .Net Framework .dll 类库;
一个对插件基本的结构进行描述的 Mapper.xml 文件;
一个保存着插件配置的 Config.xml 文件。
a. 类库 见后文。
b. Mapper.xml Mapper.xml 对插件的基本内容进行了描述。以下是一个案例(/GS.Terminal.SmartBoard.Logic/Mapper.xml):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="utf-8" ?> <Extensibility xmlns ="urn:Chinags-Extensibility-1.0" Name ="智能班牌逻辑插件" SymbolicName ="GS.Terminal.SmartBoard.Logic" Version ="3.4.2.62" StartLevel ="41" > <License > nX8iVjYNw97wiLmepVhiT...(略)</License > <Activator Type ="GS.Terminal.SmartBoard.Logic.Program" /> <Runtime > <Assembly Path ="GS.Terminal.SmartBoard.Logic.dll" Share ="true" /> <Assembly Path ="GS.Terminal.SmartBoard.LocalDB.dll" Share ="false" /> </Runtime > <ObjectSpaces > <Channel ConnectionName ="sqlite" ModelAssembly ="GS.Terminal.SmartBoard.LocalDB" Name ="sqlite" /> </ObjectSpaces > <Services > <Service TypeAndName ="GS.Terminal.SmartBoard.Logic.Core.Service" Caption ="与服务通讯实时接收推送" > </Service > <Service TypeAndName ="GS.Terminal.SmartBoard.Logic.Core.AdministratorService" Caption ="管理员相关服务" > </Service > </Services > </Extensibility >
这里就关键的几点进行解释:
Name 与 SymbolicName 字段:前者不重要,后者必须 与存储插件的文件夹名称一致。
StartLevel 字段:该字段的值越小,启动越早。建议破解插件在通软规定的“其他插件” StartLevel 段(≥80)启动,以避免潜在的依赖关系问题。
License 字段:插件的证书。后文会对此进行详细解释。此字段欠缺会导致插件被拒绝加载!
Activator 字段:一个实现了 GS.Unitive.Framework.Core.IAddonActivator 接口的类。后文会对此进行详细解释。
Runtime/Assembly 字段:即 .dll 类库的文件名。shared 的真伪决定了能否在别的插件中创建该类库中某个类的实例(不重要)。
ObjectSpaces 字段:实际用途暂不明确,可能与本地数据库读写相关。
Services 字段:将类库中的某些类注册为一系列可以从其他插件中读取并调用其方法的“服务”。服务系统是插件系统中非常关键的一部分。
如果这个文件没有正常配置,插件是无法加载的。
c. Config.xml Config.xml 相当于插件的配置文件。形如:
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="utf-8" ?> <Settings xmlns ="urn:Chinags-Configuration" AddonName ="GS.Terminal.Theme" > <Dictionaries > <Dict Name ="Theme" Caption ="默认主题" > <Key Caption ="主题名称" Choice ="" Name ="ThemeName" Value ="Default" /> </Dict > </Dictionaries > </Settings >
AddonName 字段:与 AddonSymbolicName 一致。
Dictionaries 与 Key:相当于是一系列键值对。具体使用后文会解释。
2. 插件中类库的具体结构 尽管在反编译当中会发现通软自己的插件中有着巨大多命名空间和类,但是实际上大部分的内容都是用来实现各种业务逻辑的。一般说来,一个最基本的插件最多只需要包含以下内容:
一个实现了 GS.Unitive.Framework.Core.IAddonActivator 接口的 激活器 类(名称不一定是这个)。该类应当在 Mapper.xml 的 Activator 字段中被注册。例如,对于命名空间 MyProject.MyNamespace 下一个类 MyActivator,应当这样写:
1 <Activator Type ="MyProject.MyNamespace.MyActivator" />
Service 服务类。名称可以任取(当然为了可读性方面的考虑建议以 Service 结尾)。其应当在 Mapper.xml 的 Services 字段中被注册。例如,对于命名空间 MyProject.MyNamespace 下一个类 MyService,应当这样写:
1 2 3 4 5 6 7 <Services > <Service TypeAndName ="MyProject.MyNamespace.MyService" Caption ="我的服务" > </Service > </Services >
注册后,就可以在别的插件调用 MyService 里面的方法了,详见后文。
3. 插件内部运作与插件间交互的基本操作 主要介绍以下三点:插件的启停、插件数据相关、插件服务的调用。
a. 插件的启停 前面说到,插件应当包含一个实现了 GS.Unitive.Framework.Core.IAddonActivator 接口的激活器类。该类应当实现其 Start(IAddonContext) 及 Stop(IAddonContext) 方法。这两个方法分别会在插件启动/停止时被调用。
比如这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 using System; using GS.Unitive.Framework.Core;using GS.Unitive.Framework.Persistent;namespace MyProject.MyNamespace { public class MyActivator : IAddonActivator { public IAddonContext addonContext; public void Start (IAddonContext context ) { this .addonContext = context; this .addonContext.Logger.Info("成功加载插件" ); } public void Stop (IAddonContext context ) { } } }
这样,便可以实现基本的插件启停。
b. 插件数据相关 首先介绍对插件配置的读取。
假如我们的插件有这样一个配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="utf-8" ?> <Settings xmlns ="urn:Chinags-Configuration" AddonName ="MyProject" > <Dictionaries > <Dict Name ="baseConfig" Caption ="基础配置" > <Key Caption ="第一个" Choice ="" Name ="Key1" Value ="Value1" /> </Dict > </Dictionaries > </Settings >
那么,想要获取 baseConfig 中 Key1 的值,便可以这样写:
1 2 string value1 = addonContext.DictionaryValue("baseConfig" , "Key1" );
另外,如果没有找到 Key1,则会返回 null。
然后介绍创建和读取交互式数据。
交互式数据一经创建,便可由任何插件读取。
1 2 3 4 5 6 7 8 object data = somedata; addonContext.IntercativeData<object >("DataKey" , data);dynamic result = addonContext.IntercativeData("DataKey" );object newdata = somedata; addonContext.IntercativeData<object >("DataKey" , newdata);
c. 插件服务调用 对于其他插件已经注册了的服务,可以通过以下方法进行调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 dynamic service = this .addonContext.GetFirstOrDefaultService("GS.Terminal.MainShell" , "GS.Terminal.MainShell.Services.UIService" ); service.RegistBackgroundCommand("0" , new Action( () => { service.ShowPrompt("Fuck GS!!" , 10 ); } ));
注意,如果指定的服务不存在,则会返回 null。
4. 常用的服务
GS.Terminal.TimeLine 任务计划与定时执行相关;
GS.Terminal.GarnitureControl 创建浮窗等;
GS.Terminal.DeviceManager 管理读卡器等外设;
GS.Terminal.MainShell.Services.UIService 管理用户界面;
5. 基本插件案例 通软在很多服务中都大量使用了 dynamic 动态类型。以下将以读卡器使用为例介绍。
a. 读卡器的使用 以下代码在刷卡时会调用 DoSomething(string) 函数,并传入卡号作为参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 dynamic device = addonContext.GetFirstOrDefaultService("GS.Terminal.DeviceManager" ,"GS.Terminal.DeviceManager.Service.DeviceCallControl" ); deviceMan.RegistCardCallback( new Action<dynamic >( (dynamic data) => { string cardid = data.OperaDeviceData.Message; DoSomething(cardid); } ) );
二、通软插件的反编译修改 0. 概论 尽管服务可以解决相当一部分的问题。但是仍然有一部分功能并没有被写在服务中,无法通过简单的方法进行调用,这时候就需要对通软原有的插件进行反编译修改。
1. 基本流程案例 这里以播放跑马灯进行举例说明。
众所周知,通软在 GS.Terminal.SmartBoard.Logic 中给出了跑马灯的实现(GS.Terminal.SmartBoard.Logic.Garitures.BannerMessageControl)。但是,播放跑马灯的方法,并不能通过某个服务进行调用。因此,有必要通过某种的举措,将播放跑马灯的方法暴露给其他插件。
考虑以下操作:
使用 dnSpy 在 GS.Terminal.SmartBoard.Logic.Core 下创建一个 MyCustomService 类。
修改 GS.Terminal.SmartBoard.Logic 的 Mapper.xml,注册这个类为一个服务。
在这个类中添加以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 using GS.Terminal.SmartBoard.Logic.Garitures; ...namespace GS.Terminal.SmartBoard.Logic.Core { public class MyCustomService { public void ShowShadowLantern (string msg ) { BannerMessageControl.AddBannerMsg(msg); } } }
编译并保存。
然后,就可以在其他插件中通过服务调用 MyCustomService.ShowShadowLantern() 显示跑马灯了。
2. 特殊功能备忘:滑动页面相关 电子班牌的滑动页面是一个非常有趣的东西。比如“班级风采” 一栏,又可以放图片又可以放视频,可以说有很多手脚可以动。
但是,令人意外的是,不像跑马灯和“更多”栏里面的大图片,“班级风采”里面图片视频等的定时展示并非使用 TimeLineTask,而是另一套非常复杂(且非常屎山)的实现。
这种实现方式可以归纳为 VisualPublish - VisualTemplate - VisualBlock 的三级结构(具体实现时还包含了大量的数据库读写)。其中:
VisualPublish 规定了电子班牌加载时使用的主题。
VisualTemplate 描述滑动界面的结构及属性。
VisualBlock 描述滑动页面上的元素的结构及属性。
如果想要修改“班级风采”上的视频,考虑以下实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 using System;using System.Collections.Generic;using System.Drawing;using System.IO;using GalaSoft.MvvmLight.Threading;using IVisualBlock;using SmartBoardViewModels.Models.VisualBlock;namespace GS.Terminal.SmartBoard.Logic.Core { public partial class MyCustomService { public void AddMultiMediaVisualTemplate (string media_json_filename ) { IBlockService firstOrDefaultService = Program.AddonContext.GetFirstOrDefaultService<IBlockService>("GS.Terminal.VisualBlock" ); BlockTemplate blockTemplate = new BlockTemplate(); blockTemplate.TemplateName = "ClassTemplateStyle" ; blockTemplate.DisplayName = "校园风采" ; blockTemplate.Index = 2 ; blockTemplate.TemplateType = BlockTemplateType.Theme; BaseBlock block = firstOrDefaultService.GetBlock("ClassMultiMedia" ); block.Init(Program.AddonContext); IUpdate update = (IUpdate)block; update.LoadLocalData(media_json_filename); blockTemplate.Blocks = new List<VisualBlockItem>(); blockTemplate.Blocks.Add(new VisualBlockItem { Id = Guid.NewGuid(), BlockComponent = "班级风采" , BlockTypeName = ((IBlock)block).TypeName, DataSource = "Services/SmartBoard/BlockClassMultiMedia/json" , Height = 10 , NavTemplateName = "" , Width = 20 , X = 1 , Y = 1 , DataContext = (BaseBlock)update }); blockTemplate.Previous = Utilites.ViewModelLocator.MainPage.TemplateList[1 ]; blockTemplate.Next = Utilites.ViewModelLocator.MainPage.TemplateList[3 ]; DispatcherHelper.RunAsync(delegate { Utilites.ViewModelLocator.MainPage.TemplateList[2 ] = blockTemplate; }); } } }
事实上,同样的操作也适用于其他滑动页面,只要把各种属性的值替换掉就行了。 当然,有的页面上可能有多个 VisualBlock(如首页)。
三、杂项 1. 插件签名认证 前面提到,Mapper.xml 中的 License 字段必须正确配置,否则插件无法启动。根据通软的描述,所谓的插件证书只能在“通软统一开发平台”获取,但实际上并不存在这样的一个平台。对此,只能说傻逼通软。
因此,如果想要加载我们自己的插件,必须想办法解决掉这个证书的问题。解决思路可以有以下两种:
伪造证书;
绕过证书检测
以下,将分别介绍这两种思路。
a. 伪造证书 通软的证书表面上使用了一个所谓加密狗(SoftDog)组件,实际上只是绕了一个弯从加密狗的 dll 文件的资源文件中获取 RSA 私钥。而通软所谓的认证,也不过只是用这个私钥对插件名称进行了一个签名而已。
因此,只要提取加密狗的资源文件,便可以利用 RSA 签名轻松伪造通软的认证。
考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using System;using System.Security.Cryptography; ...public string Encrypt (string addonSymbolicName ) { using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider()) { string xmlStr = "..." ; provider.FromXmlString(xmlStr); RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(provider); formatter.SetHashAlgorithm("SHA1" ); SHA1Managed sha1 = new SHA1Managed(); byte [] rgbHash = sha1.ComputeHash(Encoding.ASCII.GetBytes(addonSymbolicName)); byte [] signature = formatter.CreateSignature(rgbHash); string b64str = Convert.ToBase64String(signature); return b64str; } }
这样,只要输入一个插件的 AddonSymbolicName,便可以生成对应的认证。
b. 绕过检测 通过对 GS.Unitive.Framework.Core.LicenseValidation 的 Verify() 方法进行修改,使其返回值恒为真,以绕过通软的插件认证检测。
这个方法由于过于暴力,并且已经有更加安全的平替,不建议使用。
2. 关于 localData.db 建议使用 SQLiteStudio 打开,格式 System.Data,密码一般为 123。
3. 更新器无效化 为了防止更新导致反编译修改的代码被覆盖,有必要对更新器 AutoUpdate.exe 进行无效化处理。
主要处理 AutoUpdate.MainViewModel 的 Do() 方法。
1 2 3 4 5 6 7 8 9 public void Do () { ... if (this .getRemoteFeatureCode(text)) { LocalLogWriter.Write("无更新" ); } ... }
这样一来,即使有更新,也不会下载更新。
4. 关于编译器生成代码的反推 注意到通软的一些插件反编译后仍然存在大量无法进一步还原的编译器生成代码,阅读起来非常困难。以下介绍一点简单的识别法。
a. 执行动态类型方法 考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Startup { ... private static void InitManagentWindow () { if (Startup.<>o__6.<>p__0 == null ) { Startup.<>o__6.<>p__0 = CallSite<Action<CallSite, object , string , Action>>.Create(Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "RegistBackgroundCommand" , null , typeof (Startup), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null ), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.Constant, null ), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null ) })); } Startup.<>o__6.<>p__0.Target(Startup.<>o__6.<>p__0, Program.AddonContext.GetFirstOrDefaultService("GS.Terminal.MainShell" , "GS.Terminal.MainShell.Services.UIService" ), "100000" , delegate { DoSomething(); } ); } ... }
因此,原始代码应当为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Startup { ... private static void InitManagentWindow () { dynamic service = Program.AddonContext.GetFirstOrDefaultService("GS.Terminal.MainShell" , "GS.Terminal.MainShell.Services.UIService" ); service.RegistBackgroundCommand("100000" , new Action( () => { DoSomething(); } )); } ... }
b. 获取动态类型的属性 这个在刷卡器反编译出的代码处表现的尤其明显。考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 internal class LogicCore { ... internal static void RegistCardService () { object firstOrDefaultService = Program.AddonContext.GetFirstOrDefaultService("GS.Terminal.DeviceManager" , "GS.Terminal.DeviceManager.Service.DeviceCallControl" ); ... if (target(<>p__, LogicCore.<>o__1.<>p__0.Target(LogicCore.<>o__1.<>p__0, firstOrDefaultService, null ))) { if (LogicCore.<>o__1.<>p__5 == null ) { LogicCore.<>o__1.<>p__5 = CallSite<Action<CallSite, object , Action<object >>>.Create(Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "RegistCardCallback" , null , typeof (LogicCore), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null ), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null ) })); } LogicCore.<>o__1.<>p__5.Target(LogicCore.<>o__1.<>p__5, firstOrDefaultService, delegate (dynamic data) { LogicCore.<>c__DisplayClass1_0 CS$<>8 __locals1 = new LogicCore.<>c__DisplayClass1_0(); LogicCore.<>c__DisplayClass1_0 CS$<>8 __locals2 = CS$<>8 __locals1; if (LogicCore.<>o__1.<>p__4 == null ) { LogicCore.<>o__1.<>p__4 = CallSite<Func<CallSite, object , string >>.Create(Binder.Convert(CSharpBinderFlags.None, typeof (string ), typeof (LogicCore))); } Func<CallSite, object , string > target2 = LogicCore.<>o__1.<>p__4.Target; CallSite <>p__2 = LogicCore.<>o__1.<>p__4; if (LogicCore.<>o__1.<>p__3 == null ) { LogicCore.<>o__1.<>p__3 = CallSite<Func<CallSite, object , object >>.Create(Binder.GetMember(CSharpBinderFlags.None, "Message" , typeof (LogicCore), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null ) })); } Func<CallSite, object , object > target3 = LogicCore.<>o__1.<>p__3.Target; CallSite <>p__3 = LogicCore.<>o__1.<>p__3; if (LogicCore.<>o__1.<>p__2 == null ) { LogicCore.<>o__1.<>p__2 = CallSite<Func<CallSite, object , object >>.Create(Binder.GetMember(CSharpBinderFlags.None, "OperaDeviceData" , typeof (LogicCore), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null ) })); } CS$<>8 __locals2.cardNum = target2(<>p__2, target3(<>p__3, LogicCore.<>o__1.<>p__2.Target(LogicCore.<>o__1.<>p__2, data))); Program.AddonContext.Logger.Debug("读卡器读卡结果:" + CS$<>8 __locals1.cardNum, null ); } ); } } ... }
由此,可以得出,原始代码可能为:
1 2 3 4 5 6 7 8 9 10 11 12 dynamic service = Program.AddonContext.GetFirstOrDefaultService("GS.Terminal.DeviceManager" , "GS.Terminal.DeviceManager.Service.DeviceCallControl" );if (service != null ) { service.RegistCardCallback(new Action<dynamic >( (dynamic data) => { SomeClass someclass = new SomeClass(); someclass.cardNum = data.OperaDeviceData.Message; Program.AddonContext.Logger.Debug(...); } )); }
以上。