Cotfuscation of executable .net code


    Typically, a deployed application in the file system looks something like this:

    Completely unprotected from tools like a reflector or IlSpy, but what if it becomes like this:

    At least an easy stupor is ensured for a neophyte hacker. It looks nice, and antiviruses are not interested.


    In the pictures there is a Booster.exe module, let's talk about it, this is a very small program and it can do the following:
    • pack a set of dll'ok into a set of pictures, while compressing and encrypting
    • load dlls from the set of pictures, find the types with the Main method in them and run them

    This takes into account that assemblies can use each other (for this, the AssemblyResolve event of AppDomain is used).

    If you remove all the checks, the execution start code looks like this:

    Packing code like this:
    var config = CommandLineParser.Parse(args);
    AssemblyProvider.PackAssemblies(config["img"], config["imgout"], config["asm"]);


    Let's start with packaging, the PackAssemblies method takes the path to the directory with pictures, the path to the directory where to put the pictures with the code, and the path to the directory with the assemblies. First of all, we collect information about the pictures, we need to know how much data can be stored in them:
    Dictionary imagesVolume = new Dictionary();
    var imageFiles = Directory.GetFiles(imagesFolder, "*.*").Where(file => file.ToLower().EndsWith("bmp") || 
    file.ToLower().EndsWith("png") || file.ToLower().EndsWith("jpg") || file.ToLower().EndsWith("jpeg")) .ToList();
    foreach (string file in imageFiles)
    internal void Append(string imageFile)
                using (Image img = Image.FromFile(imageFile))
                    _imagesVolume.Add(imageFile, img.Width * img.Height - 4);

    The formula is simple, multiply the width by height and subtract 4 bytes from the data size. ImageSet additionally monitors the looping through the pictures (so that they are different), and for finding a picture that can accommodate the assembly size.

    Now it remains to go over the assemblies, download each in binary form, and merge with a picture that can accommodate the size of the data. Briefly for a separate assembly like this:
    byte[] packed = AssemblyPacker.Pack(file);
    string imageFile = imageSet.FindImage(packed.Length);
    if (imageFile != null)
          using (Image img = Image.FromFile(imageFile))
                  Bitmap bmp = SteganographyProvider.Injection(img, packed);

    AssemblyPacker compresses the byte array using GZipStream , as assembly size is aligned in blocks, this can significantly reduce the size.
    An example of the end of the assembly file:, the

    part that is finished with zeros is highlighted, and below another two hundred and two zeros, which will be perfectly compressed.
    Then the compressed array is 'encrypted', I did not use cryptographic providers, the array is simply xor'ed with a set of pseudorandom numbers, while the size of the array is the generator. Those. when repeating the encryption operation, we get the original array. The algorithm is very simple, but gives a result similar to white noise.

    Raw Data

    Distribution : Post-Processing Distribution Close to Normal

    Having received the packaged assembly, we proceed to the most interesting part, merging with the picture. Where we stumble upon the first problems. .NET while preserving the image uses WinApi method GdipSaveImageToFile ( you can see here ), which receives in addition to the image links and the file name is also a reference to the handlers (encoder'y) image, which may include archiver, optimizers, etc. Each of which can change the pixel values, which will damage the stored data. The simplest solution would seem to not pass handlers, but the GdipSaveImageToFile methodand itself smart, in addition to the transferred list of handlers, it also focuses on the image format (clsid, the third parameter of the method), while, for example, for png, it can generally hammer on our list of handlers and decide what to use on its own. I still could not find a combination in which lossless image compression would work, so I use the following solution:
    Bitmap bmp = SteganographyProvider.Injection(img, packed);
    FieldInfo fi = bmp.GetType().GetField("nativeImage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    object d = fi.GetValue(bmp);
    IntPtr nativeImage = (IntPtr)d;
    Guid clsid = FindEncoder(ImageFormat.Bmp.Guid).Clsid;
    GdipSaveImageToFile(new HandleRef(bmp, nativeImage), outImagefile, ref clsid, new HandleRef(null, IntPtr.Zero));

    Those. no matter what input format, when saving, I point out that we are working with BMP, while the final image is not optimized in any way, which is immediately evident from the final image weight. But you can be sure that you read the pixels that are written.

    Having dealt with storage, we proceed to the merger. For 32-bit images, each pixel is encoded with four bytes (argb), for 24-bit images with three (rgb), initially the algorithm focused on writing in 4 bytes using the alpha channel, which allowed us to obtain images by eye that were almost indistinguishable from the original, but in total Having come to bmp, he refused the alpha channel.

    So, each byte from our array is mapped to one pixel, while the byte is divided into three numbers, the number of units, tens and hundreds, i.e. for byte 158 we get three numbers, (1, 5, 8), after which we replace the units in the rgb components with the resulting numbers, for example, the pixel (7,155,72) turns into (1, 155, 78).

     pix = MixByteAndPixel(pix, ByteDecomposition(data[index], false));
    ((byte*)bmd.Scan0)[offset] =;
    ((byte*)bmd.Scan0)[offset + 1] =;
    ((byte*)bmd.Scan0)[offset + 2] =;

    The merge algorithm is as follows, add 4 bytes at the beginning of the array with the data, in which the length of the array is written. We go through the image and merge bytes with pixels, if there are free pixels in the image, merge with random values ​​(otherwise the border where the data ended will be clearly traced on the final image, as an empty area is visible on CD / DVD discs). Save the resulting image.

    The original (left) and the picture with the assembly inside (right):


    To start, we need Booster.exe and the resulting images. Just put Booster.exe in the directory with pictures and run it. Or run with the parameter img = path_to_pictures.

    At the same time, all packing operations in the reverse order are applied to pictures:
    1. We read the pixel values, for each pixel from the rgb component we take units, and restore the original byte.
    2. After the first four bytes we get the size of the remaining data.
    3. We read the source array with the packed and encrypted assembly. We ignore the rest of the image.
    4. Decrypt the original array.
    5. Unzip the array and get the assembly directly in raw form.
    6. Download assembly
    7. If loading is successful, register the assembly in the AssemblyResolver, in case the assemblies use each other

     internal static AssembliesSet LoadAssemblies(string imagesFolder)
                AssembliesSet set = new AssembliesSet();
                foreach (string file in Directory.GetFiles(imagesFolder, "*.bmp"))
                    byte[] data = null;
                    using (Image img = Image.FromFile(file))
                        data = SteganographyProvider.Extraction(img);
                    data = AssemblyPacker.UnPack(data);
                return set;
    internal void TryAppendAssembly(byte[] rawAssembly)
                Assembly asm;
                    asm = Assembly.Load(rawAssembly);
                catch { }

    And the final part, we look in the loaded collections for available types for the Main method, and call it:
    internal void CallMain()
                foreach (var type in CollectExportedTypes())
                    MethodInfo main = type.GetMethod("Main");
                    if (main != null)
                        ParameterInfo[] paramsInfo = main.GetParameters();
                        object[] parameters = new object[paramsInfo.Length];
                        for (int i = 0; i < paramsInfo.Length; i++)
                            parameters[i] = GetDefaultValue(paramsInfo[i].ParameterType);
                        main.Invoke(null, parameters);


    Let's create a project with three assemblies, A, B and C, with A and B using assembly C. Like this, for example:
    / * the picture in the comment below * /

    Put it in the pictures, and run:
    / * the picture in the comment below * /

    As you can see from the conclusion, all the assemblies were loaded and the code was executed, including the dependence of the assemblies on each other (calling the Run method of the CommonClass class).


    Initially, I really wanted to use the png format, for some reason I was sure that it uses lossless compression algorithms, but it turned out there are losses, insignificant for the image, but critical for steganography. If someone knows how to save Bitmap to png without loss, please unsubscribe.

    I hope it was interesting. You can download and play here .

    PS Something strange is going on with pictures on Habr, steadily loses links.

    UPD . Thanks to habrauser mayorovpfor commenting on PNG, you can really save it by standard means without loss. Now the algorithm takes into account the presence of the alpha channel, and if it is, decomposes each byte into 4 components to reduce output distortion. For an example of a byte equal to 158, the decomposition will be as follows:

    In the first step, we divide hundreds, tens, and units, we obtain a vector (a1, a2, a3, a4) with values ​​(1, 5, 8, 0), then we find a4 depending on conditions :
    if (a2 >= 5 && a3 >= 5)
          a4 = 2;      a2 -= 5;      a3 -= 5;
    else if (a2 >= 5)
         a4 = 3;       a2 -= 5;
    else if (a3 >= 5)
          a4 = 4;      a3 -= 5;
          a4 = 5;

    The final vector will be like this (1, 0, 3, 2).

    Thus, all unit values ​​of rgba components will be in the range of 0-5, which in theory should smooth the picture. You can come up with a more optimal encoding.

    Also popular now: