diff --git a/.gitignore b/.gitignore index 680e2bb..6827a25 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ coverage.cobertura.xml # Live 2023 R2 server credentials — never commit wonder-sql-vd03.txt + +# Reverse-engineering IL-rewrite output: derived AVEVA binaries, never commit +docs/reverse-engineering/dnlib-write-copy/ diff --git a/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs index 92c1b02..2b66549 100644 --- a/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs +++ b/tools/AVEVA.Historian.Grpc2023CaptureHarness/Program.cs @@ -40,8 +40,15 @@ namespace AVEVA.Historian.Grpc2023CaptureHarness return 1; } - // Resolve siblings from both the core bin dir and the gRPC-runtime msi-extract dir. - string[] probeDirs = Directory.Exists(msiX64) ? new[] { binDir, msiX64 } : new[] { binDir }; + // Resolve siblings from: (optional) IL-rewrite dir FIRST (so the instrumented + // Archestra.Historian.GrpcClient.dll + ReverseInstrumentation.dll win), then the core + // bin dir, then the gRPC-runtime msi-extract dir. + string? rewriteDir = GetOption(args, "--grpc-rewrite"); + var probeList = new System.Collections.Generic.List(); + if (!string.IsNullOrEmpty(rewriteDir) && Directory.Exists(rewriteDir)) probeList.Add(rewriteDir!); + probeList.Add(binDir); + if (Directory.Exists(msiX64)) probeList.Add(msiX64); + string[] probeDirs = probeList.ToArray(); AppDomain.CurrentDomain.AssemblyResolve += (_, e) => { string simpleName = new AssemblyName(e.Name).Name + ".dll"; diff --git a/tools/AVEVA.Historian.ReverseEngineering/Program.cs b/tools/AVEVA.Historian.ReverseEngineering/Program.cs index d5698ee..4c2787a 100644 --- a/tools/AVEVA.Historian.ReverseEngineering/Program.cs +++ b/tools/AVEVA.Historian.ReverseEngineering/Program.cs @@ -58,6 +58,7 @@ try "instrument-wcf-auth-context" => InstrumentWcfAuthContext(args), "instrument-wcf-writemessage" => InstrumentWcfWriteMessage(args), "instrument-wcf-readmessage" => InstrumentWcfReadMessage(args), + "instrument-grpc-nonstream" => InstrumentGrpcNonStream(args), "mark" => WriteMarker(args), "wcf-probe" => ProbeWcf(args), "wcf-cert-probe" => ProbeWcfCertificate(args), @@ -1363,6 +1364,92 @@ static Instruction[] CreateCClientBaseOpenConnectionLogInstructions( ]; } +static int InstrumentGrpcNonStream(string[] args) +{ + // Usage: instrument-grpc-nonstream [output.dll] + // M3 R3.1 capture: injects CaptureLogger.LogByteArray at the entry of + // GrpcHistoryClient.RegisterTags (the byte[] tagInfos input) and AddNonStreamValues (the + // byte[] inBuff) so a write driven through the native 2023 R2 client dumps both buffers. + // Strong-name note: the input is signed (AVEVA key); dnlib preserves the public-key identity + // so aahClientManaged still binds to the rewritten copy under the LoadFrom context (which + // does not re-verify the SN signature). Write the rewrite to a copy — never the bin original. + if (args.Length < 2) + { + Console.Error.WriteLine("Usage: instrument-grpc-nonstream [output.dll]"); + return 1; + } + + string sourcePath = args[1]; + string outputPath = args.Length > 2 + ? args[2] + : Path.Combine("docs", "reverse-engineering", "dnlib-write-copy", "grpc2023", "Archestra.Historian.GrpcClient.dll"); + + ModuleDefMD module = ModuleDefMD.Load(sourcePath); + TypeDef historyClient = module.GetTypes().FirstOrDefault(t => t.Name == "GrpcHistoryClient") + ?? throw new InvalidOperationException("GrpcHistoryClient type not found in the module."); + + MemberRefUser logByteArray = CreateLogByteArrayRef(module); + + var instrumented = new List(); + foreach ((string methodName, string phase) in new[] + { + ("RegisterTags", "Grpc.RegisterTags.tagInfos"), + ("AddNonStreamValues", "Grpc.AddNonStreamValues.inBuff"), + }) + { + // The input byte[] is "System.Byte[]"; the out byte[] params are "System.Byte[]&". + MethodDef method = historyClient.Methods.FirstOrDefault(m => + m.Name == methodName && m.HasBody + && m.Parameters.Any(p => !p.IsHiddenThisParameter && p.Type.FullName == "System.Byte[]")) + ?? throw new InvalidOperationException($"{methodName} (with a byte[] input param) not found."); + + dnlib.DotNet.Parameter bufParam = method.Parameters + .First(p => !p.IsHiddenThisParameter && p.Type.FullName == "System.Byte[]"); + + Instruction[] injected = + [ + Instruction.Create(OpCodes.Ldstr, phase), + Instruction.Create(OpCodes.Ldarg, bufParam), + Instruction.Create(OpCodes.Call, logByteArray), + ]; + + foreach (Instruction instruction in injected.Reverse()) + { + method.Body.Instructions.Insert(0, instruction); + } + + method.Body.MaxStack = (ushort)Math.Max((int)method.Body.MaxStack, 8); + instrumented.Add(new + { + Method = methodName, + Phase = phase, + Param = bufParam.Name, + Token = "0x" + method.MDToken.Raw.ToString("X8"), + }); + } + + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath))!); + if (module.IsILOnly) + { + module.Write(outputPath); + } + else + { + module.NativeWrite(outputPath); + } + + Console.WriteLine(JsonSerializer.Serialize(new + { + Source = Path.GetFullPath(sourcePath), + Output = Path.GetFullPath(outputPath), + Type = historyClient.FullName, + Instrumented = instrumented, + LoggerMethod = "LogByteArray", + }, CreateJsonOptions())); + + return 0; +} + static int InstrumentWcfWriteMessage(string[] args) { string sourcePath = args.Length > 1 ? args[1] : Path.Combine("current", "aahClientManaged.dll");