Pull to refresh

Котфускация исполняемого .net кода

Reading time 6 min
Views 65K
(пятница)

Обычно развернутое приложение в файловой системе выглядит как-то так:



Совершенно незащищенное от инструментов типа рефлектора или IlSpy, но что если оно станет таким:



По крайней мере легкий ступор хакеру-неофиту обеспечен. Приятно смотрится, и антивирусы не заинтересуются.

Коротко


На картинках присутствует модуль Booster.exe, о нем и поговорим, это совсем небольшая программка и умеет она следующее:
  • запаковать набор dll'ок в набор картинок, при этом сжав и зашифровав
  • загрузить из набора картинок dll'ки, найти в них типы с методом Main и запустить их

При этом учитывается что сборки могут друг друга использовать (для этого используется событие AssemblyResolve у AppDomain).

Если убрать все проверки, код запуска на исполнение выглядит так:
AssemblyProvider.LoadAssemblies(imagesFolder).CallMain();


Код упаковки так:
var config = CommandLineParser.Parse(args);
AssemblyProvider.PackAssemblies(config["img"], config["imgout"], config["asm"]);


Упаковка


Начнем с упаковки, метод PackAssemblies принимает путь к каталогу с картинками, путь к каталогу куда нужно сложить картинки с кодом, и путь к каталогу со сборками. Первым делом собираем информацию о картинках, нам нужно знать какой объем данных можно в них хранить:
Dictionary<string, long> imagesVolume = new Dictionary<string, long>();
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)
                imageSet.Append(file);

//ImageSet
internal void Append(string imageFile)
        {
            using (Image img = Image.FromFile(imageFile))
            {
                _imagesVolume.Add(imageFile, img.Width * img.Height - 4);
                _images.Add(imageFile);
            }
        }

Формула проста, ширину умножаем на высоту и отнимаем 4 байта под размер данных. ImageSet дополнительно следит за закольцованным пробеганием по картинкам (чтобы были разные), и за поиск картинки способной вместить размер сборки.

Теперь остается пробежаться по сборкам, загрузить каждую в бинарном виде, и слить с картинкой, способной вместить размер данных. Вкратце для отдельной сборки так:
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);
              //Save
       }
}


AssemblyPacker сжимает массив байт используя GZipStream, т.к. размер сборки выравнивается по блокам, это может значительно сократить размер.
Пример окончания файла сборки:

, выделена часть добитая нулями, и ниже еще сотни две нулей, что отлично сожмется.
Затем сжатый массив 'шифруется', я не стал применять криптопровайдеры, массив просто xor'ится с набором псевдослучайных чисел, при этом генератором служит размер массива. Т.е. при повторении операции шифрования получаем исходный массив. Алгоритм очень простой, но дает результат похожий на белый шум.

Рапределение данных в необработанной сборке:

Распределение после обработки близко к нормальному


Получив запакованную сборку переходим к самой интересной части, слияние с картинкой. Где и натыкаемся на первые проблемы. .NET при сохранении картинки использует WinApi метод GdipSaveImageToFile (можно тут посмотреть), который принимает помимо ссылки на изображение и имени файла, еще и ссылку на обработчики(encoder'ы) изображения, среди которых могут архиваторы, оптимизаторы и т.п. Каждый из которых может изменить значения пикселей, что повлечет повреждение хранимых данных. Самым простым решением казалось бы не передавать обработчики, но метод GdipSaveImageToFile и сам по себе умный, кроме передаваемого списка обработчиков он еще ориентируется и на формат изображения (clsid, третий параметр метода), при этом, например для png, он вообще может забить на наш список обработчиков и решить что применять самостоятельно. Мне так и не удалось найти сочетание при котором работало бы сжатие изображения без потерь, поэтому использую следующее решение:
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));


Т.е. неважно какой формат на входе, при сохранении я указываю считать что работаем с BMP, при этом конечное изображение никак не оптимизируется, что сразу видно по итоговому весу изображения. Зато можно быть уверенным что прочитаешь пиксели которые записал.

Разобравшись с хранением, переходим к слиянию. Для 32-хбитных изображений каждый пиксель кодируется четырмя байтами (argb), для 24-хбитных тремя (rgb), первоначально алгоритм ориентировался на запись в 4 байта с использованием альфа канала, что позволяло получать изображения на глаз почти неотличимые от оригинала, но по итогу придя к bmp, от альфа канала отказался.

Итак, каждый байт из нашего массива сопоставляем с одним пикселем, при этом, байт разбиваем на три числа, количество единиц, десяток и сотен, т.е. для байта 158 получим три числа, (1, 5, 8), после чего заменяем единицы в rgb компонентах на получившиеся числа, например пиксель (7,155,72) превратится в (1, 155, 78).

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


Алгоритм слияния получается следующий, в начало массива с данными добавляем 4 байта, в которых записана длина массива. Проходим по изображению и сливаем байты с пикселями, если в изображении остаются свободные пиксели, сливаем со случайными значениями (иначе на конечном изображении будет четко прослеживаться граница где закончились данные, как на CD/DVD болванках видна пустая область). Сохраняем полученное изображение.

Оригинал (слева) и картинка со сборкой внутри (справа):


Запуск


Для старта нам понадобится Booster.exe и итоговые изображения. Достаточно поместить Booster.exe в каталог с картинками и запустить его. Или запустить с параметром img=путь_к_картинкам.

При этом для картинок применяются все операции упаковки в обратном порядке:
  1. Считываем значения пикселей, у каждого пикселя из rgb компонент берем единицы, и восстанавливаем исходный байт.
  2. После первых четырех байт получаем размер оставшихся данных.
  3. Считываем исходный массив с запакованной и зашифрованной сборкой. Остаток изображения игнорируем.
  4. Расшифровываем исходный массив.
  5. Разархивируем массив и получаем непосредственно сборку в raw виде.
  6. Загружаем сборку
  7. При удачной загрузке регистрируем сборку в AssemblyResolver'е, на случай если сборки используют друг друга


 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);
                set.TryAppendAssembly(data);
            }
            return set;
        }
//AssembliesSet 
internal void TryAppendAssembly(byte[] rawAssembly)
        {
            Assembly asm;
            try
            {
                asm = Assembly.Load(rawAssembly);
                AssembliesResolver.Register(asm);
                _assemblies.Add(asm);
            }
            catch { }
        }


И заключительная часть, ищем в загруженных сборах по доступным типам метод Main, и вызываем его:
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);
                }
            }
        }


Тестируем


Сделаем проект с тремя сборками, A, B и С, при этом A и B будут использовать сборку C. Вот так, например:
/*картинка в комментарии ниже*/

Пакуем в картинки, и запускаем:
/*картинка в комментарии ниже*/

Как видно по выводу, все сборки загрузились и код исполнился, в том числе работает зависимость сборок друг от друга (вызов метода Run класса CommonClass).

Заключение


Изначально очень хотелось использовать png формат, почему то был уверен что он использует алгоритмы сжатия без потерь, однако оказалось потери есть, незначительные для изображения, но критичные для стеганографии. Если кто-то знает как можно сохранить Bitmap в png без потерь, прошу отписать.

Надеюсь было интересно. Скачать и поиграться можно тут.

P.S. Что-то странное твориться с картинками на Хабре, стабильно теряет ссылки.

UPD. Спасибо хабраюзеру mayorovp за комментарий насчет PNG, действительно можно стандартными средствами сохранять без потерь. Теперь алгоритм учитывает наличие альфа канала, и при его наличии раскладывает каждый байт на 4 составляющих для снижения искажений на выходе. На примере байта равного 158, разложение будет следующим:

На первом шаге разделим сотни, десятки и единицы, получаем вектор (a1, a2, a3, a4) со значениями (1, 5, 8, 0), затем находим a4 в зависимости от условий:
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;
}
else
      a4 = 5;

Конечный вектор будет таким (1, 0, 3, 2).

Таким образом, все значения единиц rgba компонент будут в пределах 0-5, что по идее должно сгладить картинку. Можно придумать и более оптимальную кодировку.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+118
Comments 42
Comments Comments 42

Articles