Форум программистов, компьютерный форум CyberForum.ru

.NET: Разбор CrackMe и обзор некоторых моментов работы JIT'а

Войти
Регистрация
Восстановить пароль
Рейтинг: 5.00. Голосов: 3.

.NET: Разбор CrackMe и обзор некоторых моментов работы JIT'а

Запись от NickoTin размещена 09.11.2013 в 19:10
Обновил(-а) NickoTin 02.02.2016 в 10:26 (Из-за проблем с вложениями, ссылка на проект заменена ссылкой на репозиторий)
Метки crackme, csharp, dotnet, jit, secure

Здесь не будет детального рассмотрения структуры .NET PE, работы JIT'а или CLR, только то что напрямую относится к принципам работы CrackMe с небольшими отступлениями

Сейчас мы рассмотрим принцип работы вот этого CrackMe и познакомимся с некоторыми внутренними структурами JIT'а, которые используются в момент трансляции CIL to ASM.



Перед тем как начать разбор посоветовал бы Вам обзавестись парой инструментов:
  1. Исходники SSCLI, реализация ядра .NET 2, также эти исходники известны как проект Rotor. Они помогут лучше ознакомиться с внутренностями JIT'а и в общем с .NET'ом самостоятельно;
  2. CFF Explorer - замечательный инструмент для редактирование PE-файлов;
  3. Любой декомпилятор .NET сборок (по желанию).



I. Краткий обзор структуры .NET PE с использованием CFF Explorer

Что такое .NET PE - это обычный PE (Portable Executable) файл в котором помимо всего прочего хранятся метаданные сборки и CIL код. Что такое метаданные? - Метаданные это атрибуты, их названия, названия методов, полей модификаторы доступа, аргументы и прочее в том же духе.
То что файл имеет .NET метаданные можно узнать сравнив значение первых 4-х байт структуры Data Directory (1), под номером 15, с нулём - это смещение таблицы CLR Runtime Header (в CFF Explorer называется .NET Metadata Directory (2)) которая хранит информацию о версии CLR, подписи сборки, о типе сборки (mixed/managed, x86/x64), смещении метаданных и прочее. Она используется при инициализации CLR.

Нажмите на изображение для увеличения
Название: clr_data_directory.png
Просмотров: 281
Размер:	246.6 Кб
ID:	1866 Нажмите на изображение для увеличения
Название: dotnet_directory_metadata.png
Просмотров: 253
Размер:	175.1 Кб
ID:	1867

Поле MetaData RVA (Relative Virtual Address) - это поле содержащее смещение метаданных. Сами мы считать это смещение не будем, за нас это уже сделал CFF Explorer, к тому же представив все данные в удобно читаемом виде.
Название: metadata.png
Просмотров: 1289

Размер: 4.3 Кб


Структура метаданных представляет из себя набор потоков данных (stream [оф. документация]). В основном присутствует 5 потоков. Рассмотрим 3 из них с которыми нам предстоит работать:
  • #~ - основной поток с описанием классов, методов, параметров, типов, ссылок на связанные сборки
    и прочей информацией. В общем централизованное хранилище данных, которое тесно связано с другими потоками. Представлен в виде набора таблиц;
  • #Strings - список строк со служебной информацией: названия классов, методов, атрибутов и прочее. Строки хранятся в UTF-8 с null-terminated символом на конце;
  • #US - список пользовательских строк в кодировке UTF-16. Пользовательские строки это строковые константы которые мы используем в программе.
    Как с ними взаимодействует код? Через опкод ldstr. Аргументом этого опкода является mdtString - токен описывающий строку. Токен представляет из себя 4 байтовое число, где старший байт тип токена, а младшие 3 байта его значение. У ldstr этот токен имеет вид: 0x70XXXXXX, где XXXXXX - это смещение строки в потоке #US.
Потоков может быть и больше. Некоторые обфускаторы добавляют свои потоки с данными.

В потоке #~ мы будем работать с таблицами TypeDef, Field, Method, Param. В каждой из них есть поле Name в котором указано смещение имени типа в потоке #Strings. Это поле понадобится нам для проведения обфускации имен, подробнее об этом поговорим в IV разделе. Выбрав элементы в таблице Method и нажав ПКМ можно просмотреть дизассемблированный (CIL - псевдо-ассемблер) листинг метода, его физическое смещение в файле и размер.

На этом моменте остановимся в части рассмотрения .NET PE. Более детальную информацию можно узнать изучив ECMA-335 (CLI Specification) или спросив в комментариях, постараюсь ответить.

II. Основные структуры и методы входящие в процесс трансляции CIL to ASM

Скачав исходники Rotor можно сразу погрязнуть в огромном количестве файлов, переходя от одного к другому по includ'ам, но нам понадобятся всего несколько из них: corjit.h, corinfo.h

Отправной точкой является экспортируемая из mscorjit.dll (< .NET 4) и clrjit.dll (>= .NET 4) функция getJit. Мы будем работать с .NET 4, т.е. с библиотекой clrjit.dll. Эта функция возвращает ссылку на интерфейс ICorJitCompiler, наша задача добраться до метода compileMethod и перехватить его.
Прототип функции:
C++
1
extern "C" ICorJitCompiler* __stdcall getJit();
он до сих пор не менялся от версии к версии, но вот интерфейс ICorJitCompiler претерпел некоторые изменения.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*******************************************************************************
 * ICorJitCompiler is the interface that the EE uses to get IL byteocode converted
 * to native code.  Note that to accomplish this the JIT has to call back to the
 * EE to get symbolic information.  The IJitInfo passed to the compileMethod
 * routine is the handle the JIT uses to call back to the EE
 *******************************************************************************/
 
// .NET < 4
class ICorJitCompiler
{
public:
    virtual CorJitResult __stdcall compileMethod (
            ICorJitInfo                 *comp,               /* IN */
            struct CORINFO_METHOD_INFO  *info,               /* IN */
            unsigned /* CorJitFlag */   flags,               /* IN */
            BYTE                        **nativeEntry,       /* OUT */
            ULONG                       *nativeSizeOfCode    /* OUT */
            ) = 0;
 
    virtual void __stdcall clearCache() = 0;
    virtual BOOL __stdcall isCacheCleanupRequired() = 0;
};
Если в версии mscorjit он содержит 3 функции, то в clrjit уже 5. К нашему счастью разработчики решили не менять интерфейс кардинально, поэтому в первом слоте таблицы виртуальных функций (далее vftable) всё также compileMethod - функция, вершина айсберга, отвечающая за трансляцию CIL кода в машинные инструкции текущей платформы. Её-то мы и будем перехватывать для того чтобы расшифровать код перед трансляцией. Остальные функции нас не интересуют.

Если не сильно вдаваться в детали реализации интерфейса, то это стркутура с адресами vftable. В нашем случае она представляет из себя структуру содержащую одно поле - ссылку на необходимую нам vftable. Если перевести на C, то получится следующий код:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct ICorJitCompilerVtbl
{
    CorJitResult (__stdcall * compileMethod)(
            struct ICorJitCompiler      *pThis,              /* IN */
            struct ICorJitInfo          *comp,               /* IN */
            struct CORINFO_METHOD_INFO  *info,               /* IN */
            unsigned /* CorJitFlag */   flags,               /* IN */
            BYTE                        **nativeEntry,       /* OUT */
            ULONG                       *nativeSizeOfCode    /* OUT */
            );
 
    void (__stdcall * clearCache)(
            ICorJitCompiler *pThis /* IN */
            );
    BOOL (__stdcall * isCacheCleanupRequired)(
            ICorJitCompiler *pThis /* IN */
            );
} ICorJitCompilerVtbl;
 
struct ICorJitCompiler
{
    const struct ICorJitCompilerVtbl *lpVtbl;
};
т.е. чтобы добраться до таблицы нужно прочитать значение поля lpVtbl структуры ICorJitCompiler. Также стоит отметить появившийся дополнительный аргумент у функций: struct ICorJitCompiler *pThis - это ссылка на текущий экземпляр класса который вызывает метод. Запомним эти моменты, мы вернемся к ним позднее.

ICorJitInfo это огромный интерфейс из которого можно узнать подробную информацию о методе (классе/поле/модуле). Конкретно здесь (в разборе) он не используется, но при организации защиты через него можно получить всю информацию для определения метода который транслируется, например getMethodName, getMethodClass, getMethodModule, getMethodDefFromMethod. Правда перед использованием придется проверить под отладчиком/дизассемблерем индекс метода в виртуальной таблице.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*****************************************************************************
 * ICorStaticInfo contains EE interface methods which return values that are
 * constant from invocation to invocation.  Thus they may be embedded in
 * persisted information like statically generated code. (This is of course
 * assuming that all code versions are identical each time.)
 *****************************************************************************/
 
class ICorStaticInfo : public virtual ICorMethodInfo, public virtual ICorModuleInfo,
                       public virtual ICorClassInfo,  public virtual ICorFieldInfo,
                       public virtual ICorDebugInfo,  public virtual ICorArgInfo,
                       public virtual ICorLinkInfo,   public virtual ICorErrorInfo
{
    // ...
};
 
/*****************************************************************************
 * ICorDynamicInfo contains EE interface methods which return values that may
 * change from invocation to invocation.  They cannot be embedded in persisted
 * data; they must be requeried each time the EE is run.
 *****************************************************************************/
 
class ICorDynamicInfo : public virtual ICorStaticInfo
{
    // ...
}
 
/*********************************************************************************
 * a ICorJitInfo is the main interface that the JIT uses to call back to the EE and
 *   get information
 *********************************************************************************/
 
class ICorJitInfo : public virtual ICorDynamicInfo
{
    // ...
}

Главную роль в функции compileMethod играет ссылка на структуру CORINFO_METHOD_INFO, которая содержит информацию о методе, и самое важное - ссылку на массив байт-кода (ILCode) загруженного из сборки и его размер (ILCodeSize):

C++
1
2
3
4
5
6
7
8
9
10
11
12
struct CORINFO_METHOD_INFO
{
    CORINFO_METHOD_HANDLE       ftn;
    CORINFO_MODULE_HANDLE       scope;
    BYTE *                      ILCode;
    unsigned                    ILCodeSize;
    unsigned short              maxStack;
    unsigned short              EHcount;
    CorInfoOptions              options;
    CORINFO_SIG_INFO            args;
    CORINFO_SIG_INFO            locals;
};
Этот самый массив нам и нужно изменять перед отработкой транслятора.

На этом необходимая теоретическая часть заканчивается, теперь переходим к реализации.

III. Реализация защиты

Для того чтобы понять как победить CrackMe нужно понять принцип работы его защиты, поэтому для начала реализуем базовый функционал CrackMe. После понимания этого скорее всего начнут приходить мысли о том как эту систему поломать и получить необходимые данные. Начнём...

Импортируем все необходимые структуры и прототипы функций. В коде активно используются указатели поэтому нужно включить возможность использования неуправляемого кода в свойствах проекта. Использование unsafe делает код более лаконичным.

C#
1
2
3
4
5
6
7
8
9
10
        [DllImport( "clrjit.dll" )]
        private static extern void* getJit ( );
 
        [DllImport( "kernel32.dll", SetLastError = true )]
        private static extern bool VirtualProtect (
            void* lpAddress,
            void* dwSize,
            uint flNewProtect,
            uint* lpflOldProtect
            );
Функция VirtualProtect нам нужна для изменения прав доступа областей памяти на которых располагается таблица виртуальных функций и загруженный из сборки CIL код. По умолчанию на этих областях стоят права PAGE_READONLY (0x2), мы будем менять на PAGE_READWRITE (0x4).

Описываем структуру CORINFO_METHOD_INFO и делегат compileMethod:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        [StructLayout( LayoutKind.Sequential )]
        private struct CORINFO_METHOD_INFO
        {
            public void* ftn;
            public void* scope;
            public byte* ILCode;
            public uint ILCodeSize;
            public ushort maxStack;
            public ushort EHcount;
            public uint options;
            // Эти поля нам не понадобятся
            // public CORINFO_SIG_INFO args;
            // public CORINFO_SIG_INFO locals;
        };        
        
        [UnmanagedFunctionPointer( CallingConvention.StdCall )]
        private delegate int CompileMethod (
            void* pThis,
            void* compHnd,
            CORINFO_METHOD_INFO* info,
            uint flags,                 // CorJitFlag
            byte** entryAddress,        // Разыменовав указатель получим адрес
                                        // по которому находится
                                        // сгенерированные машинные инструкции
            uint* nativeSizeOfCode      // Размер сгенерированного кода
            );
Опишем структуру HookInfo которая будет хранить адрес на оригинальную функцию compileMethod и на наш перехватчик, в этой же структуре сохраним адрес по которому располагается vftable интерфейса ICorJitCompiler.

C#
1
2
3
4
5
6
        struct HookInfo
        {
            public CompileMethod OriginalCompileMethod;
            public CompileMethod MyCompileMethod;
            public void* pJit;
        }
Теперь рассмотрим сам перехват:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
        private static HookInfo mHookInfo;
 
        private static uint Protect ( void* address, uint protection, void* size )
        {
            if ( !VirtualProtect( address, size, protection, &protection ) )
                throw new Win32Exception();
            return protection;
        }
 
        private static void Main ( )
        {
            // Получаем адрес vftable
            mHookInfo.pJit = *(void**)getJit();
            // Сохраняем адрес нашего перехватчика
            mHookInfo.MyCompileMethod = DetouredCompileMethod;
            // Подготавливаем наш перехватчик к работе
            // другими словами говорим CLR что этот метод нужно подготовить к исполнению
            // иначе мы получим StackOverflowException во время выполнения
            RuntimeHelpers.PrepareDelegate( mHookInfo.MyCompileMethod );
            // Меняем права доступа первых 4-х байт vftable ICorJitCompiler
            // на ReadWrite для изменения адреса compileMethod на наш
            uint p = Protect( mHookInfo.pJit, 0x04, (void*)0x4 );
            {
                // Разыменовывая vftable мы получим первый слот в таблице, т.е. адрес compileMethod
                // Сохраняем его в HookInfo
                mHookInfo.OriginalCompileMethod = Marshal.GetDelegateForFunctionPointer( new IntPtr( *(void**)mHookInfo.pJit ), typeof( CompileMethod ) ) as CompileMethod;
                RuntimeHelpers.PrepareDelegate( mHookInfo.OriginalCompileMethod );
                // Заменяем первый слот в таблице на наш перехватчик
                *(void**)mHookInfo.pJit = (void*)Marshal.GetFunctionPointerForDelegate( mHookInfo.MyCompileMethod );
            }
            // Восстанавливаем права доступа
            Protect( mHookInfo.pJit, p, (void*)0x4 );
            
            new Test().Do();
        }

И самое главное - перехватчик:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
        static int DetouredCompileMethod (
            void* pThis,
            void* compHnd,
            CORINFO_METHOD_INFO* info,
            uint flags,
            byte** nativeAddress,
            uint* nativeSizeOfCode )
        {
            // Возвращаемое значение
            int nRet;
            byte* code = info->ILCode;
            uint size = info->ILCodeSize;
            
            // Меняем права доступа на всю область в которой расположен код
            uint p = Protect( code, 0x4, (void*)size );
            {
                // Здесь должен быть алгоритм расшифровки кода
 
                nRet = mHookInfo.OriginalCompileMethod( pThis, compHnd, info, flags, nativeAddress, nativeSizeOfCode );
                // [Antidump]
                // Обнуляем адрес по которому располагался код
                info->ILCode = null;
                // Затираем расшифрованный код
                for ( uint i = 0; i < size; i++ )
                    code[i] = 0xFF;
            }
            Protect( code, p, (void*)size );
            
            return nRet;
        }

На этом описание принципов работы CrackMe завершено. Реализацию класса Test приводить не стану, её мы увидим ниже.

Проект с базовой реализацией: https://bitbucket.org/sstregg/jithook

IV. Разбор CrackMe

з.ы. Большая часть защиты построена вручную ввиду небольшого приложения и небольших затрат по человеко-минутам, потому не стоит кидаться помидорами все проделанные ниже манипуляции достаточно легко автоматизируются, достаточно иметь базовые знания о строении .NET PE.

Попробовав открыть CrackMe в декомпиляторе, мы увидим ад из имён - везде 1.
Используя CFF Explorer этого можно добиться как-минимум 2 способами:
  1. Самый быстрый и лёгкий способ. Как мы уже рассматривали выше у каждого значения в таблицах TypeDef, Field, Method, Param есть поле Name и оно ссылается на поток #Strings, таким образом изменив одно наше значение в потоке #Strings и сославшись на него в таблицах мы изменим все имена типов.
  2. Долгий и опасный способ. Меняем каждое значение в потоке #Strings на которое ссылается поле Name - долго и нудно, да и еще чревато проблемам описанными ниже.
Возможные проблемы:
  • На один элемент в #Strings может быть несколько ссылок в таблицах, таким образом если имя типа совпадает с именем уже существующего в другой сборке (на которую есть ссылка), то после замены во время выполнения мы получим исключение о том что не найден тип, т.к. нам в таком случае нужно будет заменить и имя типа в другой сборке.

Попробовав открыть методы класса Test получим либо Exception, либо молчание, в редких случаях декомпиляторы будут пытаться разобрать тот бред что там находится.
Метод работы CrackMe нам уже известен, поэтому попробуем его расшифровать статически. Открыв сборку в декомпиляторе найдём метод состоящий из 6 параметров, это нужный нам перехватчик, в нём достаточно легко прослеживается какие действия совершаются над массивом перед исполнением оригинальной compileMethod:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    // numPtr - массив с кодом
    // num2 - длина кода    
    
    // Если метод начинается с байта 0xFF, то метод зашифрован
    if (numPtr[0] == 0xFF)
    {
        byte num7 = numPtr[1];
        // Если 2-й байт не 0xA и не 0x2, то будет исключение ExecutionEngineException
        // если же он корректный тогда заменяем первые 2 байта
        if (num7 == 0xA)
        {
            numPtr[0] = 0x72;
            numPtr[1] = 0x1;
        }
        else if (num7 == 0x2)
        {
            numPtr[0] = 0x28;
            numPtr[1] = 0x1E;
        }
        else
            throw new ExecutionEngineException();
        
        // Проходимся по оставшемуся массиву
        // Это простое обратимое шифрование с зависимостью от
        // длины кода и текущей позиции байта
        for (uint k = 2; k < num2; k++)
        {
            numPtr[k] = (byte) (numPtr[k] ^ (num2 + k));
        }
    }



Отлично, у нас есть алгоритм шифрования/дешифровки, теперь вытащив код из сборки на диске можно легко его расшифровать и подменить взамен существующего.
Теперь нужно найти методы, нам известно их начало, это байты FF 0A и FF 02 для соответствующих методов. Можно поискать самостоятельно эти последовательности, но есть шанс что их будет очень много Воспользуемся CFF Explorer.
Скопировать код из сборки можно двумя способами:
  1. Перейдём к нужному методу и дизассемблируем его (ПКМ на методе Disassemble Method). Для зашифрованных методов код либо не будет отображен, либо будет не полностью, не суть, нам главное увидеть то что это нужные методы и узнать их смещение и длину. Переходим на вкладку HexEditor и переходим на нужное смещение, вуаля - код перед нами, выделяем участок ПКМ -> Copy -> C#/Java Array.
  2. Метод посложнее, т.к. тут придётся разбираться со строением заголовков методов и искать их длину самостоятельно. Существуют Fat и Tiny методы. Tiny это короткие методы без локальных переменных/обработчиков исключений/и прочих требований, я так понимаю это методы которые можно без проблем заинлайнить. Fat - это соответственно полноценные методы. Их заголовки существенно различаются и там много нюансов.
    Найти начало заголовка метода можно выбрав метод в таблице Method и просмотрев его RVA. Скопируем его значение и перейдем на вкладку Address Converter, введём в поле RVA наше значение - мы переместил в начало метода. Найти начало кода несложно, но вот с длиной придётся потрудится. Здесь я это не рассматриваю, по этому поводу всё описано в параграфе II.25.4 Common Intermediate Language physical layout документа ECMA-335.
    Нажмите на изображение для увеличения
Название: address_converter.png
Просмотров: 214
Размер:	89.2 Кб
ID:	1865

Зашифрованный код мы скопировали, теперь расшифруем его, метод получится что-то вроде этого:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
        private static void Main ( string[] args )
        {
            byte[] numPtr0 =
            {
                0xFF, 0x0A, 0xA3, 0xA4, 0xD5, 0x8E, 0xBA, 0xA8, 0xA9, 0xA0, 0x83, 0xB2, 0xAD, 0xAE, 0xA5, 0x98,
                0xAE, 0xB2, 0xB3, 0xBE, 0xDA, 0x96, 0xB7, 0xB8, 0xB3, 0xB0, 0xB9, 0xCE, 0xAA, 0xBE, 0xBF, 0xB0,
                0xE9, 0xCE, 0xC3, 0xC4, 0xC3, 0xCD, 0xC1, 0x46, 0xA0, 0xCD, 0x45, 0xA5, 0xE3, 0xDF, 0xBD, 0xB1,
                0xD1, 0xD2, 0xA3, 0xFC, 0xC8, 0xD6, 0xD7, 0xD2, 0xF1, 0xFB, 0xDB, 0xDC, 0xD7, 0xF8, 0xF5, 0xF6,
                0xED, 0xC9, 0xF3, 0xE2, 0xED, 0xE0, 0xEF, 0x79, 0xF6, 0xD0, 0xE3, 0xB4, 0x8C, 0x3C, 0x73, 0xF8,
                0xE9, 0xAA, 0xFF, 0xFC, 0xF3, 0x78, 0x9E, 0xCA, 0x13, 0xFC, 0x75, 0x95, 0xFA, 0x70, 0x96, 0x28,
                0x23, 0x02, 0x03, 0x0E, 0x08, 0x10, 0x14, 0x0C, 0x22, 0x2B, 0x0C, 0x1D, 0x09, 0x9F, 0x09, 0x01,
                0x15, 0x83, 0x3D, 0x05, 0x67, 0x77, 0x17, 0x18, 0x69, 0x32, 0x06, 0x1C, 0x1D, 0x14, 0x37, 0x01,
                0x21, 0x22, 0x29, 0x02, 0x0F, 0x37, 0x23, 0x3F, 0x71, 0x39, 0x2F, 0x3D, 0x29, 0x27, 0x1D, 0xEA,
                0x43, 0x49, 0x33, 0x34, 0x45, 0x1E, 0x2A, 0x38, 0x39, 0x30, 0x13, 0x23, 0x3D, 0x3E, 0x35, 0x66,
                0x6B
            };
 
            byte[] numPtr1 =
            {
                0xFF, 0x02, 0x2D, 0x2E, 0x25, 0x33, 0x5E, 0x12, 0x33, 0x34, 0x3F, 0x3C, 0x21, 0x33, 0x12, 0x29,
                0x3D, 0x3B, 0x3B, 0x39, 0xAE, 0x60, 0xE9, 0x42, 0x43, 0x44, 0x42, 0x1F, 0x26, 0x9A, 0xD5, 0x4D,
                0x53, 0x14, 0x46, 0x49, 0x49, 0xDE, 0x38, 0x60, 0xB4, 0x52, 0x7F
            };
 
            Decrypt( numPtr0 );
            var m0 = ToHex( numPtr0 );
            Decrypt( numPtr1 );
            var m1 = ToHex( numPtr1 );
 
        }
 
        private static string ToHex ( byte[] buff )
        {
            var sb = new StringBuilder();
 
            for ( int i = 0; i < buff.Length; i++ )
            {
                sb.AppendFormat( "{0:X2}", buff[i] );
            }
 
            return sb.ToString();
        }
 
        static void Decrypt ( byte[] numPtr )
        {
            if ( numPtr[0] == 0xff )
            {
                switch ( numPtr[1] )
                {
                case 0x2:
                    numPtr[0] = 0x28;
                    numPtr[1] = 0x1E;
                    break;
 
                case 0xA:
                    numPtr[0] = 0x72;
                    numPtr[1] = 0x1;
                    break;
 
                default:
                    throw new ExecutionEngineException();
                }
                for ( uint k = 2; k < numPtr.Length; k++ )
                {
                    numPtr[k] = (byte)(numPtr[k] ^ (numPtr.Length + k));
                }
            }
        }
В результате мы получаем HEX-код в строковом виде, на которые нужно заменить зашифрованный код. Просмотрев после этого код в декомпиляторе можно увидеть алгоритм расшифровки ключа



Провернуть всё тоже самое можно и под отладчиком поставив точку останова на оригинальном методе трансляции и сдампив код из памяти. Для это цели подойдет любой отладчик позволяющий ставить бряки и просматривать память.




Наверное стоит сделать выводы:
[+] Плюсы метода:
  • Дешифровка кода на лету;
  • Возможность написать нативный загрузчик который будет нести в контейнере полностью зашифрованную сборку, её методы, сохранив весь алгоритм дешифровки в нативной среде.
[-] Минусы:
  • Метод недокументированный, что может повлечь разнообразные необъяснимые проблемы;
  • В данном виде метод легко обходимый. Погрузившись глубже в compileMethod и поставив перехваты там, можно усложнить процесс перехвата кода сторонними средствами и собственно самим взломщикам.

На этом всё Спасибо за внимание.

P.S. Жду ключ к CrackMe

Использованная литература:
  1. .NET Internals and Code Injection
  2. ECMA-335
  3. Overview of Metadata
Вложения
Тип файла: zip JitHook.zip (4.6 Кб, 155 просмотров)
Размещено в C# .NET, .NET
Просмотров 2247 Комментарии 3
Всего комментариев 3

Комментарии

  1. Старый комментарий
    Аватар для Psilon
    Мда, а я пытался все это разрулить с одним только рефлектором и божьей помощью Ну еще IDA приплел, но он не сильно помог. Хотя мог бы догадаться найти нужные байты, зная хотя бы примерно смещение.

    Защита неплохая, главное что нестандартная... Если еще повозиться с ntfs-потоками, то можно еще забавнее сделать.
    Запись от Psilon размещена 09.11.2013 в 19:45 Psilon вне форума
  2. Старый комментарий
    Аватар для Psilon
    А пробовали атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)] использовать? Тогда если например много однообразных проверок выполняется, вместо того, чтобы курочить один метод проверки придется выковыривать из всех заинлайненых вызовов проверки.
    Запись от Psilon размещена 10.11.2013 в 15:02 Psilon вне форума
  3. Старый комментарий
    Аватар для NickoTin

    Не по теме:

    Извиняюсь за долгое молчание, был в больнице, и всю следующую неделю буду



    Цитата:
    Сообщение от Psilon
    А пробовали атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)] использовать?
    А при чем здесь этот атрибут? Он если и будет применен то только в момент трансляции, до этого это такой же обычный метод, только с доп. метаданными. Если бы он инлайнился в IL, то да, но это нарушало бы общую концепцию связанную с метаданными, да и рефлексия бы тогда обломалась.

    p.s. на x64 тоже работает
    Запись от NickoTin размещена 15.11.2013 в 15:46 NickoTin вне форума
    Обновил(-а) NickoTin 15.11.2013 в 17:30
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Рейтинг@Mail.ru