Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Undetected [Сурс] Windows — PhantomCall x64 Call Stack Spoofer и обфускатор указателей (C++20)

Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
126
Реакции
3
Выкатили свежий релиз с загашников — чел откопал на старой флешке PhantomCall. Это x64 call stack spoofer, который подойдет тем, кто только начинает ковырять байпасы и хочет понять, как античиты палят хуки через стек.

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

Что внутри:
  1. GS_SPOOF: Маскирует ваш вызов под легитимный стек (RtlUserThreadStart — BaseThreadInitThunk — ntdll), чтобы чекеры не видели, что вызов идет из левого модуля.
  2. GS_OBFUSCATE: XOR-шифрует указатели на функции с рандомным ключом при каждом запуске процесса.

Технические нюансы:
  1. Requirements: Windows 10/11 x64, C++20, MSVC + MASM.
  2. Limitations: Только 4 аргумента, никакой поддержки float/double, требует MSVC. Если включен CET/Shadow Stack — работать не будет.
main.cpp:
Expand Collapse Copy
#include <cstdio>
#include <Windows.h>
#include "phantomcall/phantomcall.h"
 
int  add(int a, int b) { return a + b; }
int  multiply(int a, int b) { return a * b; }
void log_message() { printf("called via spoofed stack\n"); }
 
int main() {
    auto obf_add      = GS_OBFUSCATE(add);
    auto obf_multiply = GS_OBFUSCATE(multiply);
    auto obf_log      = GS_OBFUSCATE(log_message);
 
    int a = obf_add.call(10, 20);
    printf("direct call:  add(10, 20) = %d\n", a);
 
    int b = GS_SPOOF(obf_add, 5, 3);
    printf("spoofed call: add(5, 3) = %d\n", b);
 
    int c = GS_SPOOF(obf_multiply, 6, 7);
    printf("spoofed call: multiply(6, 7) = %d\n", c);
 
    GS_SPOOF(obf_log);
 
    if (obf_add.valid())
        printf("pointer integrity check passed\n");
 
    return 0;
}

phantomcall.h:
Expand Collapse Copy
#pragma once
#include <Windows.h>
#include <Psapi.h>
#include <cstdint>
#include <atomic>
#include <type_traits>
#include <utility>
#include <random>
#include <cstring>
#include <mutex>
#pragma comment(lib, "Psapi.lib")
 
struct alignas(8) spoof_call_frame {
    void* trampoline;
    void* btit_retaddr;
    uint64_t btit_ss;
    void* ruts_retaddr;
    uint64_t ruts_ss;
    uint64_t gadget_ss;
    uint64_t real_ret;
    uint64_t real_rsp;
    uint64_t saved_rbx;
    uint64_t saved_r12;
};
 
static_assert(offsetof(spoof_call_frame, trampoline) == 0);
static_assert(offsetof(spoof_call_frame, btit_retaddr) == 8);
static_assert(offsetof(spoof_call_frame, btit_ss) == 16);
static_assert(offsetof(spoof_call_frame, ruts_retaddr) == 24);
static_assert(offsetof(spoof_call_frame, ruts_ss) == 32);
static_assert(offsetof(spoof_call_frame, gadget_ss) == 40);
static_assert(offsetof(spoof_call_frame, real_ret) == 48);
static_assert(offsetof(spoof_call_frame, real_rsp) == 56);
static_assert(offsetof(spoof_call_frame, saved_rbx) == 64);
static_assert(offsetof(spoof_call_frame, saved_r12) == 72);
 
extern "C" uint64_t spoof_stub(
    uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4,
    spoof_call_frame* frame, void* target);
 
namespace spoof {
 
    struct spoof_config {
        void* trampoline;
        void* btit_retaddr;
        uint64_t btit_ss;
        void* ruts_retaddr;
        uint64_t ruts_ss;
        uint64_t gadget_ss;
    };
 
    namespace detail {
 
        inline uintptr_t session_key() noexcept {
            static std::atomic<uintptr_t> s_key{ 0 };
            uintptr_t v = s_key.load(std::memory_order_acquire);
            if (v) return v;
            std::random_device rd;
            uintptr_t k = (static_cast<uintptr_t>(rd()) << 32) | rd();
            if (!k) k = 0xc0ffeedeadbeefULL;
            uintptr_t expected = 0;
            s_key.compare_exchange_strong(expected, k, std::memory_order_release);
            return s_key.load(std::memory_order_acquire);
        }
 
        inline uintptr_t obfuscate(uintptr_t ptr) noexcept { return ptr ^ session_key(); }
 
        template<typename... Args> struct all_trivially_copyable_impl : std::true_type {};
        template<typename T, typename... Rest>
        struct all_trivially_copyable_impl<T, Rest...>
            : std::integral_constant<bool,
            std::is_trivially_copyable<T>::value&&
            all_trivially_copyable_impl<Rest...>::value> {
        };
 
        template<typename... Args> struct all_fit_register_impl : std::true_type {};
        template<typename T, typename... Rest>
        struct all_fit_register_impl<T, Rest...>
            : std::integral_constant<bool,
            (sizeof(T) <= 8) && all_fit_register_impl<Rest...>::value> {
        };
 
        template<typename... Args> struct has_fp_impl : std::false_type {};
        template<typename T, typename... Rest>
        struct has_fp_impl<T, Rest...>
            : std::integral_constant<bool,
            std::is_floating_point<typename std::remove_cv<
            typename std::remove_reference<T>::type>::type>::value ||
            has_fp_impl<Rest...>::value> {
        };
 
        template<typename... Args>
        constexpr bool all_trivially_copyable = all_trivially_copyable_impl<Args...>::value;
        template<typename... Args>
        constexpr bool all_fit_register = all_fit_register_impl<Args...>::value;
        template<typename... Args>
        constexpr bool has_fp_args = has_fp_impl<Args...>::value;
 
        inline void pack_args(uint64_t*, std::size_t) {}
 
        template<typename T, typename... Rest>
        inline void pack_args(uint64_t* a, std::size_t idx, T val, Rest... rest) {
            a[idx] = 0;
            std::memcpy(&a[idx], &val, sizeof(T));
            pack_args(a, idx + 1, rest...);
        }
 
        struct pc_unwind_code {
            union { struct { BYTE CodeOffset; BYTE UnwindOpAndInfo; }; USHORT FrameOffset; };
            BYTE op()   const { return UnwindOpAndInfo & 0xF; }
            BYTE info() const { return (UnwindOpAndInfo >> 4) & 0xF; }
        };
 
        struct pc_unwind_info {
            BYTE RawByte0;
            BYTE SizeOfProlog;
            BYTE CountOfCodes;
            BYTE RawByte3;
            pc_unwind_code UnwindCode[1];
            BYTE flags() const { return RawByte0 >> 3; }
        };
 
        inline uint64_t parse_unwind_codes(pc_unwind_info* ui) {
            uint64_t size = 8;
            BYTE n = ui->CountOfCodes;
            for (BYTE i = 0; i < n; ) {
                if (i >= 128) break;
                BYTE op = ui->UnwindCode[i].op();
                BYTE info = ui->UnwindCode[i].info();
                switch (op) {
                case 0: size += 8; i += 1; break;
                case 1:
                    if (i + 1 >= n) { i = n; break; }
                    if (info == 0) { size += (uint64_t)ui->UnwindCode[i + 1].FrameOffset * 8; i += 2; }
                    else { size += *(ULONG*)&ui->UnwindCode[i + 1]; i += 3; }
                    break;
                case 2:  size += (uint64_t)info * 8 + 8; i += 1; break;
                case 3:  i += 1; break;
                case 4:  i += (i + 1 < n) ? 2 : n - i; break;
                case 5:  i += (i + 2 < n) ? 3 : n - i; break;
                case 8:  i += (i + 1 < n) ? 2 : n - i; break;
                case 9:  i += (i + 2 < n) ? 3 : n - i; break;
                case 10: size += info ? 88 : 80; i += 1; break;
                default: i += 1; break;
                }
            }
            return size;
        }
 
        inline uint64_t calculate_stack_size(void* fn) {
            if (!fn) return 8;
            DWORD64 image_base = 0;
            auto* rf = RtlLookupFunctionEntry((DWORD64)fn, &image_base, nullptr);
            if (!rf) return 8;
            auto* ui = (pc_unwind_info*)((uint8_t*)image_base + rf->UnwindData);
            uint64_t size = parse_unwind_codes(ui);
            if (ui->flags() & 4) {
                BYTE aligned = (ui->CountOfCodes + 1) & ~1;
                auto* cr = (RUNTIME_FUNCTION*)&ui->UnwindCode[aligned];
                auto* cui = (pc_unwind_info*)((uint8_t*)image_base + cr->UnwindData);
                size += parse_unwind_codes(cui) - 8;
            }
            if (size > 0x10000) return 8;
            return size;
        }
 
        inline uint64_t aligned_gadget_ss(uint64_t gs, uint64_t bs, uint64_t rs) noexcept {
            if (((gs + bs + rs) % 16) != 0) gs += 8;
            return gs;
        }
 
        inline void* scan_for_call(void* fn_base, std::size_t limit = 64) noexcept {
            if (!fn_base) return nullptr;
            auto* b = reinterpret_cast<const uint8_t*>(fn_base);
            for (std::size_t i = 0; i + 1 < limit; ++i) {
                if (b[i] == 0xE8)                             return const_cast<uint8_t*>(b + i);
                if (b[i] == 0xFF && (b[i + 1] & 0xF8) == 0xD0) return const_cast<uint8_t*>(b + i);
                if (b[i] == 0xFF && b[i + 1] == 0x15)           return const_cast<uint8_t*>(b + i);
            }
            return nullptr;
        }
 
        inline void* find_jmp_rbx(HMODULE mod) noexcept {
            if (!mod) return nullptr;
            auto* dos = (IMAGE_DOS_HEADER*)mod;
            auto* nt = (IMAGE_NT_HEADERS*)((uint8_t*)mod + dos->e_lfanew);
            auto* sec = IMAGE_FIRST_SECTION(nt);
            for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sec) {
                auto* base = (uint8_t*)mod + sec->VirtualAddress;
                DWORD  size = sec->Misc.VirtualSize;
                for (DWORD j = 0; j + 2 <= size; ++j) {
                    if (base[j] != 0xFF || base[j + 1] != 0xE3) continue;
                    MEMORY_BASIC_INFORMATION mbi{};
                    if (!VirtualQuery(base + j, &mbi, sizeof(mbi))) continue;
                    constexpr DWORD exec = PAGE_EXECUTE | PAGE_EXECUTE_READ
                        | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
                    if (!(mbi.Protect & exec)) continue;
                    if (j > 0 && base[j - 1] == 0xCC) continue;
                    if (j > 0 && base[j - 1] == 0x00 && j > 1 && base[j - 2] == 0x00) continue;
                    return base + j;
                }
            }
            return nullptr;
        }
 
        inline bool is_system_module(HMODULE mod) noexcept {
            char path[MAX_PATH]{};
            if (!GetModuleFileNameA(mod, path, MAX_PATH)) return false;
            char sys[MAX_PATH]{};
            GetSystemDirectoryA(sys, MAX_PATH);
            return _strnicmp(path, sys, strlen(sys)) == 0;
        }
 
        inline void* find_system_jmp_rbx() noexcept {
            const char* preferred[] = {
                "ntdll.dll", "kernel32.dll", "kernelbase.dll",
                "win32u.dll", "gdi32.dll", "user32.dll",
                "VCRUNTIME140.dll", "VCRUNTIME140_1.dll",
                "msvcp140.dll", nullptr
            };
            for (int i = 0; preferred[i]; ++i)
                if (auto* m = GetModuleHandleA(preferred[i]))
                    if (auto* g = find_jmp_rbx(m)) return g;
 
            HMODULE mods[256]; DWORD needed = 0;
            if (EnumProcessModules(GetCurrentProcess(), mods, sizeof(mods), &needed))
                for (DWORD i = 0; i < needed / sizeof(HMODULE); ++i)
                    if (is_system_module(mods[i]))
                        if (auto* g = find_jmp_rbx(mods[i])) return g;
 
            return nullptr;
        }
 
        inline const spoof_config& get_config() {
            static spoof_config cfg{};
            static std::once_flag flag;
            std::call_once(flag, []() {
                HMODULE k32 = GetModuleHandleA("kernel32.dll");
                HMODULE ntdll = GetModuleHandleA("ntdll.dll");
                cfg.trampoline = find_system_jmp_rbx();
                cfg.btit_retaddr = scan_for_call(GetProcAddress(k32, "BaseThreadInitThunk"));
                cfg.ruts_retaddr = scan_for_call(GetProcAddress(ntdll, "RtlUserThreadStart"));
                if (!cfg.trampoline || !cfg.btit_retaddr || !cfg.ruts_retaddr) {
                    cfg.trampoline = nullptr;
                    return;
                }
                cfg.btit_ss = calculate_stack_size(cfg.btit_retaddr);
                cfg.ruts_ss = calculate_stack_size(cfg.ruts_retaddr);
                cfg.gadget_ss = calculate_stack_size(cfg.trampoline);
                cfg.gadget_ss = aligned_gadget_ss(cfg.gadget_ss, cfg.btit_ss, cfg.ruts_ss);
            });
            return cfg;
        }
 
    }
 
    template<typename Fn> class obfuscated_fn;
 
    template<typename Ret, typename... Args>
    class obfuscated_fn<Ret(Args...)> {
        uintptr_t m_obf{};
 
        Ret(*decode() const)(Args...) {
            return reinterpret_cast<Ret(*)(Args...)>(detail::obfuscate(m_obf));
        }
 
    public:
        obfuscated_fn() = default;
 
        explicit obfuscated_fn(Ret(*fn)(Args...)) noexcept
            : m_obf(detail::obfuscate(reinterpret_cast<uintptr_t>(fn))) {
        }
 
        template<typename... A>
        Ret call(A&&... a) const { return decode()(std::forward<A>(a)...); }
 
        [[nodiscard]] bool valid() const noexcept {
            auto ptr = detail::obfuscate(m_obf);
            if (!ptr) return false;
            MEMORY_BASIC_INFORMATION mbi{};
            VirtualQuery(reinterpret_cast<void*>(ptr), &mbi, sizeof(mbi));
            return (mbi.Protect & (PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE
                | PAGE_EXECUTE | PAGE_EXECUTE_WRITECOPY)) != 0;
        }
 
        void* raw()    const noexcept { return reinterpret_cast<void*>(detail::obfuscate(m_obf)); }
        uintptr_t stored() const noexcept { return m_obf; }
        Ret(*get() const)(Args...) { return decode(); }
    };
 
    template<typename Ret, typename... Args>
    __forceinline Ret spoof_call(obfuscated_fn<Ret(Args...)>& fn, Args... args) {
        static_assert(!detail::has_fp_args<Args...>,
            "phantomcall: floating-point arguments are not supported");
        static_assert(sizeof...(Args) <= 4,
            "phantomcall: maximum 4 arguments supported");
        static_assert(detail::all_trivially_copyable<Args...>,
            "phantomcall: all arguments must be trivially copyable");
        static_assert(detail::all_fit_register<Args...>,
            "phantomcall: all arguments must be <= 8 bytes");
 
        const spoof_config& cfg = detail::get_config();
        if (!cfg.trampoline)
            return fn.call(args...);
 
        spoof_call_frame frame{};
        frame.trampoline = cfg.trampoline;
        frame.btit_retaddr = cfg.btit_retaddr;
        frame.btit_ss = cfg.btit_ss;
        frame.ruts_retaddr = cfg.ruts_retaddr;
        frame.ruts_ss = cfg.ruts_ss;
        frame.gadget_ss = cfg.gadget_ss;
 
        uint64_t a[4] = {};
        detail::pack_args(a, 0, args...);
 
        if constexpr (std::is_void_v<Ret>) {
            spoof_stub(a[0], a[1], a[2], a[3], &frame, fn.raw());
        }
        else {
            uint64_t r = spoof_stub(a[0], a[1], a[2], a[3], &frame, fn.raw());
            Ret result{};
            std::memcpy(&result, &r, sizeof(Ret));
            return result;
        }
    }
 
}
 
#define GS_OBFUSCATE(fn) \
    ::spoof::obfuscated_fn<std::remove_pointer_t<decltype(fn)>>(fn)
 
#define GS_SPOOF(obf_fn, ...) \
    ::spoof::spoof_call(obf_fn, ##__VA_ARGS__)

spoof_stub.asm:
Expand Collapse Copy
TRAMPOLINE   EQU 0
BTIT_RET     EQU 8
BTIT_SS      EQU 16
RUTS_RET     EQU 24
RUTS_SS      EQU 32
GADGET_SS    EQU 40
REAL_RET     EQU 48
REAL_RSP     EQU 56
SAVED_RBX    EQU 64
SAVED_R12    EQU 72
 
.code
 
fixup PROC
    mov  rsp, [r12 + REAL_RSP]
    mov  rbx, [r12 + SAVED_RBX]
    mov  r11, [r12 + REAL_RET]
    mov  r12, [r12 + SAVED_R12]
    jmp  r11
fixup ENDP
 
spoof_stub PROC
    mov  r10, [rsp+28h]
    mov  r11, [rsp+30h]
 
    mov  rax, [rsp]
    mov  [r10 + REAL_RET], rax
 
    lea  rax, [rsp+8]
    mov  [r10 + REAL_RSP], rax
 
    mov  [r10 + SAVED_RBX], rbx
    mov  [r10 + SAVED_R12], r12
 
    mov  r12, r10
 
    xor  rax, rax
    push rax
 
    sub  rsp, [r12 + RUTS_SS]
    mov  rax, [r12 + RUTS_RET]
    mov  [rsp], rax
 
    sub  rsp, [r12 + BTIT_SS]
    mov  rax, [r12 + BTIT_RET]
    mov  [rsp], rax
 
    sub  rsp, [r12 + GADGET_SS]
    mov  rax, [r12 + TRAMPOLINE]
    mov  [rsp], rax
 
    lea  rbx, fixup
 
    jmp  r11
spoof_stub ENDP
 
END

Сурсы полезны тем, кто хочет разобраться в архитектуре, а не просто копипастить готовые решения. Изучайте, как работают оффсеты и вызовы, но помните: если будете пихать это в продакшн-чит без доработки напильником — отлетите в бан быстрее, чем успеете сказать "пермач".
 
Назад
Сверху Снизу