Гайд Защищаем импорты дллки с помощью сервер маппера + VMProtect_Con.exe

Предисловие.

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



Скажу пару слов о структуре статьи. Во первых, я не буду разжевывать тему клиент серверного соединения, сокетов, внутренности PE header,и архитектуры Vmprotect, поскольку эта не является прямой темой этой статьи, если вы не в курсе как это работает, то вы слабо поймете, что написано дальше. Сперва я собираюсь сделать небольшое введение, где будет расписано теория и примеры, что облегчит понимания метода маппинга в тандеме с Vmprotect ,потом я напишу пошаговую инструкцию, где будет рассказан алгоритм действий как воплотить все это в жизнь. Сурс код я выкладывать не буду, не столько потому что я дорожу им ,а больше потому что он очень-очень криво написан. Причина этому заключается в том, что моя цель была не сделать сервер маппер для общего использования или чтобы кто то мог легко спастить это, а просто так называемый Proof of Concept. Поэтому в инструкции будет код, который будет предназначен не для пастинга ,а для того чтобы лучше разъяснить материал читателю. Ну что приступим.

Введение.

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


Простой сервер маппер, с вырезанным ImportDirectory


Так суть этого метода заключается в том, чтобы сервер сделал send на клиент с информацией имен импортов которые ему нужны, клиент сделал ответ на сервер с адресами импортов, сервер их вписал в IAT и затер ImportDirectory(OriginalFirstThunk). Чтобы дамп дллки был рабочий , крекеру нужно просто пройтись по IAT получить каждого адрес импорта, узнать его имя и сделать лоадер, который будет вписывать адреса в соответствующую ячейку IAT. IAT при таком способе маппинга выглядит как в обычном бинарнике за исключением того, что ImportDirectory вырезан . На скрине ниже показан IAT замапленного бинаря(ничего не обычного он такой же как в обычной программе).
1713650001261.png


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



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

1713650023295.png

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

1713650044560.png


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

Вызываем Import через RelativeCall(работает только для 32 бит)


При таком способе мы используем инструкцию relative call e8 ?? ?? ?? ??(где ?? байты оффсета).При таком методе мы не используем IAT и в теории можем его оставить, точно так же как можем не вырезать ImportDirectory(OriginalFirstThunk), так как если даже реверсер заполнит IAT правильно это ему мало чем поможет поскольку мы не будем никогда на него ссылаться при вызове импортов. Выглядит примерно это следующим образом.

1713650085616.png

Как вы видите мы не используем IAT, а просто вызываем импорт по оффсету к нему. В конце вы видите инструкцию nop которая используется для выравнивания,поскольку до маппинга там была инструкция ff 15 ?? ?? ?? ?? , которая на 1 байт длиннее скрин ниже.

1713650119804.png


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

Как вы видите мы не обращаемся больше к IAT.Именно этот метод мы будем использовать в тандеме с вмпротект, который даст нам много преимуществ. Перейдем к практике.

Relative call на импорт + Vmprotect_Con.exe


Так ну начнем. В чем суть. Суть в том, что мы делаем сервер маппер, который будет маппить импорты как в крайнем методе через RelativeCall. После чего мы на сервере через консольное приложение Vmprotect_Con.exe уже бинарник с замапленными импортами мы будем протектить каждый раз при новом инжекте, и отправлять на клиент. И как вы думаете, что придется делать крекеру чтобы сделать лоадер, который будет фиксить импорты? Как вы думаете где будут храниться адреса импортов? Смотрите скрины ниже.
1713650201305.png


Мы видим, что импорт вызывается внутри виртуальной машины его адрес записывается в виртуальный стек(скрин выше) и после чего он вызывается(скрин ниже)
1713650233930.png

Но насколько мы помним Vmrotect_Con.exe протектил дллку с уже замапленными импортами ,которые вызываются с помощью инструкции RelativeCall, то есть вмпротект не берет адрес импорта c IAT,поэтому его патчить бессмысленно. Так откуда же он берет адрес импорта. Откуда взялось у нас значение eax с готовым адресом GetProcAddress? Давай посмотрим назад. Мы видим, что eax(адрес GetProcAddress) записывется в виртуальный стек, а перед этим высчитывается с помощью инструкции add eax,ecx(0xF8B номер инструкции).Давайте посмотрим, что такое eax и что такое ecx,для этого я просмотрю трейс до этого момента и посмотрю что это за значения, прикреплю скрины ниже.

1713650278986.png

Угу, мы видим что значением в операнде EAX инструкции add eax,ecx является F13A0000,те кто реверсил семплы вмпротект знает ,что эта релокация ,которая находится в VmEntry-не будем тут останавливаться. Это просто то как вмпротект обфусцирует бинарники и его внутренние механизмы, объяснять долго и это не тема нашей статьи. Окей, откуда же берется регистр ecx инструкции add eax,ecx?Еще раз смотрим скрин ниже.
1713650306343.png


Я закрыл случайно значение ecx стрелочкой, но мы видим что оно равняется 0x8415F7F0.Именно это и есть оффсет к нашему импорту и именно его должен будет запатчить реверсер в своем лоадере, чтобы сделать рабочий дамп и зафиксить таким образом импорты. Посмотрим откуда же он берется?


1713650321664.png



Мы видим что он записываться в регистр [ebp]-это вирутальный стек.Но перед этим он расшифровывается, а изначальное значение берется из инструкции mov ecx,dword ptr ds:[esi].На что же указывает наше esi?Те кто реверсил вмпротект и знаком с его архитектурой тот уже понял о чем речь.esi – в данном случае это указатель на внутренний массив вмпротект ,где хранятся зашифрованные константы, операнды, адреса следующих хендлеров и в том числе наши ИМПОРТЫ. Реверсеру придется, чтобы сделать рабочий софт с зафикшеными импортами, патчить контекст вмки Vmprotect.Во первых он не сможет вписать в esi сразу адрес импорта в нашем случае GetProcAddress, поскольку он дальше будет расшифровываться и получиться мусор,а не адрес импорта и нас крашнет. Реверсер должен сделать обратные операции, которые делались при расшифровке. Зашифровать импорт и вписать его по адресу esi, который этот регистр имел на момент выполнения инструкции mov ecx,dword ptr[esi](инструкция 0x0f20).Но даже не это самое сложное. Проблема в другом больше.Как реверсер узнает где хранятся импорты, если они зашифрованы, и что ему нужно патчить? Паттерн сканнинг сделать не получится поскольку это все под Vmprotect.IAT фиксить смысла нету: он у нас не используется. Ему придется возиться с Vmprotect и искать все вызовы импортов,что вручную сделать нереально и дело может прийти к тому,что ему нужно будет писать деобфускатор для такого мощного протектора как Vmprotect только для того чтобы пофиксить импорты?Да именно так.Этот способ имеет очевидные преимущества над предыдущими. Человек который с этим столкнется будет вынужден возиться с вмпротектом, что очень-очень трудоемкий процесс. А не как обычно это бывает что протектор кладут криво или не вшивают критические проверки в него(например проверки на хвид,время подписки,антидебаг который абузит баги ТитанХайда ScyllaHide) и крекер просто забивает на этот протектор болт и крекает софт.Толку от такого подхода нету, лучше уже вообще не использовать протектор: инициализация софта и авторизация пройдет быстрее. Ладно перейдем к практике.

Алгоритм действий


Сейчас я напишу алгоритм действий, код предоставленный ниже будет написан на языке c++ и будет использоваться не как рабочий код, который можно спастить, а просто для того чтобы гайд не состоял из одной теории без конкретных участков кода.



P.S.некоторые моменты очень спорны в реализации: на них я буду акцентировать внимание и говорить о возможных проблемах (но проект и не задумывался для общего использования повторюсь, а просто Proof of Concept).

1.Генерируем ProjectFile Vmprotect


Поскольку мы будем использовать консольку Vmprotect почитаем немножко о ней на сайте Vmprotect.
1713652688629.png


Мы видим, что тут написаны параметры, но нас сейчас интересует аргумент -pf(Project file).Мы должны будем его создать сами. Для этого заходим в GUI Vmprotect.(для этого гайда я буду использовать версию 3.5.0(лицензированную)). Выбираем нашу длл и выбираем функции, которые хотим запротектить. В моем случае я выбрал функцию Start ,а дллка которую мы будем маппить имеет имя ToInject.dll.
1713650474072.png


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


Генерируем ProjectFIle. Для этого заходим сюда

1713650523050.png


Выбираем сохранить проект как. И сохраняем в следующем формате
1713650540503.png

Все ProjectFile сгенерировали. Поехали к самому мапперу.

2.Делаем send с сервера на клиент с информацией о требуемых импортов.

Отправляем запрос на клиент с именами функций, модулей ,которые нужно замапить и получаем их адреса. Наброски кода снизу.
C++:
//Server.cpp

std::string get_info_imports()
{
    int index = 0;
    nlohmann::json json;
    for (auto& [mod, imports] : m_imports) {
        for (auto& i : imports) {
            json[mod].emplace_back(i.name);
            printf("found import:%s\n --- %d", i.name.data(), index);
             index++;
        }
    }
    return json.dump();
}
void FullInfoOnImports(std::vector<BYTE> pe)
{
    uintptr_t base = (uintptr_t)pe.data();

    _IMAGE_NT_HEADERS* pOldNtHeader = reinterpret_cast<IMAGE_NT_HEADERS*>(base + reinterpret_cast<IMAGE_DOS_HEADER*>(base)->e_lfanew);
    IMAGE_OPTIONAL_HEADER* pOpt = &reinterpret_cast<IMAGE_NT_HEADERS*>(base + reinterpret_cast<IMAGE_DOS_HEADER*>(base)->e_lfanew)->OptionalHeader;
    if (pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size)
    {
        auto* pImportDescr = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(base + pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
        while (pImportDescr->Name)
        {
            uintptr_t szMod = base + pImportDescr->Name;
    
            std::string mod_name = (char*)szMod;
            ULONG_PTR* pThunkRef = reinterpret_cast<ULONG_PTR*>(base + pImportDescr->OriginalFirstThunk);
            ULONG_PTR* pFuncRef = reinterpret_cast<ULONG_PTR*>(base + pImportDescr->FirstThunk);

            if (!pThunkRef)
                pThunkRef = pFuncRef;

            for (; *pThunkRef; ++pThunkRef, ++pFuncRef)
            {
                m_import import_;
                import_.rva = (DWORD)pFuncRef - base;
                if (IMAGE_SNAP_BY_ORDINAL(*pThunkRef))
                {
                    printf("found import by ordinal..\n");
                    continue;
                }
                else
                {
                    auto* pImport = reinterpret_cast<IMAGE_IMPORT_BY_NAME*>(base + (*pThunkRef));
                    import_.name = pImport->Name;
                }
                m_imports[mod_name].emplace_back(import_);
            }
    
            ++pImportDescr;
        }
    }
}
std::vector<BYTE>file_raw;
 std::vector<BYTE>file_virtual;
ReadFile("D:\\Server\\Debug\\ToInject.dll", file_raw);

MapFile(file_raw, file_virtual);
FullInfoOnImports(file_virtual);
std::string imports_info = get_info_imports();

write(acceptSocket, packet_t(imports_info, packet_type::write, "", imports));

3.Делаем send с клиента на сервер с адресами импортов

C++:
//Client.cpp
nlohmann::json final_imports;
 for (auto& [key, value] : j.items()) {

    for (auto& i : value) {
        auto name = i.get<std::string>();
        DWORD addr = 0;
            
        addr = FindImport(base, name);
              
        if (!addr)
        {
            printf("Cann not find import\n");
            exit(0);
        }
        if (IsForwardExport(base,addr))
        {
            addr = HandleForwardExport(addr);

        }

        final_imports[name] = addr;
    }
}
write(clientSocket, packet_t(final_imports.dump(), packet_type::write,
     "", imports));

4.Делаем некоторую работу на сервере


Первое,что мы делаем это заполняем IAT адресами импортов, полученных от клиента

C++:
void WriteImportsData(const std::string& msg,std::vector<BYTE>&data)
{
    auto j = nlohmann::json::parse(msg);


    for (auto& [mod, funcs] : m_imports) {
        for (auto& func : funcs) {
            if (!j.contains(func.name)) {
                printf("missing %s import address.", func.name);
                continue;
            }

            uint32_t addr = j[func.name];
            printf("func name %s --- 0x%X\n", func.name.c_str(), addr);

            *reinterpret_cast<uint32_t*>(data.data() + func.rva) = addr;
        }
    }
}
    packet_t packet_imp = read(acceptSocket);

if (!nlohmann::json::accept(packet_imp.message)) {
     printf("Invalid json format.\n");
     exit(0);
 }
 

 WriteImportsData(packet_imp.message, file_virtual);

Далее идет самый спорный момент, если кто-то знает как это обойти- скажите. Дело в том что мы не знаем размер бинарника, который получится после вмпротекта заранее ,поэтому мы будем протектить бинарь на сервере 2 раза.Первый раз чтобы узнать примерный размер бинарника после протекта и второй раз после будем протектить уже саму дллку, которую будем мапить.

C++:
//Server.cpp

void VmprotectBin()
{
    PROCESS_INFORMATION ProcessInfo; //This is what we get as an [out] parameter

    STARTUPINFOA StartupInfo; //This is an [in] parameter

    memset(&StartupInfo, 0, sizeof(StartupInfo));
    StartupInfo.cb = sizeof StartupInfo; //Only compulsory field

    const char* cmdArgs = "VMProtect_Con.exe D:\\Server\\Debug\\ToInject.dll";
 
    if (CreateProcessA("D:\\VMProtectUltimate3.5.0\\VMProtect_Con.exe",
        (char*)cmdArgs,
        NULL, NULL, FALSE, 0, NULL,
        NULL, &StartupInfo, &ProcessInfo))
    {
        WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
        CloseHandle(ProcessInfo.hThread);
        CloseHandle(ProcessInfo.hProcess);

        printf("Yohoo!");
    }
    else
    {
        printf("The process could not be started...");
    }

}
DWORD check_required_size()
{
    VmprotectBin();
    std::vector<BYTE>data;
    ReadFile("D:\\Server\\Debug\\ToInject.vmp.dll", data);

    IMAGE_OPTIONAL_HEADER* pOldOptHeader = nullptr;
    IMAGE_FILE_HEADER* pOldFileHeader = nullptr;
    IMAGE_NT_HEADERS* pOldNtHeader = reinterpret_cast<IMAGE_NT_HEADERS*>(data.data() + reinterpret_cast<IMAGE_DOS_HEADER*>(data.data())->e_lfanew);


    pOldOptHeader = &pOldNtHeader->OptionalHeader;
    pOldFileHeader = &pOldNtHeader->FileHeader;

    DWORD image_size = pOldOptHeader->SizeOfImage;
    image_size = image_size * 1.5;
    std::remove("D:\\Server\\Debug\\ToInject.vmp.dll");
    return image_size;
}
nlohmann::json alloc1;
uintptr_t img_size = check_required_size();
alloc1["image_size"] = img_size;
write(acceptSocket, packet_t(alloc1.dump(), packet_type::write, "", alloc));


Тут нужно чу-чуть разъяснить что происходит. Дело в том,что мы должны отправить на клиент размер нашего файла ,чтобы лоадер смог сделать VirtuaAllocEx на нужное количество памяти. Но как мы будем знать размер запротекченного файла заранее ? Я не нашел ответ на этот вопрос, поэтому выбрал следующий подход. Сначала я создаю процесс Vmprotect_Con.exe(консолька вмпротекта),и передаю ему аргументы командной строки.


C++:
const char* cmdArgs = "VMProtect_Con.exe D:\\Server\\Debug\\ToInject.dll";
 
    if (CreateProcessA("D:\\VMProtectUltimate3.5.0\\VMProtect_Con.exe",
        (char*)cmdArgs,
        NULL, NULL, FALSE, 0, NULL,
        NULL, &StartupInfo, &ProcessInfo))
    {
        WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
        CloseHandle(ProcessInfo.hThread);
        CloseHandle(ProcessInfo.hProcess);

        printf("Yohoo!");
    }

Как вы видете cmdArgs содержит только параметр inputfile,который надо запротектить(D:\Server\Debug\ToInject.dll).Имя OutputFile я не указываю, поэтому вмпротект выберет имя, указанное в нашем ProjectFile, параметр -pf я так же не указываю, потому что проектный файл vmp хранится у меня в той же директории что и мой exe, и он будет его искать там же. Но если вы хотите указать имя выходного файла и директории в который искать Projectfile, это будет выглядеть примерно следующим образом.

C++:
const char* cmdArgs = "VMProtect_Con.exe InputFile OutputFile -pf ProjectFile";

После чего я читаю PE header запротекченного бинаря и смотрю его новый ImageSize, после чего умножаю ImageSize * 1.5,потому что размер файла может разниться от протекту к проекту и лучше выделить больше чем меньше. После чего отправляю size который должен выделить лоадер в нашем процессе с сервера на клиент.

C++:
//check_required_size
DWORD image_size = pOldOptHeader->SizeOfImage;
    image_size = image_size * 1.5;
    std::remove("D:\\Server\\Debug\\ToInject.vmp.dll");
    return image_size;
}
nlohmann::json alloc1;
uintptr_t img_size = check_required_size();
alloc1["image_size"] = img_size;
write(acceptSocket, packet_t(alloc1.dump(), packet_type::write, "", alloc));

5.Выделяем адрес в процессе и возвращаем его серверу, чтобы он мог пофиксить релокации.

C++:
//Client.cpp
packet = read(clientSocket);
auto jsdata = nlohmann::json::parse(packet.message);
DWORD size_image = jsdata["image_size"];
 MemoryToWrite = VirtualAllocEx(pHandle, 0, size_image, MEM_RESERVE | MEM_COMMIT,
     PAGE_EXECUTE_READWRITE);
        jsdata["base"] = (uintptr_t)MemoryToWrite;

        write(clientSocket, packet_t(jsdata.dump(), packet_type::write, "", alloc));

6.Делаем готовый бинарь и отправляем его клиенту.


Фиксим релокации. Код FixRelocs() вставлять не буду так как это довольно тривиально.

C++:
//Server.cpp

packet = read(acceptSocket);
nlo DWORD base = anjs["base"];
 FixRelocs(base,(DWORD)file_virtual.data());hmann::json anjs = nlohmann::json::parse(packet.message);
MakeImportsInline(file_virtual,base);
  FixRelocsAfterMap(base, (DWORD)vmprotected_bin_virt.data());
  WriteImportsData(packet_imp.message, vmprotected_bin_virt);
   anjs.clear();
   DWORD entry_offset = GetEntryPoint((DWORD)file_virtual.data());
   anjs["entry"] = base + entry_offset;

 
   write(acceptSocket, packet_t(anjs.dump(), packet_type::write, "", inject));
   write(acceptSocket, vmprotected_bin_virt);

Дальше мы разберем функцию MakeImportsInline (не лучшее название знаю), так как она сделает всю не очевидную и нетривиальную работу.

C++:
//Server.cpp
void MakeImportsInline(std::vector<BYTE>& base_, DWORD new_reloced_base)
{
    char filename[MAX_PATH] = "D:\\Server\\Debug\\ToInject.dll";
    char newLocation[] = "D:\\Server\\Debug\\ToInjectcp.dll";

    CopyFileA(filename, newLocation, false);

    std::vector<BYTE>raw_bytes;
    ReadFile("D:\\Server\\Debug\\ToInject.dll", raw_bytes);
 
    IMAGE_NT_HEADERS* pOldNtHeader = nullptr;
    IMAGE_OPTIONAL_HEADER* pOldOptHeader = nullptr;
    IMAGE_FILE_HEADER* pOldFileHeader = nullptr;
    uintptr_t base = (uintptr_t)base_.data();

    pOldNtHeader = reinterpret_cast<IMAGE_NT_HEADERS*>(base + reinterpret_cast<IMAGE_DOS_HEADER*>(base)->e_lfanew);
    pOldOptHeader = &pOldNtHeader->OptionalHeader;
    pOldFileHeader = &pOldNtHeader->FileHeader;

    DWORD locationDelta = base - pOldNtHeader->OptionalHeader.ImageBase;
    auto* pSectionHeader = IMAGE_FIRST_SECTION(pOldNtHeader);
    for (UINT i = 0; i != pOldFileHeader->NumberOfSections; ++i, ++pSectionHeader)
    {
        if (!_stricmp(".text", (const char*)pSectionHeader->Name))
            break;
    }
    DWORD address;
    DWORD start = base + pSectionHeader->VirtualAddress;
    DWORD size = pSectionHeader->Misc.VirtualSize;
    DWORD end = size + start;
 
    do
    {
        address = FindPattern(start, size, "\xff\x15\x00\x00\x00\x00", "xx????");
        if (address == -1)
            break;
        DWORD offset = address - base ;
        DWORD IATAddress = *(DWORD*)(address + 2);
        donot_patch.push_back(offset + 2);
        IATAddress  = IATAddress - new_reloced_base + pOldNtHeader->OptionalHeader.ImageBase + locationDelta;
        DWORD FuncAddress = *(DWORD*)(IATAddress );
        DWORD src = (offset + new_reloced_base);
        DWORD rva = FuncAddress - src -5;
        DWORD raw_offset = RvaToRaw(base, offset);
           DWORD r_address = (uintptr_t)(raw_bytes.data() + raw_offset);
        *(BYTE*)r_address = 0xe8;
        *(DWORD*)(r_address + 1) = rva;
        *(BYTE*)(r_address + 5) = 0x90;
        size -=( address - start) + 6;
        printf("Write at offset:0x%X\n", raw_offset);
 
        start = address + 6;
    } while (address != -1);
    bool res = DeleteFileA("D:\\Server\\Debug\\ToInject.dll");
    if (!res)
    {
        int err = GetLastError();
        printf("\n");
    }
    std::ofstream fs("D:\\Server\\Debug\\ToInject.dll",std::ios::binary);

    if (!fs.is_open())
    {
        printf("Wtf?\n");
    }
 
    fs.write((char*)raw_bytes.data(), raw_bytes.size());

    VmprotectBin();
    std::vector<BYTE>vmprotected_bin;
    ReadFile("D:\\Server\\Debug\\ToInject.vmp.dll",vmprotected_bin);

    MapFile(vmprotected_bin, vmprotected_bin_virt);
DeleteFile(filename);
Rename(newLocation,filename);

 
}

Сначала я сохраняю оригинальный файл, чтобы его не потерять и даю ему имя ToInjectcp.dll.

C++:
char filename[MAX_PATH] = "D:\\Server\\Debug\\ToInject.dll";
    char newLocation[] = "D:\\Server\\Debug\\ToInjectcp.dll";

    CopyFileA(filename, newLocation, false);

Дальше я читаю наш файл ToInject.dll и ищу секцию текст где буду делать pattern scanning.

C++:
ReadFile("D:\\Server\\Debug\\ToInject.dll", raw_bytes);
 
IMAGE_NT_HEADERS* pOldNtHeader = nullptr;
IMAGE_OPTIONAL_HEADER* pOldOptHeader = nullptr;
IMAGE_FILE_HEADER* pOldFileHeader = nullptr;
uintptr_t base = (uintptr_t)base_.data();

pOldNtHeader = reinterpret_cast<IMAGE_NT_HEADERS*>(base + reinterpret_cast<IMAGE_DOS_HEADER*>(base)->e_lfanew);
pOldOptHeader = &pOldNtHeader->OptionalHeader;
pOldFileHeader = &pOldNtHeader->FileHeader;

DWORD locationDelta = base - pOldNtHeader->OptionalHeader.ImageBase;
auto* pSectionHeader = IMAGE_FIRST_SECTION(pOldNtHeader);
for (UINT i = 0; i != pOldFileHeader->NumberOfSections; ++i, ++pSectionHeader)
{
    if (!_stricmp(".text", (const char*)pSectionHeader->Name))
        break;
}
address = FindPattern(start, size, "\xff\x15\x00\x00\x00\x00", "xx????");

Тут мы ищем эти самые инструкции

1713651358873.png

И меняем их на
1713651380144.png


относительные прыжки перед этим высчитав относительный адрес к импорту, но делаем это в массиве, который содержит то как хранится наш файл на диске.
C++:
std::vector<BYTE>raw_bytes;
ReadFile("D:\\Server\\Debug\\ToInject.dll", raw_bytes)
//По этой причине  мы переводим RvaToRaw(rva to file offset)
DWORD raw_offset = RvaToRaw(base, offset);
   DWORD r_address = (uintptr_t)(raw_bytes.data() + raw_offset);
*(BYTE*)r_address = 0xe8;
*(DWORD*)(r_address + 1) = rva;
*(BYTE*)(r_address + 5) = 0x90;
size -=( address - start) + 6;

Так же добавляем оффсет к релокации, которые мы не будем патчить в будущем: в запротекчином бинарнике, когда будем фиксить релокации по второму кругу.(Это адреса наших инструкций call relative).
C++:
donot_patch.push_back(offset + 2);

После чего удаляем старый файл и записываем на диск новый с зафикшеными импортами.(Очень важно дать файлу точно такое же имя которое было при компиляции нашей дллке, иначе мы не сможем запротектить,поскольку сломается pdb файл на который ссылается Vmprotect)

C++:
bool res = DeleteFileA("D:\\Server\\Debug\\ToInject.dll");
if (!res)
{
    int err = GetLastError();
    printf("\n");
}
std::ofstream fs("D:\\Server\\Debug\\ToInject.dll",std::ios::binary);

if (!fs.is_open())
{
    printf("Wtf?\n");
}
 
fs.write((char*)raw_bytes.data(), raw_bytes.size());

Протектим этот файл(смотрите вышу реализацию Vmprotect.bin()).Читаем его, заново мапим, фиксим импорты и релокации.(Да иат будет заполнен, но мы его не будем использовать, так что это не страшно)

C++:
void FixRelocsAfterMap(DWORD new_base, DWORD base)
{
    int count = 0;
    auto* pOpt = &reinterpret_cast<IMAGE_NT_HEADERS*>(base + reinterpret_cast<IMAGE_DOS_HEADER*>(base)->e_lfanew)->OptionalHeader;

    BYTE* LocationDelta = (BYTE*)(new_base - pOpt->ImageBase);

    if (LocationDelta)
    {
        if (!pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size)
            return;

        auto* pRelocData = reinterpret_cast<IMAGE_BASE_RELOCATION*>(base + pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
        while (pRelocData->VirtualAddress)
        {
            UINT AmountOfEntries = (pRelocData->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
            WORD* pRelativeInfo = reinterpret_cast<WORD*>(pRelocData + 1);
            for (UINT i = 0; i != AmountOfEntries; ++i, ++pRelativeInfo)
            {
                if (RELOC_FLAG(*pRelativeInfo))
                {
                    UINT_PTR* pPatch = reinterpret_cast<UINT_PTR*>(base + pRelocData->VirtualAddress + ((*pRelativeInfo) & 0xFFF));
                    DWORD offset = (DWORD)pPatch - base;
                    auto it = std::find(donot_patch.begin(), donot_patch.end(), (DWORD)offset);//relocs that refer to our relative call skip or will invalid our calls
            
                    if(it == donot_patch.end()){
                    *pPatch += reinterpret_cast<UINT_PTR>(LocationDelta);
                    count++;
                    }
            
                }
            }
            pRelocData = reinterpret_cast<IMAGE_BASE_RELOCATION*>(reinterpret_cast<BYTE*>(pRelocData) + pRelocData->SizeOfBlock);


        }
    }
    printf("Relocs count fix relocs:0x%X", count);
}
VmprotectBin();
std::vector<BYTE>vmprotected_bin;
ReadFile("D:\\Server\\Debug\\ToInject.vmp.dll",vmprotected_bin);

MapFile(vmprotected_bin, vmprotected_bin_virt);

FixRelocsAfterMap(base, (DWORD)vmprotected_bin_virt.data());
 WriteImportsData(packet_imp.message, vmprotected_bin_virt);

После чего мы передаем address of entry point и нашу длл клиенту(аддресс ентри поинта можно затереть, но делать этого я не буду-не имеет значения для нашего гайда).

C++:
//Server.cpp

nlohmann::json anjs;
anjs["entry"] = base + entry_offset;

 
write(acceptSocket, packet_t(anjs.dump(), packet_type::write, "", inject));
write(acceptSocket, vmprotected_bin_virt);

7.Инжектим


C++:
//Client.cpp

packet = read(clientSocket);
   auto jsdata =  nlohmann::json::parse(packet.message);
DWORD entry = jsdata["entry"];
 std::vector<BYTE >img;
 read(clientSocket, img);

 bool res = WriteProcessMemory(pHandle, MemoryToWrite, img.data(),
     img.size(), NULL);


 static std::vector<uint8_t> shellcode = { 0x55, 0x89, 0xE5, 0x6A, 0x00, 0x6A, 0x01, 0x68, 0xEF, 0xBE,
     0xAD, 0xDE, 0xB8, 0xEF, 0xBE, 0xAD, 0xDE, 0xFF, 0xD0, 0x89, 0xEC, 0x5D, 0xC3 };
 void* shellEntry = VirtualAllocEx(pHandle, 0, shellcode.size(), MEM_RESERVE | MEM_COMMIT,
     PAGE_EXECUTE_READWRITE);
 *reinterpret_cast<uint32_t*>(&shellcode[8]) = (DWORD)MemoryToWrite;
 *reinterpret_cast<uint32_t*>(&shellcode[13]) = entry;
  res = WriteProcessMemory(pHandle, shellEntry, shellcode.data(),
      shellcode.size(), NULL);
 CreateRemoteThread(pHandle, 0, 0, (LPTHREAD_START_ROUTINE)shellEntry, 0, 0, 0);


Конец​


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

Вложения

  • 1713650440208.png
    1713650440208.png
    34.9 KB · Просмотры: 26
Последнее редактирование: