Гайд Защищаем импорты как Gamesense

practice makes perfect
Пользователь
Статус
Оффлайн
Регистрация
16 Мар 2019
Сообщения
86
Реакции[?]
67
Поинты[?]
19K
ЭТА СТАТЬЯ НЕ МОЯ С СОСЕДНЕГО ФОРУМА ПОПРОСИЛИ ПЕРЕСЛАТЬ.

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



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

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


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

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

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



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

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

1713716762721.png

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

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

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


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


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

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

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

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

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

Пожалуйста, авторизуйтесь для просмотра ссылки.


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

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


Пожалуйста, авторизуйтесь для просмотра ссылки.


Мы видим что он записываться в регистр [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.
Пожалуйста, авторизуйтесь для просмотра ссылки.

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

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

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

Пожалуйста, авторизуйтесь для просмотра ссылки.

Выбираем сохранить проект как. И сохраняем в следующем формате
Пожалуйста, авторизуйтесь для просмотра ссылки.
Все 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????");
Тут мы ищем эти самые инструкции

Пожалуйста, авторизуйтесь для просмотра ссылки.
И меняем их на
Пожалуйста, авторизуйтесь для просмотра ссылки.


относительные прыжки перед этим высчитав относительный адрес к импорту, но делаем это в массиве, который содержит то как хранится наш файл на диске.
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);

Конец

Эта моя первая статья, если есть адекватные замечания пишите.Если будет спрос,могу переписать свой соурс на более-менее нормальный лад и выложить.
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
30 Авг 2023
Сообщения
15
Реакции[?]
2
Поинты[?]
3K
C++:
//Client.cpp

packet = read(clientSocket);
   auto jsdata =  nlohmann::json::parse(packet.message);
DWORD entry = jsdata["entry"];
std::vector<BYTE >img;
read(clientSocket, img);
Я конечно не expert, но это будет работать только в локальной сети, в ином случае у тебя сплит пуш пакетами произойдет. Так что, я бы использовал какую-нибудь нормальную либу для этого
 
practice makes perfect
Пользователь
Статус
Оффлайн
Регистрация
16 Мар 2019
Сообщения
86
Реакции[?]
67
Поинты[?]
19K
Я конечно не expert, но это будет работать только в локальной сети, в ином случае у тебя сплит пуш пакетами произойдет. Так что, я бы использовал какую-нибудь нормальную либу для этого
Да, будет. НО статья не моя, да и это proof of concept
 
На самом деле я Zodiak
Участник
Статус
Онлайн
Регистрация
22 Дек 2020
Сообщения
959
Реакции[?]
169
Поинты[?]
59K
WriteProcessMemory хукают и идем храпеть под заборчик с боярой
 
practice makes perfect
Пользователь
Статус
Оффлайн
Регистрация
16 Мар 2019
Сообщения
86
Реакции[?]
67
Поинты[?]
19K
WriteProcessMemory хукают и идем храпеть под заборчик с боярой
1713733261581.png
ладно, это может быть троллинг, НО В ИННОМ СЛУЧАЕ, АДМИН, НЕ БАНЬ.
Ты совсем балбес? При чем здесь WriteProcessMemory, гений?
 
c:\buildworker\csgo_rel_win64
Участник
Статус
Оффлайн
Регистрация
18 Окт 2022
Сообщения
587
Реакции[?]
209
Поинты[?]
137K
Это всё конечно очень интересно, но казалось бы, причём тут скит?
1714438511422.png
 
Пользователь
Статус
Оффлайн
Регистрация
17 Ноя 2021
Сообщения
231
Реакции[?]
33
Поинты[?]
4K
хорошая статья, но жсон как будто лишний - хукаешь json.dump() и у тебя финальные версии всех пакетов которые идут на сервер
 
Начинающий
Статус
Оффлайн
Регистрация
31 Май 2023
Сообщения
53
Реакции[?]
15
Поинты[?]
14K
Это всё конечно очень интересно, но казалось бы, причём тут скит?
Посмотреть вложение 275758
может потому что так в ските сделано, не думал?
хорошая статья, но жсон как будто лишний - хукаешь json.dump() и у тебя финальные версии всех пакетов которые идут на сервер
кидаешь под вирту и никто ничо не хукает
 
Пользователь
Статус
Оффлайн
Регистрация
17 Ноя 2021
Сообщения
231
Реакции[?]
33
Поинты[?]
4K
ЧВК EB_LAN
Эксперт
Статус
Оффлайн
Регистрация
26 Янв 2021
Сообщения
1,547
Реакции[?]
517
Поинты[?]
190K
один хуй у тебя стринг в памяти будет типа "version:" или еще что-то по которому найти можно будет
да и в целом, использовать жсон от нлохмана, как формат для сериализации - плохая идея
 
Начинающий
Статус
Оффлайн
Регистрация
31 Май 2023
Сообщения
53
Реакции[?]
15
Поинты[?]
14K
один хуй у тебя стринг в памяти будет типа "version:" или еще что-то по которому найти можно будет
нет
upd:
я еще че то затупил и тока ща догнал, что тебе расшифрованный запрос с списком импортов ничем не поможет
 
Последнее редактирование:
practice makes perfect
Пользователь
Статус
Оффлайн
Регистрация
16 Мар 2019
Сообщения
86
Реакции[?]
67
Поинты[?]
19K
Это всё конечно очень интересно, но казалось бы, причём тут скит?
Посмотреть вложение 275758
чекай как в ските сделано, в том же кряке. На ксго вот такая шляпа в ските была, как описана в данной статье.
один хуй у тебя стринг в памяти будет типа "version:" или еще что-то по которому найти можно будет
хорошая статья, но жсон как будто лишний - хукаешь json.dump() и у тебя финальные версии всех пакетов которые идут на сервер
хуки чего либо не помогут, т.к импорт будет под вмп и тебе надо будет девиртить это дело.
Вот тебе пример как вмп делает с обычными импортами с IAT(ниже мой псевдокод):
Код:
PUSH_DCONST IAT_ADDR
READ_DVSP
RET
Что в асм коде можно перевести как
Код:
push [IAT_ADDR]
ret
Как вмп делает с этими импортами, то что в статье:
Код:
PUSH_DCONST IMPORT_ADDR
RET
Что в асм коде можно перевести как
Код:
push IMPORT_ADDR
ret
То есть, в 1 случае тебе патчить вмку не надо, тебе надо просто заменить адрес, который лежит в IAT, а во втором случае(что описан в статье), тебе надо будет запатчить уже константу, которая хранится зашифрованной в байт коде вмки вмп. Старался максимально просто объяснить.
В своем софте, я бы мог даже не накладывать на лоадер или драйвер протектор, а просто использовать данную фичу в дллке софта. И кряка не было в 99% случаев, т.к если у человека нет девирта - gg

Тот код, что я предоставил, это то, что он выполняет под виртой.
 
Последнее редактирование:
EFI_COMPROMISED_DATA
лучший в мире
Статус
Оффлайн
Регистрация
26 Янв 2018
Сообщения
921
Реакции[?]
1,637
Поинты[?]
84K
т.к если у человека нет девирта - gg
зачем тут девирт? тут же просто коллы делаются из под вмки, чтобы их разобрать даже особо тулинга никакого не нужно :prison:
 
НЕКАСЕСТВЕНЫЙ КАД
Участник
Статус
Оффлайн
Регистрация
27 Фев 2019
Сообщения
1,415
Реакции[?]
247
Поинты[?]
4K
practice makes perfect
Пользователь
Статус
Оффлайн
Регистрация
16 Мар 2019
Сообщения
86
Реакции[?]
67
Поинты[?]
19K
зачем тут девирт? тут же просто коллы делаются из под вмки, чтобы их разобрать даже особо тулинга никакого не нужно :prison:
1714745715918.png
под пивко за 3 дня вообще реально разобрать? Ну, а если без шуток, то ты мб не понял тему. Почитай вот это, тут наглядно видно, что надо будет найти push_dconst хендлер вмки и патчить байт-код вм хендлер тейбла, чтобы изменить константу.
 
EFI_COMPROMISED_DATA
лучший в мире
Статус
Оффлайн
Регистрация
26 Янв 2018
Сообщения
921
Реакции[?]
1,637
Поинты[?]
84K
под пивко за 3 дня вообще реально разобрать?
зависит от пивка

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

алсо я мельком в треде видел, что ты вроде два раза репротектишь бинарник - один для того чтобы узнать размер, второй чтобы уже сам бинарник на выхлопе сделать. если не ошибаюсь в скриптинге вмп можно сид рандому задать и будет +- репродуцируемые билды (могу ошибаться, очень уж лень проверять)
 
practice makes perfect
Пользователь
Статус
Оффлайн
Регистрация
16 Мар 2019
Сообщения
86
Реакции[?]
67
Поинты[?]
19K
зависит от пивка


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

алсо я мельком в треде видел, что ты вроде два раза репротектишь бинарник - один для того чтобы узнать размер, второй чтобы уже сам бинарник на выхлопе сделать. если не ошибаюсь в скриптинге вмп можно сид рандому задать и будет +- репродуцируемые билды (могу ошибаться, очень уж лень проверять)
а как мне условно найти push_dconst хендлер без девирта? и что значит тамперить?
 
EFI_COMPROMISED_DATA
лучший в мире
Статус
Оффлайн
Регистрация
26 Янв 2018
Сообщения
921
Реакции[?]
1,637
Поинты[?]
84K
а как мне условно найти push_dconst хендлер без девирта? и что значит тамперить?
чтобы идентифицировать один вм хендлер тебе не нужен девиртуализатор.
они хоть и обфусцируются всяким говном, но идентифицировать один хендлер от другого все равно возможно при должном желании. ведь для того чтобы положить эту самую dword константу, для начала надо виртуальный стек подвинуть на 4, а затем туда эту самую константу положить.
 
Сверху Снизу