Пропихиваем параметры в небезопасные операции в безопасном коде

    Всем привет. В этот раз продолжаем смеяться над нормальным вызовом методов. Предлагаю ознакомится с вызовом метода с параметрами без передачи параметров. Также попробуем преобразовать ссылочный тип в число — его адрес, без использования указателей и unsafe кода.

    Дисклеймер


    Прежде, чем приступить к повествованию, настоятельно рекомендую ознакомиться с предыдущим постом про StructLayout, т.к. вещи, оговоренные там, здесь повторяться не будут.

    Также хотелось бы предупредить, что данная статья не содержит материал, который стоит применять в реальных проектах.

    Немного начальных сведений


    Прежде, чем приступить к практике, давайте вспомним, как преобразовывается код C#.
    Разберем простой пример. Напомню, что для того, чтобы развлекаться со StructLayout, я использую только виртуальные методы.

    public class Helper 
    {
            public virtual void Foo(int param)
            {
            }
    }
    
    public class Program 
    {
        public void Main() 
        {
            Helper helper = new Helper();
            var param = 5;
            helper.Foo(param);
        }
    }
    

    Данный код не содержит ничего сложного, однако инструкции, генерируемые JiTом, содержат несколько ключевых моментов. Предлагаю разобрать лишь небольшой фрагмент сгенерированного кода.

        1: mov dword [ebp-0x8], 0x5
        2: mov ecx, [ebp-0xc]
        3: mov edx, [ebp-0x8]
        4: mov eax, [ecx]
        5: mov eax, [eax+0x28]
        6: call dword [eax+0x10]
    

    В этом небольшом примере можно наблюдать fastcall — соглашение по передаче параметров через регистры (первых двух параметров слева направо в регистрах ecx и edx), а остальные параметры передаются справа налево в стеке. Первый (неявный) параметр — адрес экземпляра класса, на котором метод вызван. Он передается первым неявным параметром для каждого экземплярного метода. Второй параметр — локальная переменная типа int (в нашем случае).

    Итак, в первой строке мы видим локальную переменную 5, здесь ничего интересного нет.
    Во второй строке мы копируем адрес экземпляра Helper в регистр ecx. Это адрес непосредственно таблицы методов.
    Третяя строка содержит копирование локальной переменной 5 в регистр edx
    Четвертая строка копирует адрес таблицы методов в регистр eax
    Пятая строка содержит сдвиг регистра eax на 40 байт загрузку значения из памяти по адресу на 40 байт большему, чем адрес таблицы методов: на адрес начала методов в таблице методов. (Таблица методов содержит разную информацию, которая хранится до этого. к такой информации, например, относится адрес таблицы методов базового класса, адрес EEClass, различные флаги, в том числе и флаг сборщика мусора, и так далее). Соответсвенно теперь в регистре eax хранится адрес первого метода из таблицы методов.
    В шестой строке вызывается метод по смещению 16 от начала, то бишь пятый в таблице методов. Почему наш единсвенный метод пятый? Напоминаю, что у object существуют 4 виртуальных метода(ToString, Equals, GetHashCode и Finalize), которые, соотвественно, будут у всех классов.

    Переходим к практике


    Пришло время приступить к небольшой демонстрации. Предлагаю вот такую заготовку (весьма похожую на заготовку из предыдущей статьи).

    [StructLayout(LayoutKind.Explicit)]
        public class CustomStructWithLayout
        {
            [FieldOffset(0)]
            public Test1 Test1;
            [FieldOffset(0)]
            public Test2 Test2;
        }
    
        public class Test1
        {
            public virtual int Useless(int param)
            {
                Console.WriteLine(param);
                return param;
            }
        }
    
        public class Test2
        {
            public virtual int Useless()
            {
                return 888;
            }
        }
    
        public class Stub
        {
            public void Foo(int stub) { }
        }
    

    И следующее наполение метода Main:

        class Program
        {
            static void Main(string[] args)
            {
                Test2 fake = new CustomStructWithLayout
                {
                    Test2 = new Test2(),
                    Test1 = new Test1()
                }.Test2;
                Stub bar = new Stub();
                int param = 55555;
                bar.Foo(param);
                fake.Useless();
                Console.Read();
            }
        }
    

    Как вы догадываетесь, по опыту предыдущей статьи, будет вызван метод Useless(int j) типа Test1.

    Но что будет выведено? Внимательный читатель, полагаю, уже ответил на этот вопрос. На консоль выведено «55555».

    Но давайте все же глянем на фрагменты сгенерированного кода

         mov ecx, [ebp-0x20]
         mov edx, [ebp-0x10]
         cmp [ecx], ecx
         call Stub.Foo(Int32)
         nop
         mov ecx, [ebp-0x1c]
         mov eax, [ecx]
         mov eax, [eax+0x28]
         call dword [eax+0x10]
    

    Думаю, вы узнали шаблон вызова виртуального метода, он начинается после L00cc: nop. Как мы видим, в ecx ожидаемо записывается адрес экзепляра, на котором вызвается метод. Но т.к. мы вызываем якобы метод типа Test2, который не имеет параметров, то в edx ничего не записывается. Однако до этого был вызван метод, которому передавался параметр как раз через регистр edx, соответсвенно, значение в нем и осталось. и его мы можем наблюдать в окне вывода.

    Есть еще один интересный нюанс. Я специально использовал значимый тип. Предлагаю попробовать заменить тип параметра метода Foo типа Stub на любой ссылочный тип, например, строку. Но тип параметра метода Useless не изменять. Ниже можете увидеть результат на моей машине с некоторыми проясняющими элементами: WinDBG и Калькулятором :)


    Картинка кликабельна

    В окне вывода выводится адрес ссылочного типа в десятичной системе счисления

    Итог


    Освежили в памяти знания о вызове методов при помощи соглашения fastcall и тут же воспользовались чудным регистром edx для передачи параметра в 2 метода за раз. Также наплевали на все типы и вспомнив, что все есть лишь байты без труда получили адрес объекта без использования указателей и unsafe кода. Далее планирую использовать полученный адрес для еще более неприменимых целей!

    Спасибо за внимание!

    P.S. Код на C# можно найти по ссылке
    • +16
    • 5,2k
    • 8
    Поделиться публикацией

    Комментарии 8

      0
      Чтобы надежно использовать эту уязвимость, нужно между вызовами запрещать прерывания — а это уже выход из песочницы :) Однако, нужно ждать патча рантайма, который будет обнулять регистровые пары перед возвратом из fastcall-метода.
        0
        А что, собственно, Вы хотели увидеть? Сделайте объекты разной внутренней структуры, с разным количеством полей и разным количеством виртуальных методов — такой фейерверк начнётся, что огого! А если это скрестить с финализатором — так вообще // удачной отладки!
          +1
          В статье был реализован аналог union С++, который дважды был инициализирован через разные поля. Поскольку типы нетривиальные, то они неизбежно имеют указатель на таблицу методов — vtable. Для стандарте C++ такая ситуация регламентирована чётко: undefined behavior.
          Переписывание с адаптацией под синтаксис C# привёл ровно к такому же результату.
            0
            А с какого перепугу у нас загрузился тип со взаимным перекрытием двух полей ссылочных типов?
              0
              Благодаря аццкому костылю в виде атрибута StructLayout
                +1

                Не такой уж он и "аццкий", просто использовать его нужно с умом. Для интеропа и экономии памяти в узких местах он играет важную роль.

              0
              5: mov eax, [eax+0x28]

              Пятая строка содержит сдвиг регистра eax на 40 байт

              Сдвиг регистра на IA32/AMD64 осуществляется инструкциями shl, shr и им подобными, аналог в языках с си-подобным синтаксисом — <<, >>.
              В процитированной пятой строке — загрузка значения из памяти по адресу eax + 0x28

                0
                Имелось в виду смещение (offset), а не сдвиг (shift). Не стоит придираться к словам настолько.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое