Внедрение кода в модули платформы Microsoft.NET

Внедрение кода в модули платформы Microsoft.NET

В общих чертах, .NET - это "наш ответ Java" от Микрософт. Судя по достаточно агрессивной политике продвижения данной платформы и дальнейшим планам компании-производителя, ожидать снижения интереса к платформе со стороны сторонних разработчиков не приходится, а значит число .NET приложений будет продолжать увеличиваться. Изложенная в статье методика внедрения кода может быть использована по разному - как говорится, в меру испорченности конкретного человека. С одинаковым успехом её можно применить, к примеру, как и для реализации механизма "навесной" защиты, так и для создания различных вредоносных программ.

Призы для победителей конкурса предоставил компьютерный интернет магазин

Автор: kr0m kr0m ят hotbox.ru

Введение

В январе 2002 года Microsoft анонсировала первую версию Microsoft.NET Framework. За прошедшие годы данная платформа приобрела достаточно высокую популярность среди разработчиков web и desktop приложений. Существует и развивается проект портирования этой платформы на Linux - Mono project
В общих чертах, .NET - это "наш ответ Java" от Микрософт. Судя по достаточно агрессивной политике продвижения данной платформы и дальнейшим планам компании-производителя, ожидать снижения интереса к платформе со стороны сторонних разработчиков не приходится, а значит число .NET приложений будет продолжать увеличиваться.
Изложенная в статье методика внедрения кода может быть использована по разному - как говорится, в меру испорченности конкретного человека. С одинаковым успехом её можно применить, к примеру, как и для реализации механизма "навесной" защиты, так и для создания различных вредоносных программ.

О статье

В данной статье рассматривается методика внедрения собственного кода в существующие программы для платформы Microsoft.NET. При постановке задачи внедрения были выдвинуты следующие условия:

1. Перехват управления должен осуществляться при запуске модифицированной сборки. Управление должно передаваться на внедренный код, с последующей передачей на старую точку входа измененной сборки.

2. Существующий код должен полностью сохранить свою функциональность, и передача управления на внедренный код не должна быть как-либо заметна для пользователя программы.

3. Реализация механизма внедрения кода должна быть максимально простой и надежной.

При исследованиях формата файла сборок .NET использовалась программа Researcher Павла Румянцева (e-mail: pavel_r---на---bk.ru). Громадное ему спасибо за проделанную работу по обработке стандарта ECMA-335. Без его труда данное исследование заняло бы гораздо большее время.
Программа, созданная в ходе данного исследования, разрабатывалась в среде Borland C++ Builder, а для разработки исследуемых примеров использовалась Microsoft Visual Studio 2003.
Исходные коды программы и бинарные модули можно загрузить здесь

Формат файла сборки .NET

Хотелось бы сразу оговорится, что при написании статьи не ставилась цель подробно описать формат сборок .NET. Автор постарался изложить необходимый минимум информации для понимания читателем методики внедрения. Если читателя интересует структура файла, то он может обратится к статье "Физическая организация метаданных в исполняемых файлах .NET", и конечно, к описанию стандарта ECMA-335.

Исполняемый файл .NET условно можно разделить на две части: PE-часть *, которая присутствует в любой Windows-программе, и IL-часть, которая содержится только в .NET файлах.
PE-часть файла содержит информацию, необходимую операционной системе при начальном запуске программы: базовый адрес программы, её отображаемый размер, количество секций и их относительные адреса, используемые DLL, используемые ресурсы и т.д.
IL-часть содержит информацию, называемую метаданными - т.е. "данными о данных", "данными, которые описываю другие данные". В этой части исполняемого файла содержится описание всех классов, описание вызываемых методов, свойств, параметров функций; так же в метаданных хранятся ссылки на внешние .NET сборки и множество другой информации. Метаданные используются .NET Framework для запуска, компиляции и работы программы.

Рассмотрим формат метаданных простейшей программы. Для анализа метаданных используется программа Researcher.

рис. 1. Формат метаданных

Как видно из рис. 1, метаданные состоят из следующих частей:

1. Сигнатура метаданных (содержит номер версии метаданных и пр. информацию)
2. Набор структур, описывающих потоки (их количество, расположение, размеры)
3. Набор потоков

Пока читатель окончательно не запутался в новой терминологии, необходимо прояснить, что такое потоки, и какая информация в них содержится. Каждый поток предназначен для хранения информации определенного вида.
Например, в потоке #~ хранятся таблицы с описанием используемых классов, их методов, передаваемых методам параметров, и др.
Поток #Strings содержит различные строки, например, имена классов, функций, пространств имен, библиотек
В потоке #US хранятся пользовательские строки, т.е. строки, явно объявленные в программе.
Поток #Blob содержит различные бинарные данные, а поток #GUID - глобальные идентификаторы.

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

рис. 2. Формат потока #~

На рисунке:

1. Структура, описывающая какие таблицы присутствуют в данном потоке
2. Массив DWORD, содержащий количество строк в каждой таблице
3. Таблицы описания модуля, используемых типов, методов, членов класса, сборок и др.

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

На текущий момент теории достаточно, давайте рассмотрим, как осуществляется вызов какой-нибудь функции из программы и как этот вызов связан с приведенными метаданными. Для примера возьмем небольшую программу, написанную на C# (см. каталог с проектом FunctionCaller):

using System;
using System.Windows.Forms;

namespace a {
    public class Test {
	[STAThread]
	private static void Main() {
	    ShowMessageA();
	}

	private static void ShowMessageA() {
	    MessageBox.Show("A");
	}

    }
}
Листинг 1. Текст исследуемой программы


Скомпилировав этот пример и получив исполняемый файл, загрузим его в дизассемблер. Для исследования файла будем использовать дизассемблер ILDasm.exe, входящий .NET Framework SDK.

рис. 3. Программа в дизассемблере

Открыв код метода Main, получим следующую картину:

.method /*06000001*/ private hidebysig static 
        void  Main() cil managed
{
IL_0000:  call       void a.Test/* 02000002 */::ShowMessageA() /* 06000002 */
IL_0005:  nop
IL_0006:  ret
} // end of method Test::Main
Листинг 2. Текст метода Main


Надо заметить, что в .NET используется не x86 ассемблер, а специализированный язык, называемый IL (Intermediate Language). Любая программа, на каком языке бы она не была написана (C#, Visual Basic, C++) транслируется в IL-код, который, в свою очередь, компилируется в команды x86 ассемблера при выполнении программы. Такая технология носит название JIT-компиляции, от Just-in-Time compiling.

Из приведенного листинга видно, что весь метод Main состоит из трех IL команд. Нас интересует только самая первая команда, собственно и являющаяся вызовом функции ShowMessageA().

Теперь давайте разберемся, как данная строка:
void a.Test/* 02000002 */::ShowMessageA() /* 06000002 */
коррелирует с содержимым таблиц потока #~?

Так как вся информация в метаданных хранится в табличном виде, то ссылки на различные структуры записываются в виде токенов. Токен состоит из двух частей - идентификатора таблицы и номера строки в ней. Обратим внимание на рис. 2: в скобках после имени каждой таблицы идет число. Это и есть идентификатор таблицы. Добавив к нему номер строки, получим токен, указывающий конкретную строку в конкретной таблице. Например, токен 0x01000001 указывает на первую строку в таблице TypeRef. Соответственно, строка вызова ф-ции ShowMessageA() в методе Main "магическим образом" преобразуется в обращение к двум таблицам (рис 4.):

1. a.Test/* 02000002 */ - токен, указывающий на вторую строку таблицы TypeDef
2. ShowMessageA() /* 06000002 */ - токен, указывающий на вторую строку таблицы Method

рис. 4. Записи в таблицах TypeDef и Method

Разбор полей, содержащихся в строках этих таблиц, осуществляется подобным указанному выше образом. Либо поле содержит простое значение, но не ссылку (например поле Flags в таблице TypeDefs), либо токен, указывающий на другую таблицу. Если полностью "развернуть" цепочку вызова метода, то в графическом виде она будет выглядеть так:

рис. 5. Ссылки на таблицы потока #~ из кода

Таким образом, добавляя соответствующие записи в таблицы, мы теоретически можем внедрить свои функции в код любого исполняемого файла .NET.

Однако при рассмотрении такой методики возникает закономерный вопрос - как наш код, внедренный подобным образом, сможет обращаться к каким-либо внешним библиотечным функциям, которые не используются в изменяемой программе? Разумеется, мы можем добавить соответствующие записи в таблицы, содержащие описание внешних библиотек и их ф-ций, однако данная задача представляется чересчур громоздкой и противоречит третьему условию нашей задачи - реализация механизма внедрения кода должна быть максимально простой и надежной.

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

Соответственно, решение задачи сводится к добавлению в метаданные изменяемого модуля соответствующих записей для вызова внешней ф-ции и добавлению небольшого участка IL-кода, вызывающего данную ф-цию.

Проследим цепочку записей в таблицах метаданных, которая используются для вызова внешней по отношению к модулю функции. Для этого используем следующую программу на языке C# (см. каталог с проектом ExtFunctionCaller):

<FONT size=-1>
using System;
using System.Windows.Forms;
using TestNamespace;

namespace a {
    public class Test {
	[STAThread]
	private static void Main() {
	    TestClass.TestMethod();
	}
    }
}
</FONT>
Листинг 3. Текст программы, содержащей вызов внешней ф-ции

Так же нам необходимо создать внешний модуль, в котором будет находится ф-ция TestMethod() (См. каталог с проектом ClassLibrary1 ):

<FONT size=-1>
using System.Windows.Forms;

namespace TestNamespace {
    public class TestClass {
	public TestClass() {}
	public static void TestMethod() {
	    MessageBox.Show("Call from external module");
	}
    }
}
</FONT>
Листинг 4. Текст программы внешнего модуля

Загрузив в дизассемблер ILDasm.exe скомпилированную программу, содержащую вызов функции TestMethod(), получим следующий листинг IL-кода для функции Main:

.method /*06000001*/ private hidebysig static 
        void  Main() cil managed
{
IL_0000:  /* 28 | (0A)00000F  */ call void [ClassLibrary1/* 23000002 */]
					TestNamespace.TestClass/* 01000010 */
					::TestMethod() /* 0A00000F */
IL_0005:  /* 00   |                  */ nop
IL_0006:  /* 2A   |                  */ ret
} // end of method Test::Main
Листинг 5. Текст метода Main

В очередной раз представим граф обхода таблиц метаданных, обход начинаем с токена 0x0A00000F, т.к. именно он является "отправной точкой" при вызове функции в IL-коде:

рис. 6. Граф вызова функции из внешнего модуля

Проанализировав граф, можно сделать следующий вывод:
Вызов функции из внешнего модуля осуществляется обращением к строке в таблице MemberRef. Указываемая строка в этой таблице содержит следующие ссылки:

1. Class - описание класса: токен строки в таблице TypeRefs
2. Name - наименование вызываемого метода: смещение в потоке #Streams
3. Signature - сигнатура параметров, передаваемых ф-ции: смещение в потоке #Blob

Строка в таблице TypeRef, в свою очередь содержит следующие ссылки:

1. ResolutionScope - используемый внешний модуль: токен строки в таблице AssemblyRef
2. Name - наименование класса: смещение в потоке #Strings
3. Namespace - наименование пространства имен: смещение в потоке #Strings

И, наконец, последняя запись в цепочке - это описание внешнего модуля, содержащееся в таблице AssemblyRef. Данная запись содержит только одно поле, представляющее интерес для нас - это поле Name, содержащее имя файла внешнего модуля.

Таким образом, для добавления вызова функции из внешнего модуля необходимо добавить записи в следующие таблицы потока #~:
1. AssemblyRef
2. TypeRef
3. MemberRef

Так же необходимо добавить четыре записи в поток #Streams:
1. Наименование библиотеки
2. Наименование класса
3. Наименование пространства имен
4. Наименование вызываемой ф-ции

В поток #Blob так же необходимо добавить одну запись, содержащую сигнатуру параметров, передаваемых функции.

Механизм внедрения

Для того, чтобы осуществлять какие-либо операции над метаданными (в частности - добавление строк в таблицы), необходимо скопировать весь блок метаданных в свободное место в конце исполняемого файла. Если количество свободного места в файле недостаточно для хранения в нем метаданных, то необходимо увеличить размер последней секции файла (как физический, так и отображаемый).

Процесс внедрения можно разбить на следующие этапы:

1. Открытие файла
2. Чтение PE структур
3. Чтение и разбор метаданных
4. Добавление в поток #Strings строк с именами функции, класса, пространства имен и сборки
5. Добавление в поток #Blob сигнатуры вызова функции
6. Добавление в поток #~ строк в таблицы AssemblyRef, TypeRef, MemberRef
7. Модификация функции, указанной как точка входа: добавляем в ф-цию код для вызова внешней функции
8. Сохранение всех измененных структур в свободное место в конце файла, с корректировкой соответствующих указателей на структуры.

Так как объем исходных текстов разработанной программы превышает 190 Кбайт, решено не приводить их целиком в тексте статьи. Для тех читателей, которых заинтересуют исходные тексты программы, ниже дано краткое описание классов и выполняемых ими ф-ций:

1. PEFile - чтение и изменение PE-структур файла
2. MetaData - чтение, разбор, модификация метаданных модуля
3. CompressedStream - работа с потоком #~
4. StringStream - работа с потоком #Strings
5. BlobStream - работа с потоком #Blob
6. Transformer - централизованное управление работой всех классов.

Чтобы читатель мог получить более подробное представление о механизме внедрения, рассмотрим инициализацию и работу класса Transformer.


    // Создаем объект класса Transformer, передавая ему имя модифицируемого файла
    Transformer* tr = new Transformer(argv[1]);

    // Производим установку параметров, таких как: имя класса, имя функции, 
    // наименование пространства имен, наименование библиотеки, сигнатуру.
    // Данные параметры специфичны для внедряемой библиотеки

    tr->SetNames("TestMethod", "TestClass", "TestNamespace", "ClassLibrary1");
    BYTE blob[] = { 0x03,
                    0x00, 0x00, 0x01
                    };
    tr->SetBlob(blob, 4);
    tr->SetAssemblyRefInfo(0x01, 0x00, 0x0704, 0x543a, 0x00, 0x00, 0x00, 0x00);

    // Производим внедрение
    tr->DoTransform();
    destroy(tr);
Листинг 6. Инициализация начальных значений и запуск процесса модификации

Пусть читателя не смущают бинарные данные, передаваемы в функции Set... класса Transormer - эти данные могут быть получены из соответствующих таблиц в метаданных тестовой программы, которая использует внедряемую сборку. В нашем случае информация была взята из таблиц проекта ExtFunctionCaller.

Программа, иллюстрирующая методику внедрения кода, находится в каталоге dnj_console. Пример модифицированного файла можно найти в каталоге Victim: Grid.exe - оригинальное, не модифицированное приложение; grid_mod.exe - приложение, в которое был внедрен вызов функции из внешнего модуля.

Заключение

В ходе исследования была разработана программа, удовлетворяющая всем поставленным условиям. Хочется отметить, что большая часть времени, затраченного в рамках проекта, пришлась на разработку функционала, осуществляющего разбор и чтение метаданных файла.
Так же необходимо упомянуть о том, что область использования данной методики немного ограничена: так называемые "strong named assemblies" не могут быть модифицированы. При запуске подобных сборок .NET Framework проверяет аутентичность информации, содержащейся в сборке, используя механизм цифровой подписи.
Однако, данное ограничение можно обойти, если производить внедрение не в саму программу, а в какую-либо из используемых библиотек.

Мы расшифровали формулу идеальной защиты!

Спойлер: она начинается с подписки на наш канал

Введите правильный пароль — подпишитесь!