• XSS.stack #1 – первый литературный журнал от юзеров форума

Обучаюсь плюсам

Вот что я понял про SEH в x64: в ntdll есть экспортируемая запись RtlLookupFunctionEntry. Я во время реверса KiUserExceptionDispatcher я сначала понял только то что RtlLookupFunctionEntry первым элементом содержит количество записей в массиве структур, который начинаются с адреса RtlLookupFunctionEntry+0x10. Структуры эти содержат imagebase модуля, размер модуля и адрес exception table.
Ниже скриншот из функции, которую IDA называет RtlpxLookupFunctionTable.
1697383745895.png

В этом контексте eax счётчик и тут начинается то что мне не понятно, eax делится на два в rdx записывается eax умноженное на 3, и достаётся запись по этому адресу. R8 - ImageBase из структуры, R14 адрес где произошло исключение, после чего к R8 прибавляется SizeOfImage и сравнивается с R14. Я нашёл хороший ресёрч в котором приведены определения структур которые мне так нужны https://lhc645.wordpress.com/2010/04/13/обработка-исключений-в-x64-usermode/ Я написал небольшой код, который выводит информацию о записях в KiUserInvertedFunctionTable.
C++:
#include <iostream>
#include <Windows.h>

int filter(unsigned int code, struct _EXCEPTION_POINTERS* ep)
{
    if (code == EXCEPTION_ACCESS_VIOLATION)
    {
        std::cout << "Memory access violation occurred!" << std::endl;
        return EXCEPTION_EXECUTE_HANDLER;
    }

    // If it's not an access violation exception, don't handle it.
    return EXCEPTION_CONTINUE_SEARCH;
}



struct InvertedEntry
{
    void* ExceptionTable;
    HMODULE imagebase;
    DWORD sizeofimage;
};

struct _RTL_INVERTED_FUNCTION_TABLE_ENTRY
{
    PIMAGE_RUNTIME_FUNCTION_ENTRY ExceptionDirectory; // виртуальный адрес .pdata (обычно)
    PVOID ImageBase; // базовый адрес модуля
    ULONG ImageSize; // размер образа
    ULONG ExceptionDirectorySize; // размер .pdata
};

struct _RTL_INVERTED_FUNCTION_TABLE
{
    ULONG Count; // число структур RTL_INVERTED_FUNCTION_TABLE_ENTRY
    ULONG MaxCount; // 0xA0 — win xp x64
    ULONG Pad[0x2];
    _RTL_INVERTED_FUNCTION_TABLE_ENTRY Entries[ANYSIZE_ARRAY];
};

int main()
{
    GetProcAddress(GetModuleHandleA("ntdll"), "RtlLookupFunctionEntry");
  
    _RTL_INVERTED_FUNCTION_TABLE* table = (_RTL_INVERTED_FUNCTION_TABLE*)GetProcAddress(GetModuleHandleA("ntdll"), "KiUserInvertedFunctionTable");
    DWORD numberofentries = table->Count;
    std::cout << "this process imagebase:" << GetModuleHandleA(0) << std::endl;
    while (numberofentries)
    {
        numberofentries--;
        std::cout << "ImageBase:" << table->Entries[numberofentries].ImageBase << std::endl;
        std::cout << "Image size:" << table->Entries[numberofentries].ImageSize << std::endl;
        std::cout << "ExceptionDirectory:" << table->Entries[numberofentries].ExceptionDirectory << std::endl;
        std::cout << "Size of exception directory:" << table->Entries[numberofentries].ExceptionDirectorySize << std::endl;
      
    }

    __try
    {
        int* ptr = nullptr;
        *ptr = 42; // This will cause an access violation exception
    }
    __except (filter(GetExceptionCode(), GetExceptionInformation()))
    {
        std::cout << "Exception caught." << std::endl;
    }

}
В своём лоадере я попробовал добавить запись о загружаемом образе, увеличил число структур в массиве на 1, это не дало результата, RtlpxLookupFunctionTable тупо проходит мимо большинста записей в массиве, я попробовал следующий код:
C++:
void Loader::PELoader::ReplaceFunctionTableEntry(HMODULE module, void* exception_section, DWORD sizeofdirectory)
{
    _RTL_INVERTED_FUNCTION_TABLE* table = (_RTL_INVERTED_FUNCTION_TABLE*)WINRTL::HashGetProcAddress(obfs::CONStrHashA("KiUserInvertedFunctionTable"), WINRTL::GetNtdll());
    ULONG entries_count = table->Count;
    HMODULE thisimagebase = WINCALL(winimport, DLLID_KERNEL32, GetModuleHandleA)(0);            //imagebase of current process
    while (entries_count)
    {
        entries_count--;
        if (table->Entries[entries_count].ImageBase == thisimagebase)
        {
            _RTL_INVERTED_FUNCTION_TABLE_ENTRY* insert = (_RTL_INVERTED_FUNCTION_TABLE_ENTRY*)&table->Entries[entries_count];

            DWORD oldprt;
            WINCALL(winimport, DLLID_KERNEL32, VirtualProtect)(table, 0x1000, PAGE_READWRITE, &oldprt);

            insert->ExceptionDirectory = (PIMAGE_RUNTIME_FUNCTION_ENTRY)exception_section;
            insert->ExceptionDirectorySize = sizeofdirectory;
            insert->ImageBase = module;
            insert->ImageSize = optheader->SizeOfImage;
            break;
        }
    }
}
И всё заработало. Но всё же хочу разобраться почему так происходит? В KiUserInvertedFunctionTable у меня показывает 0x12 - число записей в массиве, но цикл в RtlpxLookupFunctionTable сделал всего 4 итерации, вопрос: почему RtlpxLookupFunctionTable не проходится по всем структурам?
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Кстати, если говорить о косвенных сисколлах, то как минимум обычный Hollowing запустить не выйдет.
Возможно это как то связано с DEP, CFG или еще чем то подобным, но сейчас я пишу холловинг только лишь с использованием косвенных системных вызовов и NtAllocateVirtualMemory и NtWrite… крашат мой процесс, а в процессе где я делаю холловинг выделенная страница памяти заполнена нулями…
Я смог только запустить процесс, получить контекст, PEB, и базовый адрес, используя косвенные сисколлы, но когда я пытаюсь туда что то записать и тем более изменить точку входа, мой процесс крашится, хотя сисколлы отрабатывают со статусом 0x00. При смене сисколлов на юзермод апишки все работает!
Причем тут DEP и CFG? У меня все норм работает, есть свой холловинг на сисколлах. Может, у тебя просто нет прав на запись? Высокоуровневая WriteProcessMemory их выставляет (там под капотом вызов VirtualProtect)
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Слышал что в образах с этими защитами нужно выделять память только с юзермод API.. Но процесс крашится еще на этапе с NtAllocateVirtualMemory
(Выделяю с PAGE_READWRITE, и перед возобновлением NtResumeProcess меняю на PAGE_EXECUTE_READ)
Может нам в ЛС будет удобнее?
Я не веду консультаций в ЛС, поэтому спрашивай тут. Без разницы как выделять память и как её записывать, всё должно работать и оно у меня работает. Я могу чисто наванговать, что в твоём случае проблема в правах у того участка памяти куда ты пишешь

А у тебя он на прямых? У меня то на косвенных
Что такое косвенные сисколлы? У меня на обычных, инструкция syscall
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Косвенные сисколлы - jmp, вместо syscall
CFG как раз таки и есть защита от косвенных вызовов, но даже при запуске процесса без него - я не могу выделить память
С теми же параметрами VirtualAllocEx без проблем выделяет

Причем CFG к сисколлам? Это технология которая не относится к этой теме вообще. Глянь в отладчике, почему твоя прога падает, посмотри что лежит в регистрах и т.д
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Это про защиту от эксплойтов, ты не о том думаешь вообще. Короче кинь семпл под хайд, я продебажу и скажу в чем проблема
 
Я решил проблему с использованием unique_ptr, получилось собрать хелоу ворлд, при этом выделив память для класса и в импорте пусто.
C++:
#include <Windows.h>
#include <memory>
#include "WINAPI.h"
//#include "Loader.h"
#include "DYNMEM.h"
//#include "syscall.h"                //for test ( delete this)




class TestMem : public DynMem::WinDynMem
{
public:
    int age;
    LPSTR text;
};


int main()
{
    DynMem::MemoryResource res;
    DynMem::WinDynMem mem;
    mem.InitRes(&res);      //init memory functions and hHeap


    std::unique_ptr<WINIMPORT::WINIMPORT> winImportPtr(new(&res) WINIMPORT::WINIMPORT);     //init import
    WINIMPORT::WINIMPORT* winimport = winImportPtr.get();

    std::unique_ptr<TestMem> testPtr(new(&res) TestMem);
    TestMem* test = testPtr.get();
    test->text = "Hello from shellcode!!!";

    WINCALL(winimport, DLLID_USER32, MessageBoxA)(0, test->text, test->text, 0);
    return 0;
}

Решить проблему получилось путём создания класса:
C++:
#include "DYNMEM.h"
#include "WINRTL.h"
#include "NT_STRUCTS.h"
#include "OBFS.h"




    void DynMem::WinDynMem::operator delete(void* ptr)
    {
        WinDynMem* memObj = static_cast<WinDynMem*>(ptr);
        memObj->res->free_func(memObj->res->hHeap, 0, ptr);
    }

    void* DynMem::WinDynMem::operator new(size_t size, MemoryResource* res_)
    {
        if (size)
        {
            void* ptr = res_->alloc_func(res_->hHeap, 0, size);
            WinDynMem* memObj = static_cast<WinDynMem*>(ptr);
            memObj->res = res_;
            return ptr;
        }
        return nullptr;
    }

    void DynMem::WinDynMem::operator delete[](void* ptr)
    {
        WinDynMem* memObj = static_cast<WinDynMem*>(ptr);
        memObj->res->free_func(memObj->res->hHeap, 0, ptr);
    }

    void* DynMem::WinDynMem::operator new[](size_t size, MemoryResource* res_)
    {
        if (size)
        {
            void* ptr = res_->alloc_func(res_->hHeap, 0, size);
            WinDynMem* memObj = static_cast<WinDynMem*>(ptr);
            memObj->res = res_;
            return ptr;
        }
        return nullptr;
    }

    void DynMem::WinDynMem::unalloc(void* ptr)
    {
        this->res->free_func(this->res->hHeap, 0, ptr);
    }

    void* DynMem::WinDynMem::alloc(size_t size)
    {
        if (size)
        {
            return this->res->alloc_func(this->res->hHeap, 0, size);
        }
        return 0;
    }

 
    void DynMem::WinDynMem::InitRes(MemoryResource* res_)
    {
        HMODULE ntdll = WINRTL::GetNtdll();
        res_->alloc_func = (decltype(&RtlAllocateHeap))(WINRTL::HashGetProcAddress(obfs::CONStrHashA("RtlAllocateHeap"), ntdll));
        res_->free_func = (decltype(&RtlFreeHeap))(WINRTL::HashGetProcAddress(obfs::CONStrHashA("RtlFreeHeap"), ntdll));
        res_->hHeap = ((decltype(&GetProcessHeap))(WINRTL::HashGetProcAddress(obfs::CONStrHashA("GetProcessHeap"), WINRTL::GetKernel32())))();
    }
Структура MemoryResource заполняется: адресом HeapAlloc, HeapFree, и хэндла кучи. При вызове new я передаю MemoryResource как входной параметр, оператор new вызывает переданную функцию для выделения памяти. Каждый класс который хочет выделяться/освобождаться таким образом должен наследоваться от класса WinDynMem, этот класс содержит переопределённые new и delete и содержит поле MemoryResource res*. Во время вызова delete принимается параметр WinDynMem* (т.е. на вход так же могут передаваться классы, которые наследовались от WinDynMem), delete вызывает this->unallocfunc. В итоговом exe-шнике у меня нет импорта

STL часто тащит за собой много лишнего.
unique_ptr тебе не нужен.

1. Управление памятью у тебя реализовано отдельно и код организован в стиле Си, это правильный подход, аллокатор правильнее реализовать в отдельной части кода и использовать на этапе инциализации ООП.
2. В коде появляется
DynMem::WinDynMem mem;
и
std::unique_ptr<WINIMPORT::WINIMPORT> winImportPtr(new(&res) WINIMPORT::WINIMPORT); //init import
не стоит так делать по той причине, что у тебя смешивается код низкого уровня и высокого, со всеми этими new и unique_ptr, и речь не про Си и С++, а про то как ты классы начинаешь привязывать к аллокатору уровня загрузки код winimport напрямую за пределами самого класса, лучше всю кухню DynMem поместить внутрь winimport, ибо он там больше нигде и не нужен.
3. Идем далее. Тебе не нужен класс работы с импортом используемым повсеместно. Одно из правил С++, если процедуру можно реализовать вне класса, зачастую это нужно реализовать вне класса. Сделай интерфйс, чтобы WINCALL работал с двумя аргументами, без уакзания таблицы импорта. Для удобства твоя таблица импорта может быть оформлена в виде класса, который существует как синлтон и разово создается через HeapAlloc,и единственный код который её использует это WINCALL, внутри где-то через процедуру CALL_WINAPI, чтобы у тебя была единственная точка входа. Если вот этот код выше начать повсеместно применять, у тебя весь проект будет видеть класс WINIMPORT, который по сути нигде кроме WINCALL не требуется, так зачем в открытую его применять как аргумент? Инкапсулируй это внутри WINCALL!
 
Последнее редактирование:
Всем привет! Сейчас думаю на тему того как можно инжектить код в процессы с наименьшим числом палевных вызовов. В итоге я пришёл вот к этому:
1. Определить базовый адрес и размер стэка любого потока, через VirtualQuery, например.
2. Выделить в своём процессе память такого же размера и заполнить его полностью "пустым гаджетом" просто тупо ret, и на самой верхушке стэка ROP chain, который в свою очередь может,например, выделить память и исполнить шеллкод.
3. Перезаписать стэк удалённого процесса.
Я уже написал простой POC, всё работает, процесс не падает, хотя ясное дело после токого поток уже не восстановить.
Так вот как по мне самое слабое место здесь это запись в процесс. Я знаю ещё один способ записи через рефлективный вызов GetAtomNameA через QueueUserAPC. Предлагаю подискутировать, какие интересные способы инжекта знаете вы и какие способы записи определённых данных по определённому адресу вы знаете.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх