Начинающий
- Статус
- Оффлайн
- Регистрация
- 13 Фев 2026
- Сообщения
- 126
- Реакции
- 3
Выкатили свежий релиз с загашников — чел откопал на старой флешке PhantomCall. Это x64 call stack spoofer, который подойдет тем, кто только начинает ковырять байпасы и хочет понять, как античиты палят хуки через стек.
Сразу по фактам: для актуального рейдж-софта в каком-нибудь Валоранте это не панацея, но как база для обучения — вполне годно. Автор сам признает, что современные AC такой трюк раскусят на раз-два, так что не ждите магического андетекта.
Что внутри:
Технические нюансы:
Сурсы полезны тем, кто хочет разобраться в архитектуре, а не просто копипастить готовые решения. Изучайте, как работают оффсеты и вызовы, но помните: если будете пихать это в продакшн-чит без доработки напильником — отлетите в бан быстрее, чем успеете сказать "пермач".
Сразу по фактам: для актуального рейдж-софта в каком-нибудь Валоранте это не панацея, но как база для обучения — вполне годно. Автор сам признает, что современные AC такой трюк раскусят на раз-два, так что не ждите магического андетекта.
Что внутри:
- GS_SPOOF: Маскирует ваш вызов под легитимный стек (RtlUserThreadStart — BaseThreadInitThunk — ntdll), чтобы чекеры не видели, что вызов идет из левого модуля.
- GS_OBFUSCATE: XOR-шифрует указатели на функции с рандомным ключом при каждом запуске процесса.
Технические нюансы:
- Requirements: Windows 10/11 x64, C++20, MSVC + MASM.
- Limitations: Только 4 аргумента, никакой поддержки float/double, требует MSVC. Если включен CET/Shadow Stack — работать не будет.
main.cpp:
#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:
#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:
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
Сурсы полезны тем, кто хочет разобраться в архитектуре, а не просто копипастить готовые решения. Изучайте, как работают оффсеты и вызовы, но помните: если будете пихать это в продакшн-чит без доработки напильником — отлетите в бан быстрее, чем успеете сказать "пермач".