📜 ⬆️ ⬇️

Packaging, Compression and Assembly Protection

latest article is available on makeloft.xyz

The materials of this article describe the mechanisms of composition, compression, dynamic loading, as well as the paths of elementary protection of .NET assemblies using standard tools of the Visual Studio development environment. However, it is possible that the following will be true to some extent for other software platforms.

For such purposes, many utilities have been created, including paid ones, but they often work as black boxes and the developer is only slightly able to control the process. Sometimes, after the layout, the applications refuse to start at all, and the reason is quite difficult to identify.
')
But, having certain knowledge, you can perform the whole process yourself with full control at each step and the ability to debug already packed assemblies ...

image

First of all, it is worth looking at an example of an application where these mechanisms are applied, besides protection, this is a text editor Poet . Externally, the program is a single exe file of a small size, although in fact it consists of several libraries.

Immediately it should be noted that in the packaging of assemblies there are many advantages in the absence of obvious minuses:
• Many assemblies can be glued together into one and even include everything in the executable file. The application becomes monolithic and stable, because none of its parts will be lost anywhere.
• the same assemblies are easy to compress (zip), after which the files will take up much less space.
• significantly speed up the launch of the application! Subjectively, Poet began to start 2-4 times faster on different machines, after compiling all the files into one.
• a primitive protection from prying eyes will appear, that is, by disassembling the file, the outsider will not see the whole code. He will need to extract the assembly, which requires some skills.
• there are convenient opportunities for more serious protection ...
• packaging of assemblies does not exclude the simultaneous use of unpacked versions, if it is necessary for something.
• it is possible to debug the assembly!

1. Dynamic loading of assemblies in the application domain

If assembly files are added to a separate resource file, for example, Assemblies.resx , then they will be just an array of bytes. Downloading them to the application domain is very simple using the small AssemblyLoader class just below. It is enough at the right time, usually when you start the application, to call

AssemblyLoader.ResolveAssemblies<Assemblies>(AppDomain.CurrentDomain); 


 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace Loader { public static class AssemblyLoader { public static void ResolveAssembly(this AppDomain domain, string assemblyFullName) { try { domain.CreateInstance(assemblyFullName, "AnyType"); } catch (Exception exception) { Console.WriteLine(exception.Message); } } public static void ResolveAssemblies<TResource>(this AppDomain domain, Func<byte[], byte[]> converter = null) { var assemblies = ResolveAssembliesFromStaticResource<TResource>(converter); ResolveAssemblies(domain, assemblies); } public static void ResolveAssemblies(this AppDomain domain, List<Assembly> assemblies) { ResolveEventHandler handler = (sender, args) => assemblies.Find(a => a.FullName == args.Name); domain.AssemblyResolve += handler; assemblies.ForEach(a => ResolveAssembly(domain, a.FullName)); domain.AssemblyResolve -= handler; } public static void ResolveAssembly(this AppDomain domain, Assembly assembly) { ResolveEventHandler handler = (sender, args) => assembly; domain.AssemblyResolve += handler; ResolveAssembly(domain, assembly.FullName); domain.AssemblyResolve -= handler; } public static List<Assembly> ResolveAssembliesFromStaticResource<TResource>(Func<byte[], byte[]> converter = null) { var assemblyDatyType = typeof (byte[]); var assemblyDataItems = typeof (TResource) .GetProperties(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) .Where(p => p.PropertyType == assemblyDatyType) .Select(p => p.GetValue(null, null)) .Cast<byte[]>() .ToList(); var assemblies = new List<Assembly>(); foreach (var assemblyData in assemblyDataItems) { try { var rawAssembly = converter == null ? assemblyData : converter(assemblyData); var assembly = Assembly.Load(rawAssembly); assemblies.Add(assembly); } catch (Exception exception) { Console.WriteLine(exception.Message); } } return assemblies; } } } 

The author of the article for a long time could not understand how to initiate the process of loading the assembly into the domain. When trying to create an unknown type instead of calling the domain event. AssemblyResolve + = handler; they simply fell, but the event itself was not caused. And the main magic, as it turned out, is in one line domain.CreateInstance (assemblyFullName, "AnyType"); In its time, it took several days spent hugging MSDN to find an example on the backside that revealed this secret.

2. At the same time, no one forbids compressing or encrypting arrays of bytes.

For compression, a very simple class Compressor may be useful. Only now it is necessary to initiate loading of assemblies a little differently.
 AssemblyLoader.ResolveAssemblies<Resources>( AppDomain.CurrentDomain, bytes => Foundation.Compressor.ConvertBytes(bytes)); 


 using System.IO; using System.IO.Compression; namespace Foundation { public static class Compressor { public static void ConvertFile(string inputFileName, CompressionMode compressionMode, string outputFileName = null) { outputFileName = outputFileName ?? (compressionMode == CompressionMode.Compress ? inputFileName + ".compressed" : inputFileName.Replace(".compressed", string.Empty)); var inputFileBytes = File.ReadAllBytes(inputFileName); ConvertBytesToFile(inputFileBytes, outputFileName, compressionMode); } public static void ConvertBytesToFile(byte[] inputFileBytes, string outputFileName, CompressionMode compressionMode = CompressionMode.Decompress) { var bytes = ConvertBytes(inputFileBytes, compressionMode); File.WriteAllBytes(outputFileName, bytes); } public static byte[] ConvertBytes(byte[] inputFileBytes, CompressionMode mode = CompressionMode.Decompress) { using (var inputStream = new MemoryStream(inputFileBytes)) using (var outputStream = new MemoryStream()) { if (mode == CompressionMode.Compress) using (var convertStream = new GZipStream(outputStream, CompressionMode.Compress)) inputStream.CopyTo(convertStream); if (mode == CompressionMode.Decompress) using (var convertStream = new GZipStream(inputStream, CompressionMode.Decompress)) convertStream.CopyTo(outputStream); var bytes = outputStream.ToArray(); return bytes; } } } } 

We also need an elementary console utility for archiving.

 using System; using System.IO.Compression; using System.Linq; using Foundation; namespace FileCompressor { class Program { private static void Main(string[] args) { try { args.Where(n => !n.ToLower().Contains(".compressed")) .ToList() .ForEach(n => Compressor.ConvertFile(n, CompressionMode.Compress)); args.Where(n => n.ToLower().Contains(".compressed")) .ToList() .ForEach(n => Compressor.ConvertFile(n, CompressionMode.Decompress)); } catch (Exception exception) { Console.WriteLine(exception.Message); Console.ReadKey(); } } } } 

Visual Studio allows you to execute an arbitrary command line before compiling a project and after. This is configured in the project properties. That is, we can configure everything so that after the library is assembled, its file is compressed if necessary (for example, by our archiver) and automatically copied to the right place (hit the resources). With proper configuration, everything will be fine, because the version loaded into the domain corresponds to the compiled version. This is important because if the application stops running after building or other problems arise, you can easily identify the cause and fix it using debugging.

This entire configuration process, no matter how difficult it may seem, is enough to do once, and then everything will be updated automatically when recompiling without the participation of the developer.

Ideally, for desktop applications, it is convenient to use the following scheme: all assemblies without compression should be included in the main executable file, and then a loader application should be created, in which this main executable file is already compressed. Compress multiple glued files more efficiently than each separately.

All keys are given in the article, you only need to figure out and apply knowledge in their projects. But if difficulties arise, it is possible to buy the source code of the Poet editor . In addition to these examples, they contain a number of other unique and useful solutions. It may be more cost effective for someone to spy on something there than a few days or weeks to search for and develop solutions on their own.

3. Now we have a smooth approach to the protection capabilities.

Assemblies are simply arrays of bytes with which you can do anything ... And if you complicate the decryption algorithm as much as possible, then it will not be so easy for an outsider to pull out the decoded assemblies. The decryption itself can be rendered into native C ++ code, or for web applications it is possible to obtain decryption keys from the server. Two problems remain - operating system processes can be debugged or a memory dump can be removed from them.

It is not difficult to defend against a debugger

  static class AntiDebugger { private static int Fails { get; set; } private static DateTime TimeStamp { get; set; } public static void Run() { TimeStamp = DateTime.Now; new Thread(ProtectThread).Start(); //new Thread(ProtectThread).Start(); } private static void ProtectThread() { while (true) { var now = DateTime.Now; if (TimeStamp.AddSeconds(5) < now) Fails++; else { TimeStamp = now; Fails = 0; } // One fail may attend when pc wake up from the sleep mode if (Fails >= 2) InitProtection(); Thread.Sleep(TimeSpan.FromSeconds(3)); } } private static void InitProtection() { Fails = 0; Console.WriteLine("Debugger detected!"); //Process.GetCurrentProcess().Kill(); //Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; //for (var i = 0; i < 100; i++) //{ // new Thread(() => { while (true) ; }) {Priority = ThreadPriority.Highest}.Start(); //} } } 

One or a couple of threads at a specified interval check and update the value of the TimeStamp variable. If the TimeStamp contains an old value, it indicates that the computer has come out of sleep mode, the clock has been reset, or there is an attempt to debug it. If the error is repeated two or more times, the last option is most likely, so you need to enable protection. You can simply crash your process or even cause the operating system to hang, simply by running a thousand or more threads with an infinite loop and high priority. This trick disrupts the system even on powerful multi-core computers, so you need to provide options for false positives and be careful.

You can try to use this technique to protect against memory dumping, since this is not an instant operation. Only here, instead of flows, it is necessary to use at least two processes that follow each other and when one of them stops, they immediately perform protective actions.

Results

I hope that the article will be useful for developers. Once again, all the key points are described in it well, it remains only to apply them in action. If any difficulties arise, you can always purchase the full source code of the Poet editor from the site makeloft.by .

PS Information for donations and thanks.

PPS Preview version of the free Foundation Framework library.

Source: https://habr.com/ru/post/247615/


All Articles