Pull to refresh

Comments 42

Хабр удаляет ссылки на изображения в конце статьи. Дублирую тут:

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


Пакуем в картинки, и запускаем:
По крайней мере для меня это выглядит настолько гениально, что сложно сказать что-нибудь вразумительное :) Автор — молодец!
На самом деле вы купились на котят.
Замечательно, красиво и пятнично! Порадовали :)
Почему-то эти фаршированные котики меня пугают
Можно заменить на картинки фаршированных индеек, но it и котики неразрывно связаны.
Я думаю картинки с котами ещё больше мотивируют хакера докопаться до оригинала.
А потом пришел строгий админ и удалил картинки с котиками. Потому что бухгалтер не должен хранить своих любимых котиков в рабочих директориях. /лирика
Там все таки нет смешения с картинкой, просто дозапись в конец. Но идея интересная.
Расскажите подробнее, почему формат PNG не подошел. Он же поддерживает и хранение chroma и alpha-данных как индексированными, так и прямым числовым представлением. Во втором случае отклонения значений в каналах в процессе сжатия/распаковки исключены. Чем вы жали PNG?
Берем png картинку. Например, такую, от хабраюзера:


И выполняем следующий код:
string pngFile = @"O:\cat.png";
string pngOutFile = @"O:\catnew.png";
using (Image img = Image.FromFile(pngFile))
{
    var bmp = new Bitmap(img.Width, img.Height, PixelFormat.Format32bppArgb);
    using (Graphics gr = Graphics.FromImage(bmp))
    {
        gr.DrawImage(img, new Rectangle(0, 0, img.Width, img.Height));
    }
    bmp.Save(pngOutFile, ImageFormat.Png);
}

Формат идентичный для оригинала и результата — Format32bppArgb, никаких манипуляций нет, просто перезаписал. В итоге оригинал 26Кб, результат 21Кб. Можно даже не сравнивать HEX редактором… Соответственно данные будут битыми.

Использую стандартный GDI+.
Вы делаете предположение о битости данных только на основе размера файла?.. Это может быть просто лучший алгоритм сжатия. Или выкинутые метаданные.
Это для быстрого примера привел, конечно я пробовал сравнивать. Данные отличаются, на 1-2 бита, и не все, но отличаются. И изменение происходит именно в методе GdipSaveImageToFile. Вполне вероятно что есть другие библиотеки, используя которых можно без потерь работать, пока не было времени поизучать.

Тут описана эта проблема в GDI+
То, что размер файла на выходе получился меньше говорит о том, что коэффициент сжатия оказался выше или были отброшены дополнительныеметаданные. Значения пикселей хранятся в числовом виде вне зависимости от того, какая гамма у отображающего устройства (т.е., грубо говоря, если вы сохраните файл, в котором знчения по каналам = (1,0,0), то на выходе получите файл, залитый таким же красным цветом, т.е. (1,0,0).

Судя по всему, проблема из-за того, что GDI+, написанный «на отъе*ись», меняет при сохранении значения в каналах. Чудеса. 2014-й год на дворе, а .NET до сих пор работает с изображениями будто мы только выбрались из ФИДО…
Попробуйте теперь этот же файл (21 кб) так обработать. Там может быть разница в энкодерах.
Много кода
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        var orig = File.ReadAllBytes("9c63b8a4e0f74341b6484d481d434a02.png");
        var modified = Modify(orig);
        Console.WriteLine("Packed sizes: original = {0}, modified = {1}", orig.Length, modified.Length);

        Compare(orig, modified);

        orig = Unpack(orig);
        modified = Unpack(modified);
        Console.WriteLine("Unpacked sizes: original = {0}, modified = {1}", orig.Length, modified.Length);

        Compare(orig, modified);

        if (Enumerable.SequenceEqual(orig, modified))
            Console.WriteLine("Unpacked data is equal");
        else
            Console.WriteLine("Unpacked data not equal");
    }

    private static byte[] Modify(byte[] orig)
    {
        var ms = new MemoryStream();
        using (var image = Image.FromStream(new MemoryStream(orig)))
            image.Save(ms, ImageFormat.Png);
        /*using (var bmp = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb))
        {
            using (Graphics gr = Graphics.FromImage(bmp))
                gr.DrawImage(image, new Rectangle(0, 0, image.Width, image.Height));
            bmp.Save(ms, ImageFormat.Png);
        }*/
        return ms.GetBuffer();
    }

    private static byte[] Unpack(byte[] orig)
    {
        var ms = new MemoryStream();
        using (var image = Image.FromStream(new MemoryStream(orig)))
            image.Save(ms, ImageFormat.Bmp);
        return ms.GetBuffer();
    }

    private static void Compare(byte[] orig, byte[] modified)
    {
        using (var img1 = (Bitmap)Image.FromStream(new MemoryStream(orig)))
        using (var img2 = (Bitmap)Image.FromStream(new MemoryStream(modified)))
        {
            int h = img1.Height, w = img1.Width;
            if (h != img2.Height || w != img2.Width)
            {
                Console.WriteLine("Image sizes is different");
                return;
            }

            for (int x = 0; x<w; x++)
                for (int y=0; y<h; y++)
                    if (img1.GetPixel(x, y) != img2.GetPixel(x, y))
                    {
                        Console.WriteLine("\tat ({0},{1}): {2:x8} != {3:x8}", x, y, img1.GetPixel(x, y), img2.GetPixel(x, y));
                        return;
                    }
        }

        Console.WriteLine("Images are equal");
    }
}

Как показал эксперимент, виноват во всем DrawImage — именно он искажает картинку. Вероятно, дело в том, что он рисует картинку с учетом возможных аффинных трансформаций или просто масштабирования — и даже в случае, когда этого не требуется, всплывают мелкие ошибки округления. Сами по себе методы сохранения и загрузки работают корректно.

Packed sizes: original = 27314, modified = 21121
Images are equal
Unpacked sizes: original = 262198, modified = 262198
Images are equal
Unpacked data not equal


Следует также отметить, что сравнивать изображения нужно по-пиксельно, а не по-байтово, потому что последний метод находит ложные отличия (отличия в метаданных).
Спасибо, опробую и отпишу.
Заработало c PNG. Дополнил конец статьи. Спасибо! Изображение, кстати, обрабатываю способом отсюда, таким способом отрабатывает тоже правильно.
Я пытался его использовать — но что-то тоже не срослось. Сейчас уже не помню, но вроде бы он пытается подогнать физические размеры картинки в соответствии с установленным dpi. А значит, ошибки округления никуда не деваются.
Если есть желание доводить до ума, советую выкинуть вообще GDI и работать напрямую с файлами. BMP — довольно простой формат.
Напомнило случай, когда ребятам, участвовавшим в конкурсе 10k apart, не хватало тех самых 10кб для js-кода. Тогда они (вдохновившись наработками других иностранных коллег) написали небольшой скрипт, который кодировал байты js-кода в точки для png-картинки:

image
jquery.1.8.2.min.js — 91кб до сжатия, 34кб в виде png-файла

Эту картинку можно легко нарисовать на canvas-е и считать js-код обратно, выполнив его eval-ом. Совместив эту идею с вашей котофускацией, можно сделать неплохой обфускатор js файлов, особенно если запутать код самого дешифратора.
Никак не могу найти ни английскую статью с js-кодером, ни аналогичной статьи на русском. Не поделитесь ссылками? Желательно на англоязычный ресурс
Немного не понял вот этот момент: «после чего заменяем единицы в rgb компонентах на получившиеся числа»:
как при этом (7,155,72) превратится в (1, 155, 78)?
Есть байт: 158, раскладываем его на 1, 5 и 8. Обозначим a1, a2, a3, то есть чтобы получить исходный байт: b = a1*100 + a2*10 + a3
Есть пиксель, его rgb значения (7, 155, 72),

Получаем новые значения по формуле:
REDnew = (REDold/10)*10 + a1 = (7/10)*10 + 1 = 0+1 = 1
GREENnew = (GREENold/10)*10 + a2 = (155/10)*10 + 5 = 150 + 5 = 155
BLUEnew = (BLUEold/10)*10 + a3 = (72/10)*10 + 8 = 70 + 8 = 78

И получим новое значение пикселя (1, 155, 78). Заменив для каждой компоненты RGB значения единиц.
О, спасибо, теперь ясно. Просто заменяем последний значащий десятичный символ на наш.
А что делаете, если значения RGB пикселя находятся в диапазоне 250-255, а в единицах надо закодировать 6, 7, 8 или 9?
В коде проверка есть, в этом случае перед суммированием значение RGB компоненты для которой может быть переполнение предварительно уменьшается на 10.
логично просто вычесть десять, и из 257 получить 247, на глаз все равно незаметно.
А не проще было отбрасывать последние 4 бита канала и писать туда 4 бита данных?
Вопрос автору (мои поздравления с удачным пожатием PNG!) или людям сведущим:
Я сам только игры программировал, со сборками .net не работал особо. Вопрос такой: если этот лоадер (Booster) или хотя бы именно дешифровщик написать, скажем, на плюсах, можно ли каким-то образом расшифрованные массивы байтов с содержимым IL-сборок передать из сишного лоадера второму лоадеру, но уже написанному на .net, чтобы уже тот запускал, собственно, приложение?
По идее это еще увеличило бы крутость решения, т.к. декомпилить лоадер, скомпиленный в нативный код — задача не в пример сложнее, чем ковырять байткод в ILSpy.
Можно написать на плюсах дешифровку и никуда не передавать, а запустить непосредственно из нативного кода. Ниже реализация:

Реализация хостинга в нативном коде
// .h
#pragma once
#include <tchar.h>
#include <windows.h>
#include <metahost.h>
#pragma comment(lib, "mscoree.lib")

class DotNetHostDispatcher
{
private:
	ICLRMetaHost *pMetaHost;
	ICLRRuntimeInfo *pRuntimeInfo;
	ICLRRuntimeHost *pClrRuntimeHost;
	PCWSTR pszStaticMethodName;
	PCWSTR pszStringArg;
	DWORD dwLengthRet;
public:
	HRESULT hr;
	// Samples of pszVersion: L"v2.0.50727", L"v4.0.30319"
	DotNetHostDispatcher(PCWSTR pszVersion);
	// pszAssemblyPath - path to .NET dll
	void StartMain(PCWSTR pszAssemblyPath, PCWSTR pszClassName, PCWSTR pszStringArg);
	// Release
	~DotNetHostDispatcher();
};

//.cpp
#include "DotNetHostDispatcher.h"

DotNetHostDispatcher::DotNetHostDispatcher(PCWSTR pszVersion)
{
	pMetaHost = NULL;
	pRuntimeInfo = NULL;
	pClrRuntimeHost = NULL;
	pszStaticMethodName = L"Main";
	pszStringArg = L"--start";

	hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
	if (FAILED(hr)) throw L"CLRCreateInstance failed w/hr 0x%08lx";
	hr = pMetaHost->GetRuntime(pszVersion, IID_PPV_ARGS(&pRuntimeInfo));
	if (FAILED(hr)) throw L"ICLRMetaHost::GetRuntime failed w/hr 0x%08lx";
	BOOL fLoadable;
	hr = pRuntimeInfo->IsLoadable(&fLoadable);
	if (FAILED(hr)) throw L"ICLRRuntimeInfo::IsLoadable failed w/hr 0x%08lx";
	if (!fLoadable) throw L".NET runtime cannot be loaded";
	hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pClrRuntimeHost));
	if (FAILED(hr)) throw L"ICLRRuntimeInfo::GetInterface failed w/hr 0x%08lx";
	hr = pClrRuntimeHost->Start();
	if (FAILED(hr)) throw L"CLR failed to start w/hr 0x%08lx";
}

void DotNetHostDispatcher::StartMain(PCWSTR pszAssemblyPath, PCWSTR pszClassName, PCWSTR pszStringArg)
{
	hr = pClrRuntimeHost->ExecuteInDefaultAppDomain(pszAssemblyPath, pszClassName, pszStaticMethodName, pszStringArg, &dwLengthRet);
}

DotNetHostDispatcher::~DotNetHostDispatcher()
{
	if (pMetaHost)
	{
		pMetaHost->Release();
		pMetaHost = NULL;
	}
	if (pRuntimeInfo)
	{
		pRuntimeInfo->Release();
		pRuntimeInfo = NULL;
	}
	if (pClrRuntimeHost)
	{
		pClrRuntimeHost->Release();
		pClrRuntimeHost = NULL;
	}
}




И пример вызова:
#include "DotNetHostDispatcher.h"

int _tmain(int argc, _TCHAR* argv[])
{ 
        // Сборку будет исполнять вторым фреймворком
	DotNetHostDispatcher * dispatcher = new DotNetHostDispatcher(L"v2.0.50727");
        // Хостинг сборки и вызов метода Main с передачей параметра
	dispatcher->StartMain(L"DotNetAppLib.dll", L"DotNetAppLib.DotNetClass", L"--start");
	dispatcher->~DotNetHostDispatcher();
	return 0;
}


P.S. я плюсы уже плохо помню, код может быть не особого качества.
Когда-то давно писал скрипт для преобразования файл-картинка, чтобы хостить файлы в виде картинок.
Можно потрогать
Статью читал по диагонали, поэтом заранее прошу извинить, если вопрос глупый.

Если сдампить запущенный процесс, то весь ли MSIL в нём будет и все ли метаданные, или только часть?
Будут все данные.
Тогда какой в этом смысл? Чем это лучше обычной обфускации?
По крайней мере легкий ступор хакеру-неофиту обеспечен.
Это больше пятничный пост, он не предлагает замену стандартной обфускации. Собственно я в посте нигде не делаю сравнения способов обфускации друг с другом и с показанным методом. Просто делюсь решением запуска проекта хранящегося в виде картинок.
Sign up to leave a comment.

Articles