1. #1

    Исследование .NET программ через внедрение байт-кода MSIL

    1. Введение

    В этой статье, мы изучим внутренние составляющие платформы .NET Framework, испытывая новый алгоритм исследования .NET программ. На самом деле, существует несколько библиотек, которые позволяют исследовать .NET программы. Большинство из них устанавливают хук (ловушку) в код, сгенерированный после применения данного метода или путем модификации Assembly и сохранения результата модификации.
    Microsoft также предлагает Profile API, чтобы исследовать выполнение заданной программы. Однако API нужно обязательно активировать перед выполнением программы, задав специальные переменные окружения.
    Наша цель — исследовать программу, оставив нетронутым двоичный файл Assembly. И все это возможно, благодаря языку высокого уровня .NET. Как мы позже увидим, это можно сделать за счет внедрения дополнительного MSIL кода перед компиляцией нашего метода.

    2. Среда CLR

    Прежде чем мы подробно расскажем, как внедрять дополнительный MSIL код, необходимо обсудить некоторые базовые концепции, например, как работает .NET Framework и из каких основных компонентов она состоит.
    Мы будем говорить только о тех концепциях, которые нужны нам для нашей задачи.

    2.1 Базовые концепции

    Двоичный файл .NET обычно называется Assembly (даже если он не содержит двоичный код). Это самоописывающаяся структура, то есть внутри Assembly вы найдете всю необходимую для исполнения информацию.
    Мы можем получить доступ ко всей этой информации, используя рефлексию. Рефлексия позволяет нам увидеть полностью всю картину: какие типы и методы определены внутри Assembly. Мы также можем получить доступ к именами и типам параметров, которые передаются конкретному методу. Единственная информация, которая там отсутствует — это имена локальных переменных, но, как мы увидим, это не будет представлять для нас никакой проблемы.

    2.1.1 Таблицы метаданных

    Вся вышеупомянутая информация хранится в таблицах, которые называются таблицами метаданных.
    В списке ниже отображается индекс и название всех существующих таблиц:

    Код:
    00 - Module            01 - TypeRef          02 - TypeDef
    04 - Field             06 - MethodDef        08 - Param
    09 - InterfaceImpl     10 - MemberRef        11 - Constant
    12 - CustomAttribute   13 - FieldMarshal     14 - DeclSecurity
    15 - ClassLayout       16 - FieldLayout      17 - StandAloneSig
    18 - EventMap          20 - Event            21 - PropertyMap
    23 - Property          24 - MethodSemantics  25 - MethodImpl
    26 - ModuleRef         27 - TypeSpec         28 - ImplMap
    29 - FieldRVA          32 - Assembly         33 - AssemblyProcessor
    34 - AssemblyOS        35 - AssemblyRef      36 - AssemblyRefProcessor
    37 - AssemblyRefOS     38 - File             39 - ExportedType
    40 - ManifestResource  41 - NestedClass      42 - GenericParam
    Каждая таблица состоит из разного количества рядов. Размер ряда зависит от типа таблицы и может содержать ссылку на другие таблицы метаданных.
    Ссылку на таблицы мы получаем от идентификатора записи, это понятие описывается в параграфе ниже.

    2.1.2 Идентификатор записи в таблицах метаданных

    Идентификатор записи в таблицах метаданных (или кратко токен) — это фундаментальная концепция в CLR. Токен позволяет вам делать ссылку на данную таблицу с заданным индексом. Токен имеет 4-байтное значение и состоит из двух частей: табличный индекс и RID (идентификатор записи).
    Табличный индекс — это самый высший байт, который указывает на таблицу. RID — это 3-байтный идентификатор записи.
    В качестве примера рассмотрим следующий токен метаданных:

    (06)00000F

    0х06 – это число таблицы, на которую дана ссылка, в этом случае на MethodDef. Три последние байта – это RID, в нашем случае он имеет значение 0x0F.

    2.1.3 MSIL байт-код

    Когда мы пишем программу на языке .NET, компилятор переведет этот код в промежуточную форму представления MSIL или согласно определению в стандарте ECMA-335 CIL, что означает Common Intermediate Language (промежуточный язык).
    Установив Visual Studio, вы также установите очень полезную утилиту ILDasm. С ее помощью вы сможете деассемблировать, отобразив код MSIL и другую полезную информацию.
    Для примера, давайте попробуем скомпилировать следующий C# исходный код:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    public class TestClass
    {
        private String _message;
    
        public TestClass(String txt)
        {
            this._message = txt;
        }
    
        private String FormatMessage()
        {
            return "Hello " + this._message;
        }
    
        public void SayHello()
        {
            var message = this.FormatMessage();
            Console.WriteLine(message);
        }
    }
    ------#------#------#------<END   CODE>------#------#------#------
    Результат компиляции — Assembly с тремя методами: .ctor : void(string), FormatMessage : string() and SayHello : void().
    Давайте попробуем отобразить код MSIL в алгоритме SayHello:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    .method public hidebysig instance void  SayHello() cil managed
    // SIG: 20 00 01
    {
      // Method begins at RVA 0x21f8
      // Code size       16 (0x10)
      .maxstack  1
      .locals init ([0] string message)
      IL_0000:  /* 00   |                  */ nop
      IL_0001:  /* 02   |                  */ ldarg.0
      IL_0002:  /* 28   | (06)00000F       */
        call       instance string MockLibrary.TestClass::FormatMessage()
      IL_0007:  /* 0A   |                  */ stloc.0
      IL_0008:  /* 06   |                  */ ldloc.0
      IL_0009:  /* 28   | (0A)000014       */
        call       void [mscorlib]System.Console::WriteLine(string)
      IL_000e:  /* 00   |                  */ nop
      IL_000f:  /* 2A   |                  */ ret
    } // end of method TestClass::SayHello
    ------#------#------#------<END   CODE>------#------#------#------
    Для каждой команды мы видим соответствующие значения байта MSIL. Любопытно, что код не содержит ссылки на неуправляемую память, а только на токены метаданных.
    Две команды вызова ссылаются на две разные таблицы, что связано с алгоритмом FormatMessage, внедренным в текущий Assembly и алгоритмом WriteLine, внедренным во внешний Assembly.
    Если мы посмотрим на список таблиц в параграфе 2.1.1, то увидим, что токен метаданных (0A)000014 ссылается на таблицу 0х0А, то есть на таблицу MemberRef, индекс 0х14, то есть WriteLine. Токен (06)0000F ссылается на таблицу 0х06, то есть MethodDef, индекс 0х0F, то есть FormatMessage.

    2.2 Среда выполнения
    Среда выполнения CLR является очень строгой и запрещает любые опасные операции. Если мы попробуем встревать прямо посередине команды, чтобы запутать дизассемблер, создавать самые разные сомнительные операции или менять адреса, то мы придем к неутешительному выводу: в этой среде запрещено абсолютно все.
    Работа CLR основана на стеках. Это означает, что здесь нет такой концепции, как регистры и каждый параметр, для его дальнейшей передачи другим функциям, считывается из стека. Когда мы выходим из алгоритма, то стек должен быть пустым или, по крайней мере, должен содержать возвращаемое значение.
    Как уже было сказано, все основано на токене метаданных. Если мы попробуем сделать вызов при помощи недействительного токена, то получим критическое исключение. Это представляет для нас серьезную проблему, так как мы не сможем вызвать методы, на которые не будет ссылки от оригинального Assembly.

    3. JIT-компилятор
    Как только методы выполнены, у нас есть два разных сценария. Первый, когда код скомпилирован, то код просто установится в скомпилированный неуправляемый код. Второй сценарий: код еще не скомпилирован и когда код окажется в заглушке, будет вызван экспортируемый метод compileMethod, определенный в corjit.h, чтобы скомпилировать и выполнить этот метод.

    3.1 CompileMethod
    Давайте проанализируем этот интересный метод чуть подробней. Сигнатура этого метода следующая:
    Код:
    virtual CorJitResult __stdcall compileMethod (
                ICorJitInfo                 *comp,               /* IN */
                struct CORINFO_METHOD_INFO  *info,               /* IN */
                unsigned /* code:CorJitFlag */   flags,          /* IN */
                BYTE                        **nativeEntry,       /* OUT */
                ULONG                       *nativeSizeOfCode    /* OUT */
                ) = 0;
    Самая интересная структура — это CORINFO_METHOD_INFO, определяемая в corinfo.h и имеющая следующий формат:
    Код:
    struct CORINFO_METHOD_INFO
    {
        CORINFO_METHOD_HANDLE       ftn;
        CORINFO_MODULE_HANDLE       scope;
        BYTE *                      ILCode;
        unsigned                    ILCodeSize;
        unsigned                    maxStack;
        unsigned                    EHcount;
        CorInfoOptions              options;
        CorInfoRegionKind           regionKind;
        CORINFO_SIG_INFO            args;
        CORINFO_SIG_INFO            locals;
    };
    Для нашей цели, самое важное поле — это указатель ILCode на byte. Он указывает на буфер, который содержит MSIL байт-код. Модифицируя этот буфер, мы сможем изменить поток метода выполнения.
    Небольшое замечание: этот метод также очень широко применяется .NET обфускаторами Мы даже можем прочитать следующий комментарий в исходном коде:
    Примечание: обфускаторы, нацеленные на JIT, зависят от соглашения о вызове _stdcall.
    Обфускатор обычно шифрует MSIL байт-код метода, затем, когда метод готовится к выполнению, обфускатор расшифровывает байт-код и затем отправляет это значение, как указатель на byte. Это также объясняет, почему, когда мы открываем его в ILDasm или при помощи декомпилятора, мы получаем ошибку. Как они узнают, когда метод будет вызван? Все довольно просто: код, который отвечает за замену устанавливается в конструкторе типов. Этот конструктор вызывается только один раз: до того, как будет создан новый объект или конкретный тип.

    3.2 Ловушка для compileMethod

    Так как compileMethod экспортируется Clrjit.dll (или из mscorjit.dll в более старых версиях .NET), то мы без проблем сможем установить ловушку, чтобы перехватывать все запросы на компиляцию. Следующий F# псевдо-код покажет, как это можно сделать:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    [<DllImport(
        "Clrjit.dll",
        CallingConvention = CallingConvention.StdCall, PreserveSig = true)
    >]
    extern IntPtr getJit()
    
    [<DllImport("kernel32.dll", SetLastError = true)>]
    extern Boolean VirtualProtect(
        IntPtr lpAddress,
        UInt32 dwSize,
        Protection flNewProtect,
        UInt32& lpflOldProtect)
    
    let pVTable = getJit()
    _pCompileMethod <- Marshal.ReadIntPtr(pVTable)
    
    // make memory writable
    let mutable oldProtection = uint32 0
    if not <| VirtualProtect(
        _pCompileMethod,
        uint32 IntPtr.Size,
        Protection.PAGE_EXECUTE_READWRITE,
        &oldProtection)
    then
        Environment.Exit(-1)
    
    let protection = Enum.Parse(
        typeof<Protection>,
        oldProtection.ToString()) :?> Protection
    
    // save original compile method
    _realCompileMethod <-
        Some (Marshal.GetDelegateForFunctionPointer(
            Marshal.ReadIntPtr(_pCompileMethod),
            typeof<CompileMethodDeclaration>) :?> CompileMethodDeclaration
        )
    RuntimeHelpers.PrepareDelegate(_realCompileMethod.Value)
    RuntimeHelpers.PrepareDelegate(_hookedCompileMethodDelegate)
    
    // install compileMethod hook
    Marshal.WriteIntPtr(
        _pCompileMethod,
        Marshal.GetFunctionPointerForDelegate(_hookedCompileMethodDelegate)
    )
    
    // repristinate memory protection flags
    VirtualProtect(
        _pCompileMethod,
        uint32 IntPtr.Size,
        protection,
        &oldProtection
        ) |> ignore
    ------#------#------#------<END   CODE>------#------#------#------
    Когда мы модифицируем MSIL код, мы должны обратить внимание на размер стека. Нашему фреймворку нужно чуть больше пространства для работы, и если методу, который будет компилироваться, не нужны локальные переменные, то мы получим исключение при запуске. Для того чтобы решить эту проблему, достаточно просто изменить переменную maxStack структуры CORINFO_METHOD_INFO.

    4. Исследование .NET

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

    4.1. Стратегия внедрения MSIL

    Процесс запуска кода будет состоять из следующих шагов:
    1. Установка кода в самом начале кода. Этот код вызовет динамический метод.
    2. Определение динамического метода, у которого будет специфическая сигнатура.
    3. Создание ряда объектов, содержащих параметры, передаваемые методу.
    4. Вызов функции «диспетчер», которая загрузит наш Assembly и, наконец, запустит наш код, передав описатель оригинальному методу и ряду объектов, представляющих параметры метода.

    4.2 Обнаружение описателя метода

    Как мы увидим в следующем параграфе, нам будет необходимо найти описатель метода, который мы будем компилировать, чтобы получить необходимую информацию посредством рефлексии. Я нашел способ, как это можно сделать. Он не очень элегантный, но, главное, он работает.
    Псевдо-код F# ниже покажет, как вы сможете обнаружить описатель метода в заданной структуре CorMethodInfo:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    let getMethodInfoFromModule(
        methodInfo: CorMethodInfo,
        assemblyModule: Module) =
        let mutable info: FilteredMethod option = None
        try
            // dirty trick, is there a
            // better way to know the module of the compiled method?
            let mPtr =
                assemblyModule.ModuleHandle.GetType()
                .GetField("m_ptr",
                    BindingFlags.NonPublic ||| BindingFlags.Instance)
            let mPtrValue = mPtr.GetValue(assemblyModule.ModuleHandle)
            let mpData =
                mPtrValue.GetType()
                .GetField("m_pData",
                    BindingFlags.NonPublic ||| BindingFlags.Instance)
    
            if mpData <> null then
                let mpDataValue = mpData.GetValue(mPtrValue) :?> IntPtr
                if mpDataValue = methodInfo.ModuleHandle then
                    // module found, get method name
                    let tokenNum =
                        Marshal.ReadInt16(nativeint(methodInfo.MethodHandle))
                    let token = (0x06000000 + int32 tokenNum)
                    let methodBase = assemblyModule.ResolveMethod(token)
    
                    if  methodBase.DeclaringType <> null &&
                        isMonitoredMethod(methodBase) then
                        let mutable numOfParameters =
                            methodBase.GetParameters() |> Seq.length
                        if not methodBase.IsStatic then
                            // take into account the this parameter
                            numOfParameters <- numOfParameters + 1
    
                        // compose the result info
                        info <- Some {
                            TokenNum = tokenNum
                            NumOfArgumentsToPushInTheStack = numOfParameters
                            Method = methodBase
                            IsConstructor = methodBase :? ConstructorInfo
                            Filter = this
                        }
        with _ -> ()
        info
    ------#------#------#------<END   CODE>------#------#------#------
    Этот метод необходимо активировать для каждого модуля всех загруженных Assembly.
    Итак, теперь у нас есть объект MethodBase и мы можем его использовать, чтобы получить нужную нам информацию, например, количество принятых параметров и их типы.

    4.3 Внедрение rjlf через команду calli

    Наше первое препятствие заключатся в том, чтобы создать MSIL байт-код, который может вызвать произвольную функцию. Из всех доступных OpCodes, для нас представляет интерес команда calli (будьте осторожны с ее использованием, так как она делает наш код недоступным для проверки).
    На странице MSDN мы читаем:

    Указатель записи метода — это специфический указатель на собственный код (целевой машины), который с полным правом можно вызвать при помощи аргументов, описанных соглашением о вызове (токен метаданных для отдельной сигнатуры). Такой указатель можно создать, используя команды Ldftn или Ldvirtftn или взять из собственного кода
    А теперь мы может определить произвольный указатель для собственного кода. Единственная проблема заключается в том, что мы не сможем использовать Ldftn или Ldvirtftn, так как этим командам нужен токен метаданных, а мы не можем определить это значение. Но не все так плохо. Из документации по Ldftn мы читаем:

    Сохраняет неуправляемый указатель на собственный код, внедряя в стек вычислений специфический метод
    Таким образом, если у нас есть неуправляемый указатель, то мы сможем симулировать Ldftn при помощи простой команды Ldc_I4 (предположим, что мы работаем в 32-битной среде).
    К нашему сожалению, теперь у нас есть еще более серьезная проблема. Команде calli нужен callSiteDescr. Из мы можем прочитать следующее:

    <токен> - который ссылается на callSiteDescr — должен быть действительным StandAloneSig токеном
    StandAloneSig имеет табличный номер 17. Как я уже писал, мы не можем указать этот токен метаданных (так как, скорее всего, его даже нет в таблице).
    Я немного поэкспериментировал с инструкцией calli, чтобы посмотреть, принимает ли она другие виды токенов метаданных. Я обнаружил, что она также принимает токен из одной из следующих таблиц: TypeSpec, Field и MethodDef.
    Для нас наибольший интерес представляет таблица MethodDef, так как мы сможем подделать действительный токен MethodDef, создав DynamicMethod (подробнее об этом чуть позже). А теперь мы можем закрыть круг, использовав команду calli и изменив токен метаданных, чтобы задать MethodDef.
    Мы будем использовать объект MethodBase, который мы получили, выполнив предыдущий шаг, чтобы знать, сколько параметров принимает метод и направить их в стек до вызова команды calli.
    Следующий псевдо-код F# покажет, как задать команду calli:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    // load all arguments on the stack
    for i=0 to filteredMethod.NumOfArgumentsToPushInTheStack-1 do
        ilGenerator.Emit(OpCodes.Ldarg, i)
    
    // emit calli instruction with a pointer to the dynamic method,
    // the token used by the calli is not important as I'll modify it soon
    ilGenerator.Emit(OpCodes.Ldc_I4, functionAddress)
    ilGenerator.EmitCalli(
        OpCodes.Calli,
        CallingConvention.StdCall,
        dispatcherMethod.ReturnType,
        dispatcherArgs)
    
    // this index allow to modify the right byte
    let patchOffset = ilGenerator.ILOffset - 4
    ilGenerator.Emit(OpCodes.Nop)
    
    // check if I have to pop the return value
    match filteredMethod.Method with
    | :? MethodInfo as mi ->
        if mi.ReturnType <> typeof<System.Void> then
            ilGenerator.Emit(OpCodes.Pop)
    | _ -> ()
    
    // end method
    ilGenerator.Emit(OpCodes.Ret)
    ------#------#------#------<END   CODE>------#------#------#------
    Переменная functionAddress содержит собственный указатель нашего динамического метода. Последний шагом будет исправление токена метаданных calli на токен MethodDef, и мы знаем, что его значение верное.
    Следующий псевдо-код F# покажет, как модифицировать MSIL байт-код :


    Код:
    ------#------#------#------<START CODE>------#------#------#------
    // craft MethodDef metadata token index
    let b1 = (filteredMethod.TokenNum &&& int16 0xFF00) >>> 8
    let b2 = filteredMethod.TokenNum &&& int16 0xFF
    
    // calli instruction accept 0x11 as table index (StandAloneSig),
    // but seems that also other tables are allowed.
    // In particular the following ones seem to be accepted as
    // valid: TypeSpec, Field and Method (most important)
    trampolineMsil.[patchOffset] <- byte b2
    trampolineMsil.[patchOffset+1] <- byte b1
    trampolineMsil.[patchOffset + 3] <- 6uy // 6(0x6): MethodDef Table
    ------#------#------#------<END   CODE>------#------#------#------
    Так как этот шаг довольно сложный, давайте попробуем кратко описать наши действия:
    1. Мы используем команду calli, чтобы вызвать произвольный метод, задав собственный указатель адреса.
    2. Мы изменяем токен метаданных calli, задав токен MethodDef вместо токена StandAloneSig.
    3. Мы передаем значение токена метаданных токену метода, который мы компилируем на данный момент. Этот тип токена описывает метод, который необходимо вызвать.
    Следующий шаг: нам необходимо убедиться, что метод, вызванный командой calli удовлетворяет информации, содержащейся в токене метаданных, на который была дана ссылка.

    4.4 Создаем динамический метод

    Нам нужно создать динамический метод, который удовлетворяет информации, предоставленной токеном, который был передан команде calli. Из пункта мы можем прочитать:
    Дескриптор метода — это токен метаданных, который указывает на метод, а также число, тип и порядок аргументов, которые поместили в стек, чтобы передать их методу и использовать соглашение о вызове
    Итак, чтобы создать метод, который будет удовлетворять сигнатуре метода, на который ссылается токен, мы будем использовать очень мощную возможность .NET, которая позволит нам определить динамический метод. В этом шаге мы:
    1. Создадим метод с такой же сигнатурой, как и метод, который мы будем компилировать. Это даст гарантию, что информация, передаваемая токеном метаданных настоящая.
    2. Сможем задать действительный токен метаданных, так как в текущей среде выполнения создан новый динамический тип.
    Этот динамический метод вызовет другой метод (диспетчер), который принимает два аргумента: строка, которая представляет расположение Assembly для загрузки (подробнее об этом позже) и ряд объектов, которые содержат аргументы, передаваемые методу.
    Создавая этот метод, нужно быть внимательными при создании объектов, так как в .NET не все является объектом.
    Следующий псевдо-код F# создаст динамический метод с правильной сигнатурой:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    let argumentTypes = [|
        if not filteredMethod.Method.IsStatic then
            yield typeof<Object>
        yield!
            filteredMethod.Method.GetParameters()
            |> Array.map(fun p -> p.ParameterType)
    |]
    
    let dynamicType =
        _dynamicModule.DefineType(
            filteredMethod.Method.Name + "_Type" + string(!_index))
    let dynamicMethod =
        dynamicType.DefineMethod(
            dynamicMethodName,
            MethodAttributes.Static |||
            MethodAttributes.HideBySig |||
            MethodAttributes.Public,
            CallingConventions.Standard,
            typeof<System.Void>,
            argumentTypes
        )
    ------#------#------#------<END   CODE>------#------#------#------
    Теперь мы можем продолжить и создать тело метода. Нам нужно обратить внимание на два факта: параметры valueType должны быть упакованы, а параметры Enum должны быть переконвертированы в другую форму (путем проб и ошибок я выяснил, что Int32 — компромиссный вариант).

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    // push the location of the Assembly to load containing the monitors
    let assemblyLocation =
        if filteredMethod.Filter.Invoker <> null
        then filteredMethod.Filter.Invoker.Assembly.Location
        else String.Empty
    ilGenerator.Emit(OpCodes.Ldstr, assemblyLocation)
    
    // get the parameter types
    let parameters =
        filteredMethod.Method.GetParameters()
        |> Seq.map(fun pi -> pi.ParameterType)
        |> Seq.toList
    
    // create argv array
    ilGenerator.Emit(OpCodes.Ldc_I4,
        filteredMethod.NumOfArgumentsToPushInTheStack)
    ilGenerator.Emit(OpCodes.Newarr, typeof<Object>)
    
    // fill the argv array
    for i=0 to filteredMethod.NumOfArgumentsToPushInTheStack-1 do
        ilGenerator.Emit(OpCodes.Dup)
        ilGenerator.Emit(OpCodes.Ldc_I4, i)
        ilGenerator.Emit(OpCodes.Ldarg, i)
    
        // check if I have to box the value
        if filteredMethod.Method.IsStatic || i > 0 then
            // this check is necessary becasue the
            // GetParameters method doesn't consider the 'this' pointer
            let paramIndex = if filteredMethod.Method.IsStatic then i else i - 1
            if parameters.[paramIndex].IsEnum then
                // consider all enum as Int32 type to avoid access problems
                ilGenerator.Emit(OpCodes.Box, typeof<Int32>)
    
            elif parameters.[paramIndex].IsValueType then
                // all value types must be boxed
                ilGenerator.Emit(OpCodes.Box, parameters.[paramIndex])
    
        // store the element in the array
        ilGenerator.Emit(OpCodes.Stelem_Ref)
    
    // emit call to dispatchCallback
    let dispatchCallbackMethod =
        Type.GetType("ES.Anathema.Runtime.Dispatcher")
        .GetMethod("dispatchCallback", BindingFlags.Static ||| BindingFlags.Public)
    ilGenerator.EmitCall(OpCodes.Call, dispatchCallbackMethod, null)
    
    ilGenerator.Emit(OpCodes.Ret)
    ------#------#------#------<END   CODE>------#------#------#------
    4.5 Активация кода, определенного пользователем

    Для того чтобы код можно было без проблем расширить, мы можем внедрить механизм, который загрузит Assembly, определенный пользователем и активирует конкретный метод. Таким образом, у нас будет архитектура, похожая на архитектуру, основанную на подключаемых модулях. Назовем эти модули monitors. Каждый monitor можно сконфигурировать, чтобы перехватить конкретный метод.
    Для того чтобы расположить monitors, мы используем парадигму проекта программного обеспечения «программирование по соглашениям». Это подразумевает, что будут загружены все классы, чьи названия заканчиваются на monitor.
    Последний метод очень простой, он забирает объект MethodBase из стека, чтобы передать его monitor и, наконец, активировать его. Параметр assemblyLocation уточняет, где находится Assembly, определенный пользователем.

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    let dispatchCallback(assemblyLocation: String, argv: Object array) =
        if File.Exists(assemblyLocation) then
            let callingMethod =
                try
                    // retrieve the calling method from the stack trace
                    let stackTrace = new StackTrace()
                    let frames = stackTrace.GetFrames()
                    frames.[2].GetMethod()
                with _ -> null
    
            // invoke all the monitors, we use "convention over configuration"
            let bytes = File.ReadAllBytes(assemblyLocation)
            for t in Assembly.Load(bytes).GetTypes() do
                try
                    if t.Name.EndsWith("Monitor") && not t.IsAbstract then
                        let monitorConstructor =
                            t.GetConstructor([|
                                typeof<MethodBase>;
                                typeof<Object array>|])
                        if monitorConstructor <> null then
                            monitorConstructor.Invoke([|callingMethod; argv|]) |> ignore
                with _ -> ()
    ------#------#------#------<END   CODE>------#------#------#------
    4.6 Настройка таблицы SEH

    Мы уже приближаемся к завершению. Мы модифицировали MSIL байт-код, создали динамический метод и код. Последний шаг — заново переписать структуру CORINFO_METHOD_INFO и вызвать настоящий compileMethod. К сожалению, после этого вы вскоре получите ошибку выполнения, когда вы попытаетесь исследовать метод, который использует конструкцию try/catch.
    Это связано с тем, что создание кода сделало таблицу SEH недействительной. Эта таблица содержит информацию о части кода в конструкциях try/catch. Мы понимаем, что внедряя дополнительный MSIL код, свойства TryOffset и HandlerOffset примут недействительное значение.
    Эта таблица расположена после IL кода, как видно на схеме ниже:

    Fat Header
    IL Code
    SEH Table

    У нас также есть подтверждение из исходного кода. На самом деле, в corhlpr.cpp мы видим, что таблица SEH добавлена к переменной OutBuff, после того как она была заполнена IL кодом.
    Таким образом, чтобы получить адрес таблицы SEH, достаточно просто добавить к указателю ILCode, расположенному в структуре CorMethodInfo, длину MSIL кода.
    Прежде чем показать код, нужно запомнить, что таблица SEH может быть двух разных типов: FAT или SMALL. Меняется только измерение ее полей. Поэтому настройка таблицы — это всего лишь определение ее расположения и перечисление каждой конструкции для определения их значений.
    Псевдо-код #F делает именно это:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    let fixEHClausesIfNecessary(
        methodInfo: CorMethodInfo,
        methodBase: MethodBase,
        additionalCodeLength: Int32) =
        let clauses = methodBase.GetMethodBody().ExceptionHandlingClauses
        if clauses.Count > 0 then
            // locate SEH table
            let codeSizeAligned =
                if (int32 methodInfo.IlCodeSize) % 4 = 0 then 0
                else 4 - (int32 methodInfo.IlCodeSize) % 4
            let mutable startEHClauses =
                methodInfo.IlCode +
                new IntPtr(int32 methodInfo.IlCodeSize + codeSizeAligned)
    
            let kind = Marshal.ReadByte(startEHClauses)
            // try to identify FAT header
            let isFat = (int32 kind &&& 0x40) <> 0
    
            // it is always plus 3 because even if it is small it is
            // padded with two bytes. See: Expert .NET 2.0 IL Assembler p. 296
            startEHClauses <- startEHClauses + new IntPtr(4)
    
            for i=0 to clauses.Count-1 do
                if isFat then
                    let ehFatClausePointer =
                        box(startEHClauses.ToPointer())
                            :?> nativeptr<CorILMethodSectEhFat>
                    let mutable ehFatClause = NativePtr.read(ehFatClausePointer)
    
                    // modify the offset value
                    ehFatClause.HandlerOffset <-
                        ehFatClause.HandlerOffset + uint32 additionalCodeLength
                    ehFatClause.TryOffset <-
                        ehFatClause.TryOffset + uint32 additionalCodeLength
    
                    // write back the result
                    let mutable oldProtection = uint32 0
                    let memSize = Marshal.SizeOf(typeof<CorILMethodSectEhFat>)
                    if not <| VirtualProtect(
                        startEHClauses,
                        uint32 memSize,
                        Protection.PAGE_READWRITE,
                        &oldProtection) then
                        Environment.Exit(-1)
    
                    let protection = Enum.Parse(
                        typeof<Protection>,
                        oldProtection.ToString()) :?> Protection
                    NativePtr.write ehFatClausePointer ehFatClause
    
                    // repristinate memory protection flags
                    VirtualProtect(
                        startEHClauses,
                        uint32 memSize,
                        protection,
                        &oldProtection) |> ignore
    
                    // go to next clause
                    startEHClauses <- startEHClauses + new IntPtr(memSize)
                else
                    //... do same as above but for small size table
    ------#------#------#------<END   CODE>------#------#------#------
    Как только мы настроили эту таблицу, мы, наконец, можем активировать настоящий compileMethod.

    5. Практические примеры

    Представленный код – это часть проекта под названием Anathema. Он позволит вам без труда исследовать программы .NET. Давайте попробуем использовать фреймворк, исследуя веб-приложение, чтобы узнать пароли пользователя, исследовать настоящие вредоносные программы и зарегистрировать все вызовы метода.

    5.1 Кража паролей веб-приложения

    Давайте посмотрим, как мы можем использовать этот метод исследования для внедрения программы для кражи паролей веб-приложения. Для демонстрации, мы используем очень популярный .NET веб-сервер Suave. Мы напишем веб-приложение в F#, а программу для кражи паролей, как C# консольное приложение. Таким образом, мы сможем внедрить интересующий нас метод, до того как он будет скомпилирован. В другом случае, нам придется сделать вынужденное выполнение .NET, чтобы заново рекомпилировать метод и применить внедрение.
    Веб-приложение очень простое и содержит только одну форму. Его HTML-код показан ниже:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    <h1>-= Secure Web Shop Login =-</h1>
    <form method="POST" action="/login">
        <table>
            <tr>
                <td>Username:</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>Password:</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit" name="Login"></td>
            </tr>
        </table>
    </form>
    ------#------#------#------<END   CODE>------#------#------#------
    F# код, отвечающий за аутентификацию:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    let private _accounts = [
        ("admin", BCrypt.HashPassword("admin"))
        ("guest", BCrypt.HashPassword("guest"))
    ]
    
    let private authenticate(username: String, password: String) =
        _accounts
        |> List.exists(fun (user, hash) ->
            let usernameMatch = user.Equals(username, StringComparison.Ordinal)
            let passwordMatch = BCrypt.Verify(password, hash)
            usernameMatch && passwordMatch
        )
    
    let private doLogin(ctx: HttpContext) =
        match (tryGetParameter(ctx, "username"), tryGetParameter(ctx, "password")) with
        | (Some username, Some password) when authenticate(username, password) ->
            OK "Authentication successfully executed!" ctx
        | _ -> OK "Wrong username/password combination" ctx
    ------#------#------#------<END   CODE>------#------#------#------
    Самый лучший способ перехватить пароли – это метод аутентификации. Мы начнем с создания класса, который отвечает за печать полученного пароля. Это можно сделать путем создания следующего простого класса:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    class PasswordStealerMonitor
    {
        public PasswordStealerMonitor(MethodBase m, object[] args)
        {
            Console.WriteLine(
                    "[!] Username: '{0}', Password: '{1}'",
                    args[0],
                    args[1]);
        }
    }
    ------#------#------#------<END   CODE>------#------#------#------
    Последний шаг – внедрение в приложение при помощи следующего кода:

    Код:
    ------#------#------#------<START CODE>------#------#------#------
    // create runtime
    var runtime = new RuntimeDispatcher();
    var hook = new Hook(runtime.CompileMethod);
    var authenticateMethod = GetAuthenticateMethod();
    runtime.AddFilter(
        typeof(PasswordStealerMonitor),
        "SecureWebShop.Program.authenticate");
    
    // apply hook
    var jitHook = new JitHook();
    jitHook.InstallHook(hook);
    jitHook.Start();
    
    // start the real web application
    SecureWebShop.Program.main(new String[] { });
    ------#------#------#------<END   CODE>------#------#------#------
    Как только веб-приложение будет запущено и мы попробуем войти под своим логином, в консоли мы увидим следующее:

    Код:
    -= Secure Web Shop =-
    Start web server on 127.0.0.1:8080
    [14:45:49 INF] Smooth! Suave listener started in 631.728 with binding 127.0.0.1:8080
    [!] Username: 's4tan', Password: 'wrong_password'
    [!] Username: 'admin', Password: 'admin'
    5.2 Исследование вредоносной программы

    Рассмотрим в качестве примера образец вредоносной программы Hawkeye, написанную на .NET, MD5-хеш: 130efba199b389ab71a374bf95be2304.
    В образце содержатся два уровня упаковки. Мы могли бы отследить пакеры, но давайте лучше сконцентрируемся на основной полезной нагрузке (MD5: 97d74c20f5d148ed68e45dad0122d3b5). Когда будет запущена основная полезная нагрузка, будут зарегистрированы следующие вызовы метода:

    Код:
    c:\>MLogger.exe malware.exe
    [+] Debugger.My.MyApplication.Main(Args: System.String[]) : System.Void
    [+] Debugger.My.MyProject..cctor()
    [...]
    [+] Debugger.My.MyProject.get_Application() : Debugger.My.MyApplication
    [+] Debugger.My.MyProject+ThreadSafeObjectProvider`1.get_GetInstance() : T
    [+] Debugger.My.MyApplication..ctor()
    [+] Debugger.My.MyProject+ThreadSafeObjectProvider`1.get_GetInstance() : T
    [+] Debugger.My.MyProject+MyForms..ctor()
    [+] Debugger.Debugger..ctor()
    [+] Debugger.Clipboard..ctor()
    [+] Debugger.Clipboard.add_Changed(obj: Debugger.Clipboard+ChangedEventHandler)
        : System.Void
    [+] Debugger.My.Resources.Resources.get_CMemoryExecute() : System.Byte[]
    [+] Debugger.My.Resources.Resources.get_ResourceManager() :
        System.Resources.ResourceManager
    [+] Debugger.Debugger.InitializeComponent() : System.Void
    [+] Debugger.Debugger.Decrypt(
        encryptedBytes: System.String, secretKey: System.String) : System.String
    [+] Debugger.Debugger.getAlgorithm(secretKey: System.String) :
        System.Security.Cryptography.RijndaelManaged
    [+] Debugger.Debugger.Decrypt(
        encryptedBytes: System.String, secretKey: System.String) : System.String
    [+] Debugger.Debugger.getAlgorithm(secretKey: System.String) :
        System.Security.Cryptography.RijndaelManaged
    [+] Debugger.Debugger.Decrypt(
        encryptedBytes: System.String, secretKey: System.String) : System.String
    [+] Debugger.Debugger.getAlgorithm(secretKey: System.String) :
        System.Security.Cryptography.RijndaelManaged
    [...]
    [+] Debugger.Debugger.IsConnectedToInternet() : System.Boolean
    [+] Debugger.Debugger.GetInternalIP() : System.String
    [+] Debugger.Debugger.GetExternalIP() : System.String
    [+] Debugger.Debugger.GetBetween(
        Source: System.String, Before: System.String, After: System.String) : System.String
    [+] Debugger.Debugger.GetAntiVirus() : System.String
    [+] Debugger.Debugger.GetFirewall() : System.String
    [+] Debugger.Debugger.unHide() : System.Void
    [+] Debugger.My.MyProject+ThreadSafeObjectProvider`1.get_GetInstance() : T
    [+] Debugger.My.MyComputer..ctor()
    [+] Debugger.Debugger.unhidden(path: System.String) : System.Void
    [...]
    [+] Debugger.My.Resources.Resources.get_mailpv() : System.Byte[]
    [+] Debugger.My.Resources.Resources.get_ResourceManager() :
        System.Resources.ResourceManager
    [+] Debugger.Debugger.HookKeyboard() : System.Void
    [+] Debugger.Clipboard.Install() : System.Void
    [+] Debugger.My.MyProject+ThreadSafeObjectProvider`1.get_GetInstance() : T
    [+] Debugger.My.MyComputer..ctor()
    [+] Debugger.Debugger.IsConnectedToInternet() : System.Boolean
    [+] Debugger.Debugger.IsConnectedToInternet() : System.Boolean
    [+] Debugger.My.MyProject.get_Computer() : Debugger.My.MyComputer
    [...]
    6. Вывод

    Исследование .NET через MSIL внедрение байт-кода — это довольно полезный метод, который дает нам возможность полностью контролировать активацию метода, используя язык высокого уровня .NET.

    Как мы увидели, эта работа требует большой внимательности и знаний того, как работает CLR. Но конечный результат стоит затраченных усилий.

    ©Источник: Phrack underground zine
    ©Автор: Antonio "s4tan" Parata <aparata@gmail.com>

  2. #2
    Привет. А у тебя собственных статей нету?

  3. #3
    Цитата Сообщение от Pidaraz Посмотреть сообщение
    Привет. А у тебя собственных статей нету?
    Последнюю статью я написал в 2011. Сейчас много времени занимает работа, поэтому нет времени заниматься. А вообще почему возник такой вопрос?

  4. #4
    ПривеТ! Я смотрю, ресурс новый. А статья ясно скопирована откуда-то.
    Я после этого смотрел другой раздел форума. ты с реверс4ю. Было бы круто если бы контент был уникальным.

  5. #5
    Все материалы, публикуемые на форуме - уникальны. В Рунете ты их не найдешь, кроме данного ресурса. Это лично мои статьи или переводы англоязычных блогов/форумов по безопасности (попробуй найти где-нибудь книгу Black Hat Python на русском).
    Ресурс впервые был открыт в 2009 году поэтому не совсем правильно говорить что он новый. Малое количество материала лишь потому что занимаюсь я им один и не пытаюсь продвигать его.

  6. #6
    Я не совсем то имел в виду, прости,я наверное слишком резок был. Могу помочь с чем-нибудь.

  7. #7
    Цитата Сообщение от Pidaraz Посмотреть сообщение
    Я не совсем то имел в виду, прости,я наверное слишком резок был. Могу помочь с чем-нибудь.
    Буду рад любым интересным и полезным материалам на форуме. WELCOME!

Ваши права

  • Вы не можете создавать новые темы
  • Вы не можете отвечать в темах
  • Вы не можете прикреплять вложения
  • Вы не можете редактировать свои сообщения
  •  

Вход

Вход