Encrypting executable causes exception in BinaryAssemblyInfo.GetAssembly - c#

I use a dongle to protect my executable. The dongle protects the software in two ways:
inside the code with calls to read/write the dongle memory (for example to store functional data), to encrypt/decrypt data with an algorithm resident in the dongle. The encryption key is writable only.
Encrypting the EXE file and using a loader that decrypts it through the dongle. If any debugger like softice is running, the software either terminates or does not start.
It would work well and make cheaper to buy the license than to crack my software and this is my only goal.
The problem is that I cannot serialize anymore! If I try, I get the following exception:
SerializationException
Source = mscorlib
Message = Unable to find assembly 'MyApp, Version=1.0.0.3, Culture=neutral, PublicKeyToken=null'.
TargetSite = System.Reflection.Assembly GetAssembly()
Stack =
System.Runtime.Serialization.Formatters.Binary.BinaryAssemblyInfo.GetAssembly()
System.Runtime.Serialization.Formatters.Binary.ObjectReader.GetType(BinaryAssemblyInfo assemblyInfo, String name)
System.Runtime.Serialization.Formatters.Binary.ObjectMap..ctor(String objectName, String[] memberNames, BinaryTypeEnum[] binaryTypeEnumA, Object[] typeInformationA, Int32[] memberAssemIds, ObjectReader objectReader, Int32 objectId, BinaryAssemblyInfo assemblyInfo, SizedArray assemIdToAssemblyTable)
System.Runtime.Serialization.Formatters.Binary.__BinaryParser.ReadObjectWithMapTyped(BinaryObjectWithMapTyped record)
System.Runtime.Serialization.Formatters.Binary.__BinaryParser.ReadObjectWithMapTyped(BinaryHeaderEnum binaryHeaderEnum)
System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run()
System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
I must use Binary Serialization due to the nature of the data to be persistent.
How can a solve this problem?

I dedicate an enormous amount of time to this problem and I got to a workaround, more than a true solution.
I share what I learned. The serialization needs to load the assembly that generated the permanent stream in order to know exactly the structure of the saved data. With "black-box" EXE encryption systems, the assembly is not available. One possible solution could be to write a custom BynaryFormatter: for sure it's not worth the effort.
The workaround is to put the classes to be serialized in a DLL that is not encrypted and then decrypted in memory at run time by the dongle. This idea derive from the suggestions that I have found for a similar problem: one wants to deserialize in application A data written from application B.

Related

Deserializing List<IList>

I'm migrating a set of applications from .Net Framework 4.7 to .Net 5.0, but I'm running into some issues with deserializion.
The example below illustrates the problem I'm facing. If both applications are .Net framework, it works fine. If both are .Net 5.0, it works fine. But if one application is .Net 4.7 and the other is 5.0, I get an exception when deserializing the type List<IList>.
.Net 5.0 application CreateData:
using System;
using System.Collections;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Test;
namespace CreateData
{
class Program
{
static void Main()
{
DataStructure dataStructure = new DataStructure();
ArrayList arrayList = new ArrayList();
arrayList.Add(42);
dataStructure.data.Add(arrayList);
// Open a stream for writing
FileStream fs = new FileStream(#"C:\DataFile.dat", FileMode.Create);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fs, dataStructure);
fs.Close();
Console.WriteLine("Data file created, press Enter to exit");
Console.ReadLine();
}
}
}
.Net Standard 2.0 library Lib:
using System;
using System.Collections;
using System.Collections.Generic;
namespace Test
{
[Serializable]
public class DataStructure
{
public DataStructure()
{
data = new List<IList>();
}
public List<IList> data;
}
}
.Net 4.7 application ReadData:
using System;
using System.Linq;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Test;
namespace ReadData
{
class Program
{
static void Main()
{
DataStructure dataStructure = new DataStructure();
// Open the data file
FileStream fs = new FileStream(#"C:\DataFile.dat",FileMode.Open);
// Construct the binary formatter
BinaryFormatter bf = new BinaryFormatter();
// deserialize
dataStructure = (DataStructure) bf.Deserialize(fs);
fs.Close();
// Announce success
Console.WriteLine("Value = {0}", dataStructure.data.First()[0]);
Console.WriteLine("Press enter to exit");
Console.ReadLine();
}
}
}
System.Runtime.Serialization.SerializationException: Unable to load
type System.Collections.Generic.List`1[[System.Collections.IList,
System.Private.CoreLib, Version=5.0.0.0, Culture=neutral,
PublicKeyToken=7cec85d7bea7798e]] required for deserialization. at
System.Runtime.Serialization.ObjectManager.CompleteObject(ObjectHolder
holder, Boolean bObjectFullyComplete) at
System.Runtime.Serialization.ObjectManager.DoNewlyRegisteredObjectFixups(ObjectHolder
holder) at
System.Runtime.Serialization.ObjectManager.RegisterObject(Object obj,
Int64 objectID, SerializationInfo info, Int64 idOfContainingObj,
MemberInfo member, Int32[] arrayIndex) at
System.Runtime.Serialization.Formatters.Binary.ObjectReader.RegisterObject(Object
obj, ParseRecord pr, ParseRecord objectPr, Boolean bIsString) at
System.Runtime.Serialization.Formatters.Binary.ObjectReader.RegisterObject(Object
obj, ParseRecord pr, ParseRecord objectPr) at
System.Runtime.Serialization.Formatters.Binary.ObjectReader.ParseObjectEnd(ParseRecord
pr) at
System.Runtime.Serialization.Formatters.Binary.ObjectReader.Parse(ParseRecord
pr) at
System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run()
at
System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler
handler, __BinaryParser serParser, Boolean fCheck, Boolean
isCrossAppDomain, IMethodCallMessage methodCallMessage) at
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream
serializationStream, HeaderHandler handler, Boolean fCheck, Boolean
isCrossAppDomain, IMethodCallMessage methodCallMessage) at
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream
serializationStream, HeaderHandler handler, Boolean fCheck,
IMethodCallMessage methodCallMessage) at
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream
serializationStream, HeaderHandler handler, Boolean fCheck) at
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream
serializationStream, HeaderHandler handler) at
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream
serializationStream) at ReadData.Program.Main() in
Is there any way to load the type System.Collections.Generic.List1[[System.Collections.IList, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]? What can I do to resolve this?
If at all possible, I would like to migrate one application at a time instead of migrating the entire code base in one go.
Thanks!
Congratulations! You have found one of the reason not to use BinaryFormatter. Other reasons include safety, performance and size. I would recommend to switch to just about anything else as soon as possible.
The recommended way to manage serialization is to separate the types used for serialization from the types that contain actual logic. This allow the respective types to be adapted for the respective purpose. Things like backward compatibility is simpler since things like adding properties are usually possible, and you have the option to keep multiple versions of the same type in case there are larger changes in the type structure.
I would also highly recommend setting up unit tests, to test that different types serializes/deserializes correctly. But unit tests are often also a very useful tool to learn what is possible or not with a serialization library.
You might be able to do something like type-mapping, but that is a huge cludge in my opinion. It might also help to change types to something like arrays instead of lists, but if you are changing the serialization format you are probably better of switching to something better. It might be possible to write a converter in .net 4.7 to convert between the old format and a newer format.

Deserialization in a CLR Function fails

I have written some routines for the phonetic conversion of a text in C#. These routines require a bunch aof defined rules for the conversion (search-string/replace-string). The idea was to store thes rules as an embedded ressource within the assembly and then read the rules from it. The starting point for the the deserialization is as follows
public static phonet42n.Core.Rules Deserialize(phonet42n.Core.Rules.Ressources ressource)
{
string ressourceName;
phonet42n.Core.Rules returnValue;
System.Xml.XmlReader reader;
System.Xml.Serialization.XmlSerializer xmlSerializer;
phonet42n.Core.SerializableRules serializeableRules;
returnValue = new phonet42n.Core.Rules();
switch (ressource)
{
case Ressources.German_01:
ressourceName = RESSOURCE_XML_GERMAN_01;
break;
case Ressources.German_02:
ressourceName = RESSOURCE_XML_GERMAN_02;
break;
default:
ressourceName = RESSOURCE_XML_GERMAN_01;
break;
}
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ressourceName))
{
using (reader = System.Xml.XmlReader.Create(stream))
{
xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(phonet42n.Core.SerializableRules));
serializeableRules = (phonet42n.Core.SerializableRules)xmlSerializer.Deserialize(reader);
}
}
foreach (phonet42n.Core.Rule entry in serializeableRules.Rules)
{
if (entry.SearchString != null && entry.SearchString.Length > 0)
{
returnValue.Add(entry.Index, entry);
}
}
return returnValue;
}
The application works fine when executed in an regular executable.
When executing the registered function in SQL Server I get the following error:
SELECT [dbo].[Phonet42n]('mayer', 1)
produces...
Meldung 6522, Ebene 16, Status 1, Zeile 22
.NET Framework-Fehler beim Ausführen der benutzerdefinierten Routine oder des benutzerdefinierten Aggregats 'Phonet42n':
System.InvalidOperationException: Fehler im XML-Dokument (3,4). ---> System.MethodAccessException: Fehler beim Versuch der Methode "Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReader1.Read8_SerializableRules(Boolean, Boolean)", auf Methode "phonet42n.Core.Rule..ctor()" zuzugreifen.
System.MethodAccessException:
bei System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck)
bei System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
bei System.Activator.CreateInstance(Type type, Boolean nonPublic)
bei System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, StackCrawlMark& stackMark)
bei System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)
bei System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture)
bei Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReader1.Read8_SerializableRules(Boolean isNullable, Boolean checkType)
bei Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReader1.Read19_Rules()
System.InvalidOperationException:
bei System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
bei System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
bei phonet42n.Core.Rules.Deserialize(Ressources ressource)
bei phonet42n.Core.Rules..ctor(Ressources ressource, Characters characters)
bei phonet42n.Core.HashTable..ctor(Ressources ressource)
bei phonet42n.Core.Match..ctor(Re...
Any idea?
Whenever there are security exceptions, you can first try setting the Assembly to PERMISSION_SET = EXTERNAL_ACCESS, and if that doesn't work you can try UNSAFE. However, if one is attempting to dynamically load an Assembly, then that is supposed to be forbidden even for Assemblies marked as UNSAFE.
Since the issue here is of wanting to include a set of rules, that could probably be done in another Assembly. Then the main Assembly can reference the one containing the rules, and you just load the one containing the rules into SQL Server first. This would allow both Assemblies to remain marked as SAFE.
Of course, if there is no pressing need to keep the rules separate, then you could also just place them directly into a Collection in a Class.
I did an undo on evreything except the constructor part (private/public). With or without signing the assembly I get the following message (10327):
CREATE ASSEMBLY for assembly 'phonet42n.Core' failed because assembly
'phonet42n.Core' is not authorized for PERMISSION_SET =
EXTERNAL_ACCESS. The assembly is authorized when either of the
following is true: the database owner (DBO) has EXTERNAL ACCESS
ASSEMBLY permission and the database has the TRUSTWORTHY database
property on; or the assembly is signed with a certificate or an
asymmetric key that has a corresponding login with EXTERNAL ACCESS
ASSEMBLY permission.
Unfortunately I'm not familiar with signing an assembly: I just checked the checkbox in the property pages of the project properties:
Don't know whether additional steps are missing.
Anyhow, as suggested in the message I then tried to create an asymmetric key using the following statements:
USE master;
GO
IF EXISTS (SELECT * FROM [sys].[syslogins] WHERE [sid] = SUSER_SID('SQLCLRTestLogin'))
BEGIN
PRINT 'Dropping Login...'
DROP LOGIN [SQLCLRTestLogin]
PRINT 'End'
END
GO
IF EXISTS (SELECT * FROM [sys].[asymmetric_keys] WHERE [name] = 'SQLCLRTestKey')
BEGIN
PRINT 'Dropping Asymmetric Key...'
DROP ASYMMETRIC KEY [SQLCLRTestKey]
PRINT 'End'
END
GO
BEGIN
PRINT 'Creating Login...'
CREATE ASYMMETRIC KEY [SQLCLRTestKey] FROM EXECUTABLE FILE = 'D:\phonet42n.net\Core\bin\Debug\phonet42n.Core.dll'
PRINT 'End'
PRINT 'Creating Asymmetric Key...'
CREATE LOGIN [SQLCLRTestLogin] FROM ASYMMETRIC KEY [SQLCLRTestKey]
PRINT 'End'
PRINT 'Granting Access...'
GRANT EXTERNAL ACCESS ASSEMBLY TO [SQLCLRTestLogin];
PRINT 'End'
END
GO
In case of a signed assembly these statements succeed, whereas in case of an unsigned assembly I get the following Error message (15208):
The certificate, asymmetric key, or private key file does not exist or
has invalid format.
By the way.. don't know whether this matters. The target .NET Framework aof the assemly is 4.5. Finally... coming to the initial error message: Adwaenyth was right with the missing public default construtor. The message was so far misleading, as the missing constructor does not affect the registrion of the assembly. You get the message only after a successfull registration at runtime, when making a call for the function phonet42n.
I really don't know, whether this is the solution by design. And I cannot not tell whether I opend the door with that for any potential threads.
I just can emphasize the the Stairway series articles especially on on SQLCLR. Excellent! It helps to get a understanding how SQLCLR works and what it is good for.
And (really) finally: the overall outcome for me is, that I will extract the rules from the XML and put load them directly by populating a collection as suggested by srutzky. Will be probably much more performant.

Compiler Issue in Windows 7: A generic error occurred in GDI+

We have an application that we need to begin testing and developing in Windows 7 environment. It works fine compiling under WinXP in VS2008, no problems. However when I went to compile it on a windows 7 machine using VS2008 today I get the following error:
Error 12 The "GenerateResource" task failed unexpectedly.
System.Runtime.InteropServices.ExternalException (0x80004005): A generic error occurred in GDI+.
at System.Drawing.Image.Save(Stream stream, ImageCodecInfo encoder, EncoderParameters encoderParams)
at System.Drawing.Image.Save(MemoryStream stream)
at System.Drawing.Image.System.Runtime.Serialization.ISerializable.GetObjectData(SerializationInfo si, StreamingContext context)
at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder)
at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder)
at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, Header[] inHeaders, __BinaryWriter serWriter, Boolean fCheck)
at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Header[] headers, Boolean fCheck)
at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph)
at System.Resources.ResourceWriter.WriteValue(ResourceTypeCode typeCode, Object value, BinaryWriter writer, IFormatter objFormatter)
at System.Resources.ResourceWriter.Generate()
at System.Resources.ResourceWriter.Dispose(Boolean disposing)
at System.Resources.ResourceWriter.Close()
at Microsoft.Build.Tasks.ProcessResourceFiles.WriteResources(IResourceWriter writer)
at Microsoft.Build.Tasks.ProcessResourceFiles.WriteResources(String filename)
at Microsoft.Build.Tasks.ProcessResourceFiles.ProcessFile(String inFile, String outFile)
at Microsoft.Build.Tasks.ProcessResourceFiles.Run(TaskLoggingHelper log, ITaskItem[] assemblyFilesList, List`1 inputs, List`1 outputs, Boolean sourcePath, String language, String namespacename, String resourcesNamespace, String filename, String classname, Boolean publicClass)
at Microsoft.Build.Tasks.ProcessResourceFiles.Run(TaskLoggingHelper log, ITaskItem[] assemblyFilesList, List`1 inputs, List`1 outputs, Boolean sourcePath, String language, String namespacename, String resourcesNamespace, String filename, String classname, Boolean publicClass)
at Microsoft.Build.Tasks.GenerateResource.Execute()
at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(ITaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask, Boolean& taskResult)
I cannot for the life of me run this one down. I have visited the msdn forums and find that it is an issue for a lot of people, but no consistent solution has been provided by MS or anyone else.
Has anyone seen this before and fixed it? Please advise is needed!
Thanks
I compiled using the command line msbuild and that identified the problem location.
It was a resx file in a class which had one unused image in it. I removed that and all was good again. Can build fine now.
After hours of troubleshooting, I opened and built the project using the VS 2010 command line prompt using MSBUILD "my solution file path here". This provided a more visible sequence of build events, and I found my project was failing just after it compiled form 16. The forms compiling appeared to be going in order of the objects in the solution explorer. So I checked the next form and found it contained a picture box. I also checked the form after that one and found I could not open the designer without errors(object reference not set to instance of object). So apparently, the problem was now two-fold.
To resolve I had to remove the picture box object from the first form I identified as problematic (it was not being used any way) as it appeared to be corrupted. I found the second form with the null reference exception was using a user control. The code for the constructor of the user control was attempting to pass an object to a container before calling InitializeComponent(). This created the null reference since the container hadn't event been created in InitializeComponent when the problematic code was called.
After resolving the issues in the above paragraph my solution compiled under Windows 7.
There is a GDI update for Windows 7, I think, if I'm not mistaken. Maybe that will solve the issue. Does your solution build using Msbuild on the commsnd line?
Do you use TIFF images in your app?
Some time ago, I also had a problem with TIFF images as part of reports that wouldn't compile under Win7 x64 although it compiled like a charm under Vista x86. The error message also involved GDI+. I saved the images under a different format (PNG) and the problem vanished.
At the time (around March 2011), I first ensured that my Windows was up-to-date but it didn't solve the problem. So maybe there is such an update as mentionned by Erik but it didn't come through Windows Update back then (Maybe it's newer).
Delete the resources from the resource manager (right click on each and delete) then add them back. for me it solve the problem.
i had same problem.
i just used msbuild myProject.sln command and it fixed and built successfully with no errors! now i can manually build my solution.
no need to reAdding my resources

Obtain reference to a UserControl from a different appdomain (composite UI)

How might I host a WinForms Control (or WPF Window for that matter) created in a different AppDomain on a form created in my application?
I'm trying to create a composite UI application that is a featureless shell/host for plugins, where those plugins run entirely in their own app domains, so I want to be able to get UserControl objects from those plugins to be "hosted" in some sort of container control on the host process's main form.
I thought I'd cracked it, but because conversations between appdomains involve MarshallByRefObject proxies, I can't take my first prototype approach of:
shellForm.Panel1.Controls.Add(proxyObjectForUserControlInOtherAssembly);
I've posted the exception I got from this at the end of the question.
I did have a go at getting the control directly:
var ctl = Control.FromHandle(proxyObjectForUserControlInOtherAssembly.Handle);
shellForm.Panel1.Controls.Add(ctl);
This gave predictable results. I got the hWnd okay, but Control.FromHandle() returned null. Presumably because the handle was created on a different appdomain.
I'd very much appreciate a poke in the right direction if I'm hopelessly off track with this. :)
Many thanks in advance.
System.Runtime.Remoting.RemotingException was unhandled by user code
Message=Remoting cannot find field 'parent' on type 'System.Windows.Forms.Control'.
Source=mscorlib
StackTrace:
at System.Object.GetFieldInfo(String typeName, String fieldName)
at System.Object.FieldGetter(String typeName, String fieldName, Object& val)
at System.Object.FieldGetter(String typeName, String fieldName, Object& val)
at System.Windows.Forms.Control.ControlCollection.Add(Control value)
at AppDomainTest.Shell.Form1.Display(Control control) in c:\temp\AppDomainTest\AppDomainTest\AppDomainTest.Shell\Form1.cs:line 24
at System.Runtime.Remoting.Messaging.StackBuilderSink._PrivateProcessMessage(IntPtr md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs)
at System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(IMessage msg, Int32 methodPtr, Boolean fExecuteInContext)
InnerException:
I have been helpfully pointed in one possible direction by a colleague: .NET Add-in Framework
The key point from that page for my purposes is as follows:
The assemblies for these segments are
not required to be in the same
application domain. You can load an
add-in into its own new application
domain, into an existing application
domain, or even into the host's
application domain. You can load
multiple add-ins into the same
application domain, which enables the
add-ins to share resources and
security contexts.

Howto take a glimpse into mscorlib?

I want to read data into RSAParameters structure ( RSAParameters ) and did check twice, that the data is correct. But still, I get an error "invalid data" exception for this:
bei System.Security.Cryptography.CryptographicException.ThrowCryptogaphicException(Int32 hr)
bei System.Security.Cryptography.Utils._ImportKey(SafeProvHandle hCSP, Int32 keyNumber, CspProviderFlags flags, Object cspObject, SafeKeyHandle& hKey)
bei System.Security.Cryptography.RSACryptoServiceProvider.ImportParameters(RSAParameters parameters)
How can I take a look into the source code to check why _ImportKeys is throwing an exception? I have no experience with .dll 'decryption'..are there any symbols to reference somewhere for visual studio 8.0? Thank you.
Use .NET Reflector to disassemble the DLL you want to examine.
You can also use JetBrains dotPeek. It is 100% free.

Categories