Статья Защищаем код с помощью VEH и INT3

Admin

Администратор

Защищаем код с помощью VEH и INT3​


Сегодня мы с вами возьмем, да и слепим из говна и палок своего рода «софтварный анклав» (с) или же «полноценный антидамп» (с) для x86-кода под Вендой (хотя для x64 должно работать так же с минимальными изменениями). Это будет не то, чтобы убер-технология для всяких пакеров и протекторов, или же хотя бы чуточку практичная технология в том виде, в котором мы будем ее рассматривать, но мне кажется, что будет интересно. Ведь от пруф оф концепта до практичной технологии чуть меньше шагов, чем от ничего до практичной технологии. И да, конкретно эта моя реализация никоим образом (кроме концептуально-идейной составляющей) не относится к «софтварному анклаву» (с) всяких там элитных спецов, о которых вы могли слышать. У них там своя эльфийская магия. Ну да ладно.

Содержание:
1. Введение
2. Обзор технологии
3. Разработка крекмиши
4. Разработка генератора стабов
5. Разработка кода стаба
6. Заключение

1. Введение

Что же мы будем подразумевать под «софтварным анклавом»? Далее будет небольшая справочка по Intel SGX, кто в курсе, можете этот абзац пропустить. Тем более, что Intel SGX — не бро, а говно и палки — наше всё. В общем… есть такая замечательная технология Intel SGX. Предназначена она для того, чтобы производить безопасные вычисления в потенциально опасной среде. Например, представим, что вы некая компания, которая хочет «хранить свои секретики» (и свои разработки) подальше от чужих глаз. Потому, что вы разработали убер важный для компании и убер успешный алгоритм или целую программу. Вы хотите его/ее преподнести миру, но при этом заработать денежку и не позволить вашим конкурентам с помощью злых реверсеров анализировать и создавать аналоги вашего убер-алгоритма/программы, ну и зарабатывать таким образом на вас деньги. Было бы здорово запустить вашу программу в виде сервиса в интернете, например, на какой-то из облачных платформ. Но сервис в интернете — это ни разу не доверенная среда, его могут подломать злые рашн хакерс и выкачать все ваши секретики, которые вы так хотели хранить при себе. На выручку в этом случае может прийти технология Intel SGX. Она на аппаратном уровне создает защищенную область данных и кода — «анклав», при этом этот самый анклав защищен в том числе и от ядра операционной системы, то есть все гипер пупер безопасненько (ну кроме уязвимостей самой технологии, которые уже были опубликованы). Ваши ценные код/данные/секретики будут существовать и функционировать в защищенной области (анклаве), в которую извне можно только передать входные данных и получить результат вычислений обратно. Выкусите, господа ваши конкуренты со своими злыми рашн хакерс и реверсерс.

К сожалению, все было бы так просто, если бы не было так сложно. Поддержка Intel SGX есть далеко не везде, внутри анклава может существовать далеко не каждый код (например, вызывать API-функции операционной системы и сисколлы невозможно, насколько я это понимаю). Так вот в один прекрасный исторический момент воспаленный разум одного широко известного в узких кругах спеца породил идею сделать своего рода программную эмуляцию того, как работает аппаратная технология Intel SGX. То есть концептуально содержать важный код и данные в некоей защищенной (защифрованной) области — анлаве, но при этом каким-то образом его исполнять. Так появился термин «софтварный анклав». Этот термин был не очень удачным, так как многие подумали, что имеется ввиду Intel SGX, ведь технология в самом своем названии несет слово «софтварный» (SGX — Software Guard Extensions), хотя и немного в другом смысле. Повинуясь воли спеца, в дальнейшем мы будем называть «софтварным анклавом» именно программную эмуляцию «аппаратных анклавов» (Intel SGX и мелкомягких VBS — Virtualization Based Security, а не VBScript, о котором вы подумали).

Для чего это может понадобиться нам с вами? Мы, конечно же, не всякие там компании, но у нас тоже есть свои «секретики». Ну во-первых, мы можем таким образом усложнить жизнь реверсеру, который будет наш код анализировать. Безусловно, достаточно скилловые и целеустремленные реверсеры рано или поздно все равно добьются своего, поэтому, как и при обфускации кода, непреодолимой защиты быть не может, но подзаебать реверсера таким образом мы можем. Во-вторых, в это жестоком мире существуют аверы (антивирусы) с фетишом на периодические сканирования виртуальной памяти процессов. В теории «софтварный анклав» доставил бы им некоторое количество хлопот с детектом защищенного кода. В-третьих, часто «дятлы» (это - не обидный термин, их и правда так называют в их антивирусных компаниях) для снятия протектора с малвари просто ждут, когда малварь будет запущена в памяти процесса протектора, делают дамп виртуальной памяти процесса и уже из дампа извлекают малварь на анализ. Так вот «софтварный анклав» в этом смысле может считаться «полноценным антидампом», так как в дампе нашего защищенного кода не будет в открытом виде.

Конечно, программная эмуляция, как бы мы ее не реализовывали, всегда будет уступать по своей секьюрности и качеству проработки аппаратной, сделанной умными дядьками из крупных компаний. Конкретно реализация из этой статьи базируется на VEH (обработке векторных исключений) и инструкции INT3 (debug break — программной точки останова для отладки). В теории из этой базовой технологии можно было бы сделать качественный и применимый на практике продукт, но «делать этого мы конечно же не будем», поскольку это потребовало бы решения кучи подводных камней — такой, которую в одной статье не разгрести. Поэтому мы просто рассмотрим технологию на базовом уровне, а потом реализуем ее в виде небольшой крекмиши (crackme), которую я делал для того, чтобы в очередной раз быть облитым желчью и гуаном (говно летучей мыши) на форуме спецов реверсеров (не спрашивайте, зачем, это мой крест и мне его нести, у самурая нет цели, есть только путь).

Важно еще отметить, что я буду опускать некоторые понятные и общие для всех пакеров и/или протекторов вещи, которые были обмусолены вдоль и поперек в других статьях нашего уютненького комьюнити. Если что-то не понятно, спрашивайте в комментариях к статье, постараюсь либо объяснить, либо кинуть ссылку на статью, если смогу ее найти (хотя не очень дружу с местным поиском).

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

2. Обзор технологии

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

Это штуковину можно сделать несколькими способами вплоть до создания виртуальной машины для исполнения кода (вы же ждете статью на эту тему?). Я же предлагаю сделать следующим образом. Мы напишем протектор, который будет загружать полезную нагрузку в виртуальную память текущего процесса, как это обычно бывает, но вместо секции .text (секции исполняемого кода) у нас будет большая портянка из байта 0xCC (инструкции INT3, она же debug break). Инструкция INT3 вызывает исключение (программную ошибку), когда процессор на нее «наступает». Мы можем отловить это исключение, получить адрес инструкции INT3 из контекста потока, расшифровать на место инструкции INT3 одну инструкцию защищаемого кода и продолжить исполнение. Таким образом, мы выполним только одну инструкцию, а исполнение снова прервется на следующей за инструкцией INT3. Конечно, прежде чем расшифровывать следующую инструкцию, нужно будет «залить» предыдущую инструкцию байтами 0xCC для того, чтобы в единицу времени в памяти у нас была расшифрована только одна инструкция.

Отлавливать исключения мы будем с помощью векторной обработки исключений, она же VEH. Это просто и довольно легко реализуется. Кроме того, нам понадобится еще и дизассемблер длин для того, чтобы определить, сколько байт нам нужно расположить в секции текста при расшифровке инструкции. Благо в операционную систему мелкомягких давно встроен приемлемый дизассемблер в библиотеке dbgeng.dll, ну вы, наверное, об этом уже читали в одной из моих предыдущих статей. Этот дизассемблер еще и умеет выводить красивую читаемую строку из байтов инструкции, что пригодится нам при отладке. Например, если при исполнении инструкции будет возникать исключение, которое мы не ожидаем увидеть.

3. Разработка крекмиши

Для начала нам нужно разработать небольшую тестовую программу, которую мы затем будем защищать своим «анклавом». Я предлагаю сделать вот что: программа будет считывать пароль пользователя из stdin, хешировать его каким-то предельно простым алгоритмом, а затем сравнивать с эталонным значением. Своего рода классический крекми, рассмотрим код:
C++:
#include <windows.h>

constexpr DWORD cp_hash_string(LPCSTR str, DWORD seed) {
DWORD result = seed;
for(INT i = 0; str[i] != '\0'; i++) {
DWORD v1 = result * 0xab10f29f;
DWORD v2 = v1 + str[i];
result += v2 & 0xffffff;
}

return result;
}

const DWORD hash_seed = cp_hash_string(__DATE__ __TIME__, 0);
const INT buffer_size = 256;

static HANDLE stdout = INVALID_HANDLE_VALUE;
static HANDLE stdin  = INVALID_HANDLE_VALUE;

VOID init_std_handles() {
stdout = GetStdHandle(STD_OUTPUT_HANDLE);
stdin  = GetStdHandle(STD_INPUT_HANDLE);
}

VOID write_line(LPCSTR line) {
DWORD written = 0; INT length = lstrlenA(line);
WriteConsoleA(stdout, line, length, &written, NULL);
WriteConsoleA(stdout, "\n", 1, &written, NULL);
}

VOID read_line(LPSTR buffer, INT length) {
for(INT i = 0; i < length; i++) {
CHAR chr = 0; DWORD read = 0;
if(!ReadConsoleA(stdin, &chr, 1, &read, NULL))
{ break; }

if(chr == '\n' || chr == '\r')
{ buffer[i] = '\0'; break; }
else { buffer[i] = chr; }
}
}

extern "C" void entry_point() {
init_std_handles();
write_line("Enter password, bro: ");

CHAR password[buffer_size];
read_line(password, buffer_size);

DWORD hash = hash_seed;
for(INT i = 0; password[i] != '\0'; i++) {
DWORD v1 = hash * 0xab10f29f;
DWORD v2 = v1 + password[i];
hash += v2 & 0xffffff;
}

if(hash == cp_hash_string("TestDatAntidumpShit", hash_seed)) {
write_line("Password is valid, bro!");
write_line("You are real cracker, bro!");
} else {
write_line("Password is invalid, bro!");
write_line("Better luck next time, bro!");
} 

ExitProcess(0);
}

Подключаем вендовый заголовок и поехали кодить. Constexpr функция cp_hash_string реализует простой алгоритм хеширования строки с указанием зерна (стартового значения) для алгоритма. Поскольку она объявлена как constexpr, то она будет выполняться на этапе компиляции, а на ее место ее вызова в коде будет подставлено значение хеш функции от константной строки. Далее определяем две константы. Первая — уникальное зерно для алгоритма хеширования в зависимости от даты и времени компиляции. Вторая — размер буфера на стеке для строки, которую будет вводить пользователь. Затем обозначим две глобальные переменные, которые в процессе работы получат хендлы stdout и stdin соответственно. Функция init_std_handles получает и сохраняет хендлы stdout и stdin. Функция write_line записывает строку с stdout. Функция read_line считывает строку из stdin. Конечно, проделать эти операции можно было бы и функциями из CRT (типа printf и scanf), но мне хотелось, чтобы в этом семпле была только одна импортируемая библиотека — kernel32.dll (собирать его мы будем без зависимостей от CRT). Функция entry_point является точкой входа программы, в ней, собственно, все и происходит. Спрашиваем пользователя о пароле, получаем от него строку, хешируем строку и сравниваем с эталонным значением. В зависимости от результатов сравнения выводим пользователю, молодец он или нет. Теперь давайте собирать этот код в экзешничек:
Bash:
i686-w64-mingw32-gcc.exe -o payload.exe  -fno-exceptions -fno-rtti -fno-ident -O3 -Os payload.cpp  -fno-ident -s -Wl,--pic-executable -e_entry_point -nostdlib -lkernel32 -lmsvcrt

Как настоящий маргинал-нонконформист я использовал для сборки MinGW, но вы можете аналогичным образом собрать этот код с помощью Visual Studio или Clang, но постарайтесь, сделать это без зависимостей от CRT (для простоты в проекторе я опустил обработку всяких TLS’ов и других несуразностей, которые бывает тянутся за стандартными цешными библиотеками). Код написан на С++, но не использует никакие плюсовые фичи, которые тянут за собой CRT. И обратите внимание, что исполняемый файл нам нужен с релоками, таким образом его можно будет загружать в произвольное место в виртуальной памяти, он не будет привязан к одному адресу, который может быть уже занят на момент загрузки исполняемого файла протектором. По поводу релоков можно более подробно прочитать в других статьях, посвященных непосредственно PE-формату и загрузке экзешничков в память. Ну вроде все сказал, давайте переходить к разработке кода протектора.

4. Разработка генератора стабов

Чтобы не сильно заморачиваться, я решил сделать билдер для стабов нашего протектора на самом простом и понятном языке — на этом вашем Петоне. Из исполняемого файла билдер будет брать только те данные, которые в последствии понадобятся стабу для успешной загрузки нашего защищаемого кода (крекми). Для получения и извлечения этих данных я решил воспользоваться готовой библиотекой, которую можно поставить через pip, а именно — pefile (парсер PE-формата). Рассмотрим код генератора стабов:
Python:
import pefile

def encrypt_text_section(data: bytes) -> bytes:
result = bytearray()
for i in range(len(data)):
xor1 = result[i - 1] if i != 0 else 0
xor2 = ((42 + i) * 48271) & 0xFF

byte = (data[i] ^ xor2) & 0xFF
byte = (byte ^ xor1) & 0xFF 
result.append(byte)

return result

def encrypt_section(index: int, data: bytes) -> bytes:
index = index * 48271

result = bytearray()
for i in range(len(data)):
xori = ((index + i) * 48271) & 0xFF
byte = (data[i] ^ xori) & 0xFF
result.append(byte)

return result

with open("payload.hpp", "w", encoding="utf-8") as fil:
pe = pefile.PE("payload.exe")

fil.write("#pragma once\n")
fil.write("#include <windows.h>\n\n")

image_base = pe.OPTIONAL_HEADER.ImageBase
size_of_image = pe.OPTIONAL_HEADER.SizeOfImage
entry_point = pe.OPTIONAL_HEADER.AddressOfEntryPoint
section_count = len(pe.sections)

imports_dir = pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_IMPORT"]
imports_va = pe.OPTIONAL_HEADER.DATA_DIRECTORY[imports_dir].VirtualAddress

relocs_dir = pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_BASERELOC"]
relocs_va = pe.OPTIONAL_HEADER.DATA_DIRECTORY[relocs_dir].VirtualAddress

fil.write("const DWORD PayloadImageBase  = 0x{0:08X};\n".format(image_base))
fil.write("const DWORD PayloadImageSize  = {0};\n".format(size_of_image))
fil.write("const DWORD PayloadSections   = {0};\n".format(section_count))
fil.write("const DWORD PayloadEntryPoint = 0x{0:08X};\n".format(entry_point))
fil.write("const DWORD PayloadImportsRVA = 0x{0:08X};\n".format(imports_va))
fil.write("const DWORD PayloadRelocsRVA  = 0x{0:08X};\n".format(relocs_va))

fil.write("\n")
for i in range(len(pe.sections)):
data = pe.sections[i].get_data()
if i == 0: data = encrypt_text_section(data)
else: data = encrypt_section(i, data)

data = ["0x{0:02X}".format(x) for x in data]
data = ', '.join(data)

fil.write("static BYTE PayloadSection{0}Data[] = {{ {1} }};\n".format(i, data))

fil.write("\n")
pointers = ["PayloadSection{0}Data".format(i) for i in range(len(pe.sections))]
fil.write("static PBYTE PayloadSectionsData[] = {{ {0} }};\n\n".format(", ".join(pointers)))

sizes = [str(section.SizeOfRawData) for section in pe.sections]
fil.write("static DWORD PayloadSectionsSize[] = {{ {0} }};\n\n".format(", ".join(sizes)))

addresses = ["0x{0:08X}".format(section.VirtualAddress) for section in pe.sections]
fil.write("static DWORD PayloadSectionsRVA[] = {{ {0} }};\n\n".format(", ".join(addresses)))

Функции encrypt_text_section и encrypt_section используются для шифрования секции с кодом и других секций соответственно. Для секции с кодом я решил сделать технологию «блокчейн». Шучу. В общем для усложнения патчинга крекмиши каждый байт шифрованного кода зависит от предыдущего байта, то есть, если реверсер решит пропатчить 1 байт шифрованного кода, то ему нужно будет менять не один байт, а один байт и все байты после него. Остальные секции будут лежать в памяти в открытом виде, поэтому шифрование там не принципиально. Число 48271 — магическое, оно взято из minstd_rand стандарта C++11, чтобы получать шифрованные данные с хоть чуточку высокой энтропией. Но это не суть важно. Безусловно, тут можно было бы навернуть и нормального шифрования, но это вы сможете сделать в качестве домашнего задания, ведь это не является сутью текущей статьи.

Далее мы считываем файл payload.exe, скармливаем его библиотеке pefile и достаем оттуда: размер образа исполняемого файла в виртуальной памяти, базовый адрес по-умолчанию, смещение точки входа, смещения таблиц импорта и релоков, количество секций и данные всех секций. Из этих данных мы формируем файл payload.hpp, который затем подключим к основному файлу с кодом стаба. Секция .text в 99% случаев будет первой (как и в нашем случае), ей мы будем манипулировать отдельно от всех остальных в стабе. Я намерено избавился от всех ненужных нам заголовков PE-формата, поскольку отсутствие заголовков еще чуточку усложняет сам дампинг и разбор дампа с помощью всяческих интеллектуальных дизассемблеров. Ну давайте теперь переходить к самому интересному — коду стаба.
C++:
#pragma once
#include <windows.h>

const DWORD PayloadImageBase  = 0x00400000;
const DWORD PayloadImageSize  = 32768;
const DWORD PayloadSections   = 7;
const DWORD PayloadEntryPoint = 0x00001116;
const DWORD PayloadImportsRVA = 0x00006000;
const DWORD PayloadRelocsRVA  = 0x00007000;

static BYTE PayloadSection0Data[] = { /* ... */ };
static BYTE PayloadSection1Data[] = { /* ... */ };
static BYTE PayloadSection2Data[] = { /* ... */ };
static BYTE PayloadSection3Data[] = { /* ... */ };
static BYTE PayloadSection4Data[] = { /* ... */ };
static BYTE PayloadSection5Data[] = { /* ... */ };
static BYTE PayloadSection6Data[] = { /* ... */ };

static PBYTE PayloadSectionsData[] = { PayloadSection0Data, PayloadSection1Data, PayloadSection2Data, PayloadSection3Data, PayloadSection4Data, PayloadSection5Data, PayloadSection6Data };

static DWORD PayloadSectionsSize[] = { 512, 512, 512, 512, 512, 512, 512 };

static DWORD PayloadSectionsRVA[] = { 0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000, 0x00007000 };

Вот так примерно будет выглядеть сгенерированный петоновским скриптом заголовочный файл. Все предельно просто. Нам нужен заголовочный файл windows.h для определения DWORD, BYTE и PBYTE типов. Каждая из секций полезной нагрузки была зашифрована и попала в свой отдельный статический массив байт. Для удобства указатели на все секции, их размеры и RVA (relative virtual address — смещение секции относительно базового адреса загруженного в виртуальную память исполняемого файла) хранятся в отдельных массивах. Остальные необходимые нам данные из PE-заголовков определены как константы.

5. Разработка кода стаба

Итак, код стаба. Наш стаб должен выделить буфер виртуальной памяти, достаточный, чтобы разместить в нем исполняемый файл, размер этого буфера определяет константа PayloadImageSize. Для простоты мы не сохраняли права доступа к секциям, а просто выделим большой блок памяти с правами на чтение, запись и исполнение. Это не очень хорошо в целом, но для нашего пруф оф концепта - достаточно. Затем наш стаб должен правильно расшифровать и расположить все секции исполняемого файла полезной нагрузки, кроме первой (секции .text — секции с кодом). Место секции .text стаб зальет инструкциями INT3 (байтом 0xCC). Затем стаб должен настроить таблицу импорта, добавить обработчик для VEH и начать исполнение с точки входа исполняемого файла. Расшифровку и подстановку инструкции по одной будет заниматься обработчик VEH исключений. Но давайте кодить это все постепенно, начнем с простых прикладных вещей:
C++:
#include <windows.h>
#include <dbgeng.h>
#include <stdio.h>

#include "payload.hpp"

#define DO_DEBUG     0
#define MAX_INS_SIZE 16

#define ERR(...) on_error(__LINE__, ##__VA_ARGS__)

VOID on_error(DWORD line, DWORD code = 0) {
if(code == 0) { code = GetLastError(); }
_printf_l("%d - %08X\n", 0, line, code);
ExitProcess(line);
}

Подключаем необходимые нам заголовочные файлы, включая payload.hpp. Определим макросы препроцессора: DO_DEBUG — собираем ли мы отладочную версию нашего кода или нет, а MAX_INS_SIZE — определяет максимально возможный размер инструкции x86 (на практике в коде полезной нагрузки не будет инструкций больше 12 байт длиной, но, насколько, я помню максимально возможная длина инструкции x86 равна 16, так что пусть будет 16 для порядка). Макрос ERR и функция on_error отвечают за обработку ошибок. В контексте данного пруф оф концепта, если что-то идет не по нашему плану, просто вываливаем код ошибки в консоль и прибиваем свой процесс вызовом ExitProcess.
C++:
static const IID IDebugClientGuid   = { 0x27FE5639, 0x8407, 0x4F47, { 0x83, 0x64, 0xEE, 0x11, 0x8F, 0xB0, 0x8A, 0xC8 }};
static const IID IDebugControl3Guid = { 0x7DF74A86, 0xB03F, 0x407F, { 0x90, 0xAB, 0xA2, 0x0D, 0xAD, 0xCE, 0xAD, 0x08 }};

static IDebugClient*   debug_client  = NULL;
static IDebugControl3* debug_control = NULL;

BOOL debugger_attach_self() {
CoInitialize(NULL);

IDebugClient* client = NULL;
HRESULT res = DebugCreate(IDebugClientGuid, (LPVOID*)&client);
if(FAILED(res)) { ERR(res); return FALSE; }

IDebugControl3* control = NULL;
res = client->QueryInterface(IDebugControl3Guid, (LPVOID*)&control);
if(FAILED(res)) { ERR(res); return FALSE; }

ULONG flags = DEBUG_ATTACH_NONINVASIVE
| DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND;

DWORD pid = GetCurrentProcessId();
res = client->AttachProcess(0, pid, flags);
if(FAILED(res)) { ERR(res); return FALSE; }

res = control->WaitForEvent(DEBUG_WAIT_DEFAULT, INFINITE);
if(FAILED(res)) { ERR(res); return FALSE; } 

debug_control = control;
debug_client = client;
return TRUE;
}

Помните, как я говорил, что нам понадобится дизассемблер длин? Мы могли бы статически слинковать какую-либо из существующих библиотек для этого, но зачем это делать, если в системе уже есть подходящий для нас дизассемблер длин. Мы уже рассматривали его в одной из моих предыдущих статей, но давайте я вкратце напомню, что и как. С помощью COM и библиотеки dbgeng.dll мы создаем отладочного клиента IDebugClient, запрашиваем у него интерфейс IDebugControl3, клиента подключаем к текущему процессу и обрабатываем все события, связанные с его подключением. В последствии у интерфейса IDebugControl3 нам будет доступен метод Disassemble, с помощью которого мы и будем получать длину инструкции, а в отладочном режиме и дизассемблерный листинг в виде ANSI-строки.

К слову сказать, изначально я хотел реализовать эту идею не через VEH, а непосредственно с помощью перехвата отладочных событий IDebugClient через SetEventCallbacks и класса, реализующего интерфейс IDebugEventCallbacks, но, к сожалению (как выяснилось), это будет работать только для нескольких процессов. То есть процесс не может сам генерировать отладочные события и их же отлавливать, а форкать для этого второй процесс показалось не гуд. Очень жаль, такое решение, если бы работало, то выглядело бы куда красивее. Ну что же, имеем то, что имеем. Поехали дальше.
C++:
VOID decrypt_text_section(DWORD index, PBYTE buffer, DWORD size) {
for(DWORD i = 0; i < size; i++) {
DWORD bpos = index + i;
DWORD xor1 = bpos != 0 ? PayloadSectionsData[0][bpos - 1] : 0;
DWORD xor2 = ((42 + bpos) * 48271) & 0xFF;

buffer[i] = PayloadSectionsData[0][bpos] ^ xor2;
buffer[i] = buffer[i] ^ xor1;
}
}

VOID decrypt_section(DWORD index, PBYTE buffer, DWORD size) {
for(DWORD i = 0; i < size; i++) {
BYTE xori = ((index * 48271 + i) * 48271) & 0xFF;
buffer[i] = PayloadSectionsData[index][i] ^ xori; 
}
}

Сделаем функции для «расшифровки» наших «зашифрованных» секций. Они ровным счетом такие же, как и были реализованы в генераторе на этих ваших Петонах. Код тут должен говорить сам за себя, поэтому останавливаться на нем более не будем.
C++:
static LPVOID image = NULL;

BOOL fix_one_reloc(PBYTE address, DWORD inslen) {
auto ibr = (PIMAGE_BASE_RELOCATION)((PBYTE)image + PayloadRelocsRVA);
auto ofs = (PBYTE)image - PayloadImageBase;

typedef struct _IMAGE_RELOC {
WORD offset :12;
WORD type :4;
} IMAGE_RELOC, *PIMAGE_RELOC;

while(ibr->VirtualAddress != 0) {
auto list = (PIMAGE_RELOC)(ibr + 1);

while((PBYTE)list != (PBYTE)ibr + ibr->SizeOfBlock) {
if(list->type == IMAGE_REL_BASED_HIGHLOW) {
auto pointer = (PBYTE)image + ibr->VirtualAddress + list->offset;
if(pointer >= address && pointer + sizeof(ULONG_PTR) <= address + inslen)
{ *(ULONG_PTR*)(pointer) += (ULONG_PTR)ofs; return TRUE; }
} else if(list->type != IMAGE_REL_BASED_ABSOLUTE)
{ ERR(); return FALSE; }

list++;
}

ibr = (PIMAGE_BASE_RELOCATION)list;
}

return TRUE;
}

В глобальной переменной image мы будем хранить базовый адрес, где именно в виртуальной памяти наш стаб расположит секции полезной нагрузки. Функция fix_one_reloc проходит секцию релоков целиком в поисках того, есть ли релок в нашей одной конкретной инструкции. Если он есть, она патчит инструкцию на действительный адрес, исходя из значения переменной image. Если его нет, инструкция остается неизменной. Я портировал этот код из проекта donut потому, что зачем переписывать с нуля то, что уже было более долбаной тысячи раз написано.
Код:
static PBYTE old_address = NULL;
static DWORD old_length = 0;

LONG WINAPI vectored_handler(PEXCEPTION_POINTERS exception) {
if(old_address != NULL && old_length != 0) {
for(DWORD i = 0; i < old_length; i++)
{ old_address[i] = 0xCC; }

old_address = NULL;
old_length = 0;
}

auto text_ptr = (PBYTE)image + PayloadSectionsRVA[0];
auto text_end = text_ptr + PayloadSectionsSize[0];

auto address = (PBYTE)exception->ContextRecord->Eip;
if(address < text_ptr) { ERR(); return EXCEPTION_BREAKPOINT; }
if(address > text_end) { ERR(); return EXCEPTION_BREAKPOINT; }

auto offset = (DWORD)(address - text_ptr);
auto bufsiz = (DWORD)(text_end - offset);

BYTE buffer[MAX_INS_SIZE];
if(bufsiz > MAX_INS_SIZE)
{ bufsiz = MAX_INS_SIZE; } 

decrypt_text_section(offset, buffer, bufsiz);

CHAR disasm[128];
ULONG64 next = 0;
DWORD dislen = 0;

auto hres = debug_control->Disassemble((ULONG64)buffer, 0, disasm, 128, &dislen, &next);
if(FAILED(hres)) { ERR(); return EXCEPTION_BREAKPOINT; }

DWORD inslen = (DWORD)(next - (ULONG64)&buffer[0]);

#if DO_DEBUG
_printf_l("%p %d %s", 0, address, inslen, disasm);
#endif

for(DWORD i = 0; i < inslen; i++)
{ address[i] = buffer[i]; }

fix_one_reloc(address, inslen);
old_address = address;
old_length = inslen;

return EXCEPTION_CONTINUE_EXECUTION;
}

Теперь давайте реализуем обработчик VEH исключений. Нам нужно сохранять и получать адрес и размер предыдущей инструкции, для этого мы объявим две глобальные переменные: old_address и old_length. Поскольку у нас однопоточная полезная нагрузка, то это нормальное решение. Если нам было бы нужно поддерживать многопоточность, то эти данные можно было бы хранить в TLS (thread local storage), таким образом у всех потоков эти переменные были бы свои. Ну или настроить какую-либо синхронизацию потоков при установке и считывании этих переменных.

Обработчик VEH исключений (в нашем случае он называется vectored_handler) принимает указатель на структуру EXCEPTION_POINTERS, которая в свою очередь содержит указатель на контекст потока. Давайте думать о контексте потока, как о наборе значений регистров процессора в тот момент, когда произошло исключение. Ну не орите, я просто упрощаю некоторые вещи, чтобы не вдаваться в какие-то дебри, которые хорошо знать, но понимание их для статьи не так важно.

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

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

С помощью нашего системного дизассемблера длин получаем длину расшифрованной инструкции и в режиме отладочной сборки проекта еще и плюнем в консоль дизасмом этой инструкции. Затем скопируем инструкцию на ее место в виртуальной памяти и настроем ей релок, если он в ней есть. Сохраним адрес и длину текущей инструкции для следующего вызова обработчика и скажем системе, что мы все пофиксили и можно продолжать исполнение с помощью возврата константы EXCEPTION_CONTINUE_EXECUTION.
C++:
BOOL execute_payload() {
image = VirtualAlloc(NULL, PayloadImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if(image == NULL) { ERR(); return FALSE; }

PBYTE tva = (PBYTE)image + PayloadSectionsRVA[0];
for(DWORD i = 0; i < PayloadSectionsSize[0]; i++)
{ tva[i] = 0xCC; }

for(DWORD i = 1; i < PayloadSections; i++) {
PBYTE va = (PBYTE)image + PayloadSectionsRVA[i];
decrypt_section(i, va, PayloadSectionsSize[i]);
}

auto imp = (PIMAGE_IMPORT_DESCRIPTOR)((PBYTE)image + PayloadImportsRVA);
for(; imp->Name != NULL; imp++) {
auto nam = (LPCSTR)((PBYTE)image + imp->Name);

auto dll = LoadLibraryA(nam);
if(dll == NULL) { ERR(); return FALSE; }

auto oft = (PIMAGE_THUNK_DATA)((PBYTE)image + imp->OriginalFirstThunk);
auto ft  = (PIMAGE_THUNK_DATA)((PBYTE)image + imp->FirstThunk);

for(;; oft++, ft++) {
if(oft->u1.AddressOfData == 0)
{ break; }

if (IMAGE_SNAP_BY_ORDINAL(oft->u1.Ordinal)) {
auto ordinal = (LPCSTR)IMAGE_ORDINAL(oft->u1.Ordinal);
ft->u1.Function = (ULONG_PTR)GetProcAddress(dll, ordinal);
continue;
}

auto ibn = (PIMAGE_IMPORT_BY_NAME)((PBYTE)image + oft->u1.AddressOfData); 
ft->u1.Function = (ULONG_PTR)GetProcAddress(dll, ibn->Name);
}
}

AddVectoredExceptionHandler(1, vectored_handler);

typedef VOID (*PEntryPoint)();
auto entry = (PEntryPoint)((PBYTE)image + PayloadEntryPoint);
entry();

return TRUE;
}

extern "C" VOID entry_point() {
if(!debugger_attach_self()) { ERR(); }
if(!execute_payload()) { ERR(); }

ExitProcess(0);
}

Ну чтож, нам осталось только собрать все вместе. В функции execute_payload мы правильно расшифровываем и располагаем секции, а на место секции .text наливаем INT3 инструкций на всю длину секций. Затем мы настраиваем таблицу импорта, чтобы все импортируемые полезной нагрузкой функции были готовы к исполнению. Этот код тоже был портирован из donut по описанным выше причинам. Затем мы добавляем обработчик VEH, указывая, что он должен быть первым в цепочке. Ну и вызываем точку входа, которая на тот момент указывает внутрь нашего большого блока из INT3 инструкций. Функция entry_point — точка входа исполняемого файла, просто инициализирует наш системный дизассемблер длин и вызывает функцию execute_payload. Все просто, а собирать мы это все будет аналогично полезной нагрузке, вот весь батник для сборки:
Bash:
i686-w64-mingw32-gcc.exe -o payload.exe  -fno-exceptions -fno-rtti -fno-ident -O3 -Os payload.cpp  -fno-ident -s -Wl,--pic-executable -e_entry_point -nostdlib -lkernel32 -lmsvcrt
python payload.py
i686-w64-mingw32-gcc.exe -o antidump.exe -fno-exceptions -fno-rtti -fno-ident -O3 -Os antidump.cpp -fno-ident -s -e_entry_point -nostdlib -lkernel32 -lmsvcrt -ldbgeng -lole32

6. Заключение

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

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

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

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

В-пятых, а что будет, если сама полезная нагрузка возжелает перехватывать и обрабатывать исключения? В большинстве случаев – будет так себе.

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

Мы могли бы вообще не отображать исполняемый файл на виртуальную память процесса, а выбирать по одной инструкции и своего рода «интерпретировать» их так, как поступает, например, Петон с собственным байт-кодом. Таскать с собой полноценный эмулятор x86 скорее всего будет накладно, поэтому большую часть инструкций мы могли бы исполнять непосредственно на процессоре. А инструкции, которые потребуют чтения/записи памяти и условные/безусловные переходы придется обрабатывать вручную. Это существенно сложнее представленной в статье методики, да и куда менее портабельно в плане x86 и x64 кода, но скорее всего возможно при должном усердии.

Друзья, спасибо, что дочитали мою статью! Задавайте свои вопросы, пишите свои идеи по тематике статьи, делитесь знаниями!
 
Похожие темы
Emilio_Gaviriya Защищаем облака. Вирусология 0
L Интересно C# - Критичный процесс (Защищаем свой вирус) Программирование 0
M Защищаем свой Python скрипт за пару минут Программирование 0
ev0117434 Интересно Защищаем компьютер от проникновения Анонимность и приватность 8
G Защищаем свой сайт и контент от плагиата Уязвимости и взлом 1
D Защищаем свой мобильный счёт от мошенников и операторов Полезные статьи 4
Admin Интересно Код на GitHub, который хакеры хотели бы удалить – Agentic Threat Hunting Framework уже в сети. Новости в сети 0
Admin Интересно А что, так можно было? В системных утилитах FreeBSD нашли уязвимость, позволяющую исполнять чужой код через обычный роутер. Новости в сети 0
Support81 Три банды вымогателей объединились в "корпорацию зла" — делят код, базы данных и электростанции Новости в сети 0
Support81 Кто-то учился 5 лет, чтобы писать код, а кто-то просто нажимает кнопку в ChatGPT — и получает тот же результат Новости в сети 0
Support81 Код, который должен проверять других, сам стал лазейкой для хакеров — что не так с Langflow? Новости в сети 0
Support81 «PIN-код на лбу безопаснее»: Perplexity AI оказался настоящей находкой для шпиона Новости в сети 0
Support81 PIN-код для свободы: новый Android-шпион категорически против своего удаления Новости в сети 0
Support81 DeepSeek-V3: "мозги" из Китая, которые понимают код лучше вас Новости в сети 0
Support81 Киберпреступники маскируют вредоносный код под reCAPTCHA и Cloudflare Turnstile Новости в сети 0
Support81 Вредоносный код в тегах <img>: новая угроза для онлайн-платежей Новости в сети 0
Support81 Mongolian Skimmer: как Unicode помогает хакерам маскировать вредоносный код Новости в сети 0
Support81 На телефон поступил одноразовый код? Похоже, он предназначен не для вас Новости в сети 0
Support81 TAG-100: открытый код на службе шпионов Новости в сети 0
El_IRBIS Вредоносный код в дистрибутивах Linux: Понимание угрозы и меры безопасности. Вирусология 0
Support81 Темный рыцарь на продажу: в чьих руках окажется исходный код вымогателя Knight 3.0? Новости в сети 0
Support81 Взлом или небрежность? Код и пароли Binance были доступны на GitHub в течение нескольких месяцев Новости в сети 0
Support81 Осторожно, BlackCat: поддельная реклама в поиске WinSCP распространяет вредоносный код Новости в сети 0
У Промо код на 7 дней премиума в игре CROSSOUT. Раздачи и сливы 2
O ONPROXY.NET Приватные прокси по доступным ценам от 25 рублей за штуку. Промо-код: discount на 35% Доступы: RDP, VPS, SQL inj, базы, сайты, shell's 7
Р Бесплатный QR Код для прохода в ТЦ/Рестораны/клубы и тд Раздачи и сливы 2
A Сертификат | QR-код | COVID | Вакцинация Официально Ищу работу. Предлагаю свои услуги. 4
Denik Интересно Баг в Zoom позволял за считанные минуты взломать код доступа Новости в сети 0
S Промо код 75% скидки на VPS ( не уверен но по идее только на 1 месяц ) Раздачи и сливы 2
H Исходный код FortNite Brute Checker Программирование 1
S Опасные изображения. Создаем вредоносный код в картинке Вирусология 6
A Фирма «Cloudflare» открыла код реализации протокола QUIC на языке программирования Rust Новости в сети 0
T Новая уязвимость в VirtualBox, позволяющая выполнить код на стороне хост-системы Полезные статьи 0
G Как написать безопасный код на JS Программирование 0
G Скрытый код Уязвимости и взлом 0
АнАлЬнАя ЧуПаКаБрА Промо-код ivi с месячной подпиской Раздачи и сливы 0
S В Сети опубликован исходный код имитирующего WannaCry вымогателя для Android Новости в сети 0
S Новое вымогательское ПО Sorebrect способно внедрять вредоносный код Новости в сети 0
S код на скидку 350 рублей в Get Taxi Раздачи и сливы 1
X [Для новичков] [DevelStudio исх.код] Активация софта на время! Программирование 8
Glods Исходный код популярного ботнета Mirai Готовый софт 2
Admin Статья Перехват трафика сотовой сети с помощью подменной Базовой Станции (FakeBTS), OpenBTS, YateBTS. Уязвимости и взлом 0
Admin Статья Шпионить за устройствами Apple с помощью CVE-2023-26818 Уязвимости и взлом 0
Admin Статья Выстраиваем простую многозвенную схему с помощью OpenVPN Анонимность и приватность 0
Admin Статья LLM-Polymorphism: Создание самоизменяющегося кода с помощью нейросетей. Вирусология 0
Admin Интересно Китайские хакеры впервые провели почти полностью автоматизированную атаку с помощью Claude. Новости в сети 0
Admin Интересно 0 на VirusTotal и root-доступ: хакеры смогли обмануть все антивирусы с помощью легального софта для админов. Новости в сети 0
Support81 Устройства SonicWall SMA взломаны с помощью руткита OVERSTEP, связанного с программой-вымогателем Новости в сети 0
Support81 Белым по белому: как стать «гением» в науке с помощью ChatGPT Новости в сети 0
Support81 Важно! Мобильный аудит Wi-Fi сетей: как быстро найти уязвимости с помощью Stryker Уязвимости и взлом 0

Название темы