// ============================================================================= // PatchLicense.cs — one-command license-gate bypass for dnaFusion Flex API // ============================================================================= // Patches OpenOptions.dnaFusion.Flex.ServiceManager.LoadLicense() in // OpenOptions.dnaFusion.Flex.Common.dll (NOT strong-name signed) so it installs // a hand-built DNAFusion license with the features the gate checks: // // Flex = true (bool gate: IsFeatureLicensed("Flex")) // FlexMobile = 1000000 (int gate: device count) // WebUsers = 1000000 (int gate: web operator count) // // Plus a Holder (HaspKey="" / SoftKey=) so the FlexInterop.BroadcastEvent // named-pipe path (which reads license.Holder.HaspKey/SoftKey) doesn't NRE. // // The Features list is written straight into License's private // 'k__BackingField' field (located by type at patch time), so it works // regardless of how the obfuscated constructor initializes it. // // Build (needs Mono.Cecil): // dotnet new console -o patcher && cd patcher // dotnet add package Mono.Cecil // copy PatchLicense.cs over Program.cs // dotnet run -- [softkey] // // The script backs the original up to .orig, then writes the patched // assembly in place. Restart the Flex service afterwards. // ============================================================================= using System; using System.IO; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; internal static class Program { // The SoftKey the desktop DNAFusion client must present to BroadcastEvent. // If you don't use the named-pipe interop, the value is irrelevant. private const string DefaultSoftKey = "FLEXSTUB"; private const string FlexValue = "true"; private const string FlexMobileValue = "1000000"; private const string WebUsersValue = "1000000"; private static int Main(string[] args) { string path = args.Length > 0 ? args[0] : "OpenOptions.dnaFusion.Flex.Common.dll"; string softKey = args.Length > 1 ? args[1] : DefaultSoftKey; if (!File.Exists(path)) { Console.Error.WriteLine($"File not found: {path}"); return 2; } var resolver = new DefaultAssemblyResolver(); resolver.AddSearchDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); var asm = AssemblyDefinition.ReadAssembly(path, new ReaderParameters { AssemblyResolver = resolver, ReadingMode = ReadingMode.Deferred }); var mod = asm.Modules[0]; var sm = mod.GetType("OpenOptions.dnaFusion.Flex.ServiceManager"); if (sm == null) { Console.Error.WriteLine("ServiceManager type not found."); return 1; } var m = sm.Methods.FirstOrDefault(x => x.Name == "LoadLicense" && x.Parameters.Count == 0); if (m == null) { Console.Error.WriteLine("LoadLicense() (0-param) not found."); return 1; } // ---- resolve the (referenced) SoftwareProtection types ------------- var spRef = mod.AssemblyReferences.First(r => r.Name == "OpenOptions.SoftwareProtection"); TypeReference TR(string name) => new TypeReference("OpenOptions.SoftwareProtection", name, mod, spRef); var licDef = TR("License").Resolve(); var featDef = TR("Feature").Resolve(); var dnaDef = TR("DNAFusion").Resolve(); var holderDef = TR("Holder").Resolve(); var getFeatures = licDef.Methods.First(x => x.Name == "get_Features"); // Keep List as a TypeReference. Resolving it (.Resolve()) loads List // through the PATCHER's runtime, so under .NET 10 it binds to System.Private.CoreLib // and the emitted IL then fails on the .NET Framework 4.0 target. The return type // already points at the target's System.Core — use it directly. var listTypeRef = (GenericInstanceType)getFeatures.ReturnType; // List MethodReference dnaCtor = dnaDef.Methods.First(x => x.Name == ".ctor" && x.Parameters.Count == 0); MethodReference featCtor = featDef.Methods.First(x => x.Name == ".ctor" && x.Parameters.Count == 0); MethodReference featSetName = featDef.Methods.First(x => x.Name == "set_Name"); MethodReference featSetValue = featDef.Methods.First(x => x.Name == "set_Value"); // Build List method refs by hand so List stays bound to the target's // System.Core (not the patcher runtime's System.Private.CoreLib). // HasThis=true is REQUIRED: hand-built MethodReferences default to static // (HasThis=false), which emits "void .ctor()" instead of "instance void .ctor()" // -> MissingMethodException at runtime. MethodReference listCtor = new MethodReference(".ctor", mod.TypeSystem.Void, listTypeRef) { HasThis = true }; // List.Add's parameter must be the generic VAR "!0" (as the real compiler // emits), NOT the concrete Feature type: the CLR matches MemberRef signatures // against the method definition's var, so Add(Feature) fails with // MissingMethodException. Harvest the "!0" from a compiler-emitted // List::Add MemberRef that already exists in this module. var templateAdd = FindListAddTemplate(mod); var tVar = (GenericParameter)templateAdd.Parameters[0].ParameterType; // "!0" of List`1 MethodReference listAdd = new MethodReference("Add", templateAdd.ReturnType, listTypeRef) { HasThis = true }; listAdd.Parameters.Add(new ParameterDefinition(tVar)); MethodReference holderCtor = holderDef.Methods.First(x => x.Name == ".ctor" && x.Parameters.Count == 0); MethodReference holderSetH = holderDef.Methods.First(x => x.Name == "set_HaspKey"); MethodReference holderSetS = holderDef.Methods.First(x => x.Name == "set_SoftKey"); MethodReference setHolder = licDef.Methods.First(x => x.Name == "set_Holder"); MethodReference setLicense = sm.Properties.First(p => p.Name == "License").SetMethod; // import cross-assembly references into the target module MethodReference ImpM(MethodReference r) => mod.ImportReference(r); dnaCtor = ImpM(dnaCtor); featCtor = ImpM(featCtor); featSetName = ImpM(featSetName); featSetValue = ImpM(featSetValue); listCtor = ImpM(listCtor); listAdd = ImpM(listAdd); holderCtor = ImpM(holderCtor); holderSetH = ImpM(holderSetH); holderSetS = ImpM(holderSetS); setHolder = ImpM(setHolder); // ---- reflection plumbing for the Features write -------------------- // A direct stfld into SoftwareProtection's private k__BackingField // throws FieldAccessException (CLR enforces cross-assembly non-public field // access); reflection via BindingFlags.NonPublic is exempt. Harvest the // GetTypeFromHandle/GetFields MemberRefs already present in this module so the // runtime type refs carry the CORRECT value-type encoding: RuntimeTypeHandle // and BindingFlags are structs, and a hand-built TypeReference defaults to // IsValueType=false -> emits ELEMENT_TYPE_CLASS instead of // ELEMENT_TYPE_VALUETYPE -> MissingMethodException at runtime. var mscorlibRef = (AssemblyNameReference)mod.TypeSystem.Object.Scope; var getTypeFromHandle = FindModuleRef(mod, "GetTypeFromHandle"); var sysType = getTypeFromHandle?.DeclaringType ?? new TypeReference("System", "Type", mod, mscorlibRef); var rtHandleType = getTypeFromHandle?.Parameters[0].ParameterType ?? new TypeReference("System", "RuntimeTypeHandle", mod, mscorlibRef) { IsValueType = true }; var getFields = FindModuleRef(mod, "GetFields", 1); // Type.GetFields(BindingFlags) -> FieldInfo[] var fieldInfoType = (getFields?.ReturnType as ArrayType)?.ElementType ?? new TypeReference("System.Reflection", "FieldInfo", mod, mscorlibRef); var bindingFlags = getFields?.Parameters[0].ParameterType ?? new TypeReference("System.Reflection", "BindingFlags", mod, mscorlibRef) { IsValueType = true }; // instance: Type.GetField(String, BindingFlags) -> FieldInfo MethodReference getField = new MethodReference("GetField", fieldInfoType, sysType) { HasThis = true }; getField.Parameters.Add(new ParameterDefinition("name", ParameterAttributes.None, mod.TypeSystem.String)); getField.Parameters.Add(new ParameterDefinition("bindingAttr", ParameterAttributes.None, bindingFlags)); // instance: FieldInfo.SetValue(Object, Object) -> void MethodReference setValue = new MethodReference("SetValue", mod.TypeSystem.Void, fieldInfoType) { HasThis = true }; setValue.Parameters.Add(new ParameterDefinition("obj", ParameterAttributes.None, mod.TypeSystem.Object)); setValue.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, mod.TypeSystem.Object)); var licTypeRef = TR("License"); // TypeRef scope SP, for ldtoken // ---- rebuild body -------------------------------------------------- var body = m.Body; body.Instructions.Clear(); body.Variables.Clear(); var il = body.GetILProcessor(); var lLic = AddLocal(body, dnaCtor.DeclaringType); // DNAFusion var lFeats = AddLocal(body, listAdd.DeclaringType); // List // var lic = new DNAFusion(); E(il, OpCodes.Newobj, dnaCtor); E(il, OpCodes.Stloc, lLic); // var feats = new List(); E(il, OpCodes.Newobj, listCtor); E(il, OpCodes.Stloc, lFeats); // feats.Add(new Feature { Name=..., Value=... }); x3 EmitFeatureAdd(il, lFeats, featCtor, featSetName, featSetValue, listAdd, "Flex", FlexValue); EmitFeatureAdd(il, lFeats, featCtor, featSetName, featSetValue, listAdd, "FlexMobile", FlexMobileValue); EmitFeatureAdd(il, lFeats, featCtor, featSetName, featSetValue, listAdd, "WebUsers", WebUsersValue); // typeof(License).GetField("k__BackingField", 0x24).SetValue(lic, feats) // 0x24 = BindingFlags.Instance(4) | BindingFlags.NonPublic(32) E(il, OpCodes.Ldtoken, licTypeRef); E(il, OpCodes.Call, getTypeFromHandle); E(il, OpCodes.Ldstr, "k__BackingField"); E(il, OpCodes.Ldc_I4, 0x24); E(il, OpCodes.Callvirt, getField); E(il, OpCodes.Ldloc, lLic); E(il, OpCodes.Ldloc, lFeats); E(il, OpCodes.Callvirt, setValue); // lic.Holder = new Holder { HaspKey = "", SoftKey = softKey }; E(il, OpCodes.Ldloc, lLic); E(il, OpCodes.Newobj, holderCtor); E(il, OpCodes.Dup); E(il, OpCodes.Ldstr, ""); E(il, OpCodes.Callvirt, holderSetH); E(il, OpCodes.Dup); E(il, OpCodes.Ldstr, softKey); E(il, OpCodes.Callvirt, holderSetS); E(il, OpCodes.Callvirt, setHolder); // this.License = lic; E(il, OpCodes.Ldarg_0); E(il, OpCodes.Ldloc, lLic); E(il, OpCodes.Callvirt, setLicense); E(il, OpCodes.Ret); // ---- write + verify ----------------------------------------------- // Preload embedded-resource bytes so the deferred writer doesn't re-read // them from the (soon-closed) original stream (avoids BadImageFormatException). foreach (var r in mod.Resources.OfType()) r.GetResourceData(); var backup = path + ".orig"; if (!File.Exists(backup)) File.Copy(path, backup); var tmp = path + ".new"; asm.Write(tmp, new WriterParameters { }); // write to a new file (same-file write truncates the read stream) File.Delete(path); File.Move(tmp, path); var check = AssemblyDefinition.ReadAssembly(path); var m2 = check.MainModule.GetType("OpenOptions.dnaFusion.Flex.ServiceManager") .Methods.First(x => x.Name == "LoadLicense" && x.Parameters.Count == 0); Console.WriteLine($"OK patched {Path.GetFileName(path)}"); Console.WriteLine($" backup -> {backup}"); Console.WriteLine($" LoadLicense() -> {m2.Body.Instructions.Count} IL instructions, {m2.Body.Variables.Count} locals"); Console.WriteLine($" SoftKey -> {softKey}"); Console.WriteLine(" Restart the Flex service to apply."); return 0; } // Append an instruction and return it. // 0.11.6 hides the Instruction ctor; use the static Instruction.Create factory. private static Instruction E(ILProcessor il, OpCode op, object operand = null) { Instruction i = operand switch { null => Instruction.Create(op), MethodReference mr => Instruction.Create(op, mr), FieldReference fr => Instruction.Create(op, fr), string s => Instruction.Create(op, s), int n => Instruction.Create(op, n), VariableDefinition v => Instruction.Create(op, v), TypeReference t => Instruction.Create(op, t), Instruction ins => Instruction.Create(op, ins), _ => throw new InvalidOperationException($"Unsupported IL operand type: {operand.GetType()}") }; il.Append(i); return i; } private static VariableDefinition AddLocal(MethodBody body, TypeReference type) { var v = new VariableDefinition(type); body.Variables.Add(v); return v; } // Finds a compiler-emitted MemberRef to List::Add somewhere in this module and // returns it. Its Parameters[0].ParameterType is the correctly-formed generic VAR // "!0" — the shape the CLR requires in MemberRef signatures for generic methods. private static MethodReference FindListAddTemplate(ModuleDefinition mod) { foreach (var t in mod.Types) foreach (var mth in t.Methods) { if (mth.Body == null) continue; foreach (var ins in mth.Body.Instructions) if (ins.Operand is MethodReference mr && mr.Name == "Add" && mr.DeclaringType is GenericInstanceType git && git.ElementType.FullName == "System.Collections.Generic.List`1" && mr.Parameters.Count == 1) return mr; } throw new InvalidOperationException("No compiler-emitted List::Add MemberRef found in target module."); } // Finds any MemberRef with the given name (and optional parameter count) already // referenced by code in this module. Used to harvest correctly-encoded refs for // runtime types (e.g. value types) instead of hand-building them. private static MethodReference FindModuleRef(ModuleDefinition mod, string name, int? paramCount = null) { foreach (var t in mod.Types) foreach (var mth in t.Methods) { if (mth.Body == null) continue; foreach (var ins in mth.Body.Instructions) if (ins.Operand is MethodReference mr && mr.Name == name && (paramCount == null || mr.Parameters.Count == paramCount)) return mr; } return null; } private static void EmitFeatureAdd(ILProcessor il, VariableDefinition feats, MethodReference cctor, MethodReference setName, MethodReference setValue, MethodReference add, string name, string value) { E(il, OpCodes.Ldloc, feats); E(il, OpCodes.Newobj, cctor); E(il, OpCodes.Dup); E(il, OpCodes.Ldstr, name); E(il, OpCodes.Callvirt, setName); E(il, OpCodes.Dup); E(il, OpCodes.Ldstr, value); E(il, OpCodes.Callvirt, setValue); E(il, OpCodes.Callvirt, add); // NOTE: List.Add returns void — no pop. (A pop here causes stack // underflow -> InvalidProgramException at JIT time.) } }