mirror of
https://github.com/Cobalt-Strike/CallStackMasker/
synced 2026-06-06 15:24:31 +00:00
1121 lines
35 KiB
C++
1121 lines
35 KiB
C++
#pragma once
|
|
#include <ehdata.h>
|
|
#include <iostream>
|
|
#include <intrin.h>
|
|
#include <map>
|
|
#include <vector>
|
|
#include <Windows.h>
|
|
#include "psapi.h"
|
|
#pragma comment(lib,"ntdll.lib")
|
|
|
|
//
|
|
// From Ntdef.h.
|
|
//
|
|
// Treat anything not STATUS_SUCCESS as an error.
|
|
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) == 0)
|
|
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
|
|
#define STATUS_UNSUCCESSFUL ((NTSTATUS) 0x00000001L)
|
|
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS) 0xC0000004L)
|
|
|
|
#define ThreadQuerySetWin32StartAddress 9
|
|
#define SystemProcessInformation 5
|
|
#define RBP_OP_INFO 0x5
|
|
|
|
// Dynamic spoof options.
|
|
// NB This PoC only supports WaitForSingleObjectEx.
|
|
std::string targetWaitModule("kernelbase");
|
|
std::string targetWaitFunction("WaitForSingleObjectEx");
|
|
|
|
// Struct to store info on target thread to copy.
|
|
typedef struct
|
|
{
|
|
DWORD dwPid;
|
|
DWORD dwTid;
|
|
PVOID startAddr;
|
|
ULONG totalRequiredStackSize;
|
|
PVOID pFakeStackBuffer;
|
|
} threadToSpoof;
|
|
|
|
/*
|
|
Structs to call NtQuerySystemInformation.
|
|
Based on: https://github.com/thefLink/Hunt-Sleeping-Beacons/blob/main/source/Nt.h
|
|
*/
|
|
typedef NTSTATUS(WINAPI* pNtQueryInformationThread)(HANDLE, LONG, PVOID, ULONG, PULONG);
|
|
typedef NTSTATUS(WINAPI* pNtQuerySystemInformation)(int, PVOID, ULONG, PULONG);
|
|
|
|
typedef struct _CLIENT_ID {
|
|
HANDLE UniqueProcess;
|
|
HANDLE UniqueThread;
|
|
} CLIENT_ID;
|
|
|
|
typedef struct
|
|
{
|
|
SIZE_T PeakVirtualSize;
|
|
SIZE_T VirtualSize;
|
|
ULONG PageFaultCount;
|
|
SIZE_T PeakWorkingSetSize;
|
|
SIZE_T WorkingSetSize;
|
|
SIZE_T QuotaPeakPagedPoolUsage;
|
|
SIZE_T QuotaPagedPoolUsage;
|
|
SIZE_T QuotaPeakNonPagedPoolUsage;
|
|
SIZE_T QuotaNonPagedPoolUsage;
|
|
SIZE_T PagefileUsage;
|
|
SIZE_T PeakPagefileUsage;
|
|
} VM_COUNTERS;
|
|
|
|
typedef struct
|
|
{
|
|
LARGE_INTEGER KernelTime;
|
|
LARGE_INTEGER UserTime;
|
|
LARGE_INTEGER CreateTime;
|
|
ULONG WaitTime;
|
|
PVOID StartAddress;
|
|
CLIENT_ID ClientId;
|
|
LONG Priority;
|
|
LONG BasePriority;
|
|
ULONG ContextSwitches;
|
|
ULONG ThreadState;
|
|
ULONG WaitReason;
|
|
} SYSTEM_THREAD_INFORMATION;
|
|
|
|
typedef struct
|
|
{
|
|
USHORT Length;
|
|
USHORT MaximumLength;
|
|
PWSTR Buffer;
|
|
} UNICODE_STRING;
|
|
|
|
typedef struct
|
|
{
|
|
ULONG NextEntryOffset;
|
|
ULONG NumberOfThreads;
|
|
LARGE_INTEGER WorkingSetPrivateSize;
|
|
ULONG HardFaultCount;
|
|
ULONG NumberOfThreadsHighWatermark;
|
|
ULONGLONG CycleTime;
|
|
FILETIME CreateTime;
|
|
FILETIME UserTime;
|
|
FILETIME KernelTime;
|
|
UNICODE_STRING ImageName;
|
|
LONG BasePriority;
|
|
#ifdef _WIN64
|
|
ULONG pad1;
|
|
#endif
|
|
ULONG ProcessId;
|
|
#ifdef _WIN64
|
|
ULONG pad2;
|
|
#endif
|
|
ULONG InheritedFromProcessId;
|
|
#ifdef _WIN64
|
|
ULONG pad3;
|
|
#endif
|
|
ULONG HandleCount;
|
|
ULONG SessionId;
|
|
ULONG_PTR UniqueProcessKey;
|
|
VM_COUNTERS VirtualMemoryCounters;
|
|
ULONG_PTR PrivatePageCount;
|
|
IO_COUNTERS IoCounters;
|
|
SYSTEM_THREAD_INFORMATION ThreadInfos[1];
|
|
} SYSTEM_PROCESS_INFORMATION;
|
|
|
|
/*
|
|
Thread WaitReason enum, based on https://gist.github.com/TheWover/799822ce3d1239e0bd5764ac0b0adfda.
|
|
*/
|
|
typedef enum _KWAIT_REASON
|
|
{
|
|
Executive,
|
|
FreePage,
|
|
PageIn,
|
|
PoolAllocation,
|
|
DelayExecution,
|
|
Suspended,
|
|
UserRequest,
|
|
WrExecutive,
|
|
WrFreePage,
|
|
WrPageIn,
|
|
WrPoolAllocation,
|
|
WrDelayExecution,
|
|
WrSuspended,
|
|
WrUserRequest,
|
|
WrEventPair,
|
|
WrQueue,
|
|
WrLpcReceive,
|
|
WrLpcReply,
|
|
WrVirtualMemory,
|
|
WrPageOut,
|
|
WrRendezvous,
|
|
WrKeyedEvent,
|
|
WrTerminated,
|
|
WrProcessInSwap,
|
|
WrCpuRateControl,
|
|
WrCalloutStack,
|
|
WrKernel,
|
|
WrResource,
|
|
WrPushLock,
|
|
WrMutex,
|
|
WrQuantumEnd,
|
|
WrDispatchInt,
|
|
WrPreempted,
|
|
WrYieldExecution,
|
|
WrFastMutex,
|
|
WrGuardedMutex,
|
|
WrRundown,
|
|
WrAlertByThreadId,
|
|
WrDeferredPreempt,
|
|
MaximumWaitReason
|
|
} KWAIT_REASON, * PKWAIT_REASON;
|
|
|
|
/*
|
|
The utility functions/structs below are based on the following two PoCs:
|
|
- https://github.com/WithSecureLabs/CallStackSpoofer
|
|
- https://github.com/WithSecureLabs/TickTock
|
|
Full credit to @WithSecureLabs.
|
|
*/
|
|
|
|
//
|
|
// A lookup map for modules and their corresponding image base.
|
|
//
|
|
typedef std::map<std::string, HMODULE> imageMap;
|
|
// std::wstring equivalent.
|
|
std::map<std::wstring, HMODULE> imageBaseMap;
|
|
|
|
//
|
|
// Used to store information for individual stack frames for call stack to spoof.
|
|
//
|
|
struct StackFrame {
|
|
std::wstring targetDll;
|
|
std::string targetFunc;
|
|
ULONG offset;
|
|
ULONG totalStackSize;
|
|
BOOL requiresLoadLibrary;
|
|
BOOL setsFramePointer;
|
|
PVOID returnAddress;
|
|
BOOL pushRbp;
|
|
ULONG countOfCodes;
|
|
BOOL pushRbpIndex;
|
|
StackFrame() = default;
|
|
StackFrame(std::wstring dllPath, std::string function, ULONG targetOffset, ULONG targetStackSize, bool bDllLoad) :
|
|
targetDll(dllPath),
|
|
targetFunc(function),
|
|
offset(targetOffset),
|
|
totalStackSize(targetStackSize),
|
|
requiresLoadLibrary(bDllLoad),
|
|
setsFramePointer(false),
|
|
returnAddress(0),
|
|
pushRbp(false),
|
|
countOfCodes(0),
|
|
pushRbpIndex(0)
|
|
{
|
|
};
|
|
};
|
|
|
|
//
|
|
// Unwind op codes: https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170.
|
|
//
|
|
typedef enum _UNWIND_OP_CODES {
|
|
UWOP_PUSH_NONVOL = 0, /* info == register number */
|
|
UWOP_ALLOC_LARGE, /* no info, alloc size in next 2 slots */
|
|
UWOP_ALLOC_SMALL, /* info == size of allocation / 8 - 1 */
|
|
UWOP_SET_FPREG, /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */
|
|
UWOP_SAVE_NONVOL, /* info == register number, offset in next slot */
|
|
UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */
|
|
UWOP_SAVE_XMM128 = 8, /* info == XMM reg number, offset in next slot */
|
|
UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */
|
|
UWOP_PUSH_MACHFRAME /* info == 0: no error-code, 1: error-code */
|
|
} UNWIND_CODE_OPS;
|
|
|
|
//
|
|
// Calculates the image base for the given stack frame
|
|
// and adds it to the image base map.
|
|
//
|
|
NTSTATUS GetImageBase(const StackFrame& stackFrame)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
HMODULE tmpImageBase = 0;
|
|
|
|
// [0] Check if image base has already been resolved.
|
|
if (imageBaseMap.count(stackFrame.targetDll))
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [1] Check if current frame contains a
|
|
// non standard dll and load if so.
|
|
if (stackFrame.requiresLoadLibrary)
|
|
{
|
|
tmpImageBase = LoadLibrary(stackFrame.targetDll.c_str());
|
|
if (!tmpImageBase)
|
|
{
|
|
status = STATUS_DLL_NOT_FOUND;
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
|
|
// [2] If we haven't already recorded the
|
|
// image base capture it now.
|
|
if (!tmpImageBase)
|
|
{
|
|
tmpImageBase = GetModuleHandle(stackFrame.targetDll.c_str());
|
|
if (!tmpImageBase)
|
|
{
|
|
status = STATUS_DLL_NOT_FOUND;
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
|
|
// [3] Add to image base map to avoid superfluous recalculating.
|
|
imageBaseMap.insert({ stackFrame.targetDll, tmpImageBase });
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Uses the offset within the StackFrame structure to
|
|
// calculate the return address for fake frame.
|
|
// *Modified this to use GetProcAddress + offset in function*.
|
|
//
|
|
NTSTATUS CalculateReturnAddress(StackFrame& stackFrame)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
|
|
try {
|
|
const PVOID targetImageBaseAddress = imageBaseMap.at(stackFrame.targetDll);
|
|
if (!targetImageBaseAddress) {
|
|
status = STATUS_DLL_NOT_FOUND;
|
|
goto Cleanup;
|
|
}
|
|
auto funcAddr = GetProcAddress((HMODULE)targetImageBaseAddress, stackFrame.targetFunc.c_str());
|
|
if (!funcAddr) {
|
|
status = STATUS_ORDINAL_NOT_FOUND;
|
|
goto Cleanup;
|
|
}
|
|
stackFrame.returnAddress = (PCHAR)funcAddr + stackFrame.offset;
|
|
}
|
|
catch (const std::out_of_range&)
|
|
{
|
|
std::cout << "Dll \"" << stackFrame.targetDll.c_str() << "\" not found" << std::endl;
|
|
status = STATUS_DLL_NOT_FOUND;
|
|
goto Cleanup;
|
|
}
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Calculates the total stack space used by the fake stack frame. Uses
|
|
// a minimal implementation of RtlVirtualUnwind to parse the unwind codes
|
|
// for target function and add up total stack size. Largely based on:
|
|
// https://github.com/hzqst/unicorn_pe/blob/master/unicorn_pe/except.cpp#L773
|
|
//
|
|
NTSTATUS CalculateFunctionStackSize(PRUNTIME_FUNCTION pRuntimeFunction, const DWORD64 ImageBase, StackFrame& stackFrame)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
PUNWIND_INFO pUnwindInfo = NULL;
|
|
ULONG unwindOperation = 0;
|
|
ULONG operationInfo = 0;
|
|
ULONG index = 0;
|
|
ULONG frameOffset = 0;
|
|
|
|
// [0] Sanity check incoming pointer.
|
|
if (!pRuntimeFunction)
|
|
{
|
|
std::cout << " [-] No RUNTIME_FUNCTION found for target function.\n";
|
|
status = STATUS_INVALID_PARAMETER;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [1] Loop over unwind info.
|
|
// NB As this is a PoC, it does not handle every unwind operation, but
|
|
// rather the minimum set required to successfully mimic the default
|
|
// call stacks included.
|
|
pUnwindInfo = (PUNWIND_INFO)(pRuntimeFunction->UnwindData + ImageBase);
|
|
while (index < pUnwindInfo->CountOfCodes)
|
|
{
|
|
unwindOperation = pUnwindInfo->UnwindCode[index].UnwindOp;
|
|
operationInfo = pUnwindInfo->UnwindCode[index].OpInfo;
|
|
// [2] Loop over unwind codes and calculate
|
|
// total stack space used by target function.
|
|
switch (unwindOperation) {
|
|
case UWOP_PUSH_NONVOL:
|
|
// UWOP_PUSH_NONVOL is 8 bytes.
|
|
stackFrame.totalStackSize += 8;
|
|
// Record if it pushes rbp as
|
|
// this is important for UWOP_SET_FPREG.
|
|
if (RBP_OP_INFO == operationInfo)
|
|
{
|
|
stackFrame.pushRbp = true;
|
|
// Record when rbp is pushed to stack.
|
|
stackFrame.countOfCodes = pUnwindInfo->CountOfCodes;
|
|
stackFrame.pushRbpIndex = index + 1;
|
|
}
|
|
break;
|
|
case UWOP_SAVE_NONVOL:
|
|
//UWOP_SAVE_NONVOL doesn't contribute to stack size
|
|
// but you do need to increment index.
|
|
index += 1;
|
|
break;
|
|
case UWOP_ALLOC_SMALL:
|
|
//Alloc size is op info field * 8 + 8.
|
|
stackFrame.totalStackSize += ((operationInfo * 8) + 8);
|
|
break;
|
|
case UWOP_ALLOC_LARGE:
|
|
// Alloc large is either:
|
|
// 1) If op info == 0 then size of alloc / 8
|
|
// is in the next slot (i.e. index += 1).
|
|
// 2) If op info == 1 then size is in next
|
|
// two slots.
|
|
index += 1;
|
|
frameOffset = pUnwindInfo->UnwindCode[index].FrameOffset;
|
|
if (operationInfo == 0)
|
|
{
|
|
frameOffset *= 8;
|
|
}
|
|
else
|
|
{
|
|
index += 1;
|
|
frameOffset += (pUnwindInfo->UnwindCode[index].FrameOffset << 16);
|
|
}
|
|
stackFrame.totalStackSize += frameOffset;
|
|
break;
|
|
case UWOP_SET_FPREG:
|
|
// This sets rsp == rbp (mov rsp,rbp), so we need to ensure
|
|
// that rbp is the expected value (in the frame above) when
|
|
// it comes to spoof this frame in order to ensure the
|
|
// call stack is correctly unwound.
|
|
stackFrame.setsFramePointer = true;
|
|
break;
|
|
default:
|
|
std::cout << " [-] Error: Unsupported Unwind Op Code\n";
|
|
status = STATUS_ASSERTION_FAILURE;
|
|
goto Cleanup;
|
|
}
|
|
|
|
index += 1;
|
|
}
|
|
|
|
// If chained unwind information is present then we need to
|
|
// also recursively parse this and add to total stack size.
|
|
if (0 != (pUnwindInfo->Flags & UNW_FLAG_CHAININFO))
|
|
{
|
|
index = pUnwindInfo->CountOfCodes;
|
|
if (0 != (index & 1))
|
|
{
|
|
index += 1;
|
|
}
|
|
pRuntimeFunction = (PRUNTIME_FUNCTION)(&pUnwindInfo->UnwindCode[index]);
|
|
return CalculateFunctionStackSize(pRuntimeFunction, ImageBase, stackFrame);
|
|
}
|
|
|
|
// Add the size of the return address (8 bytes).
|
|
stackFrame.totalStackSize += 8;
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Retrieves the runtime function entry for given fake ret address
|
|
// and calls CalculateFunctionStackSize, which will recursively
|
|
// calculate the total stack space utilisation.
|
|
//
|
|
NTSTATUS CalculateFunctionStackSizeWrapper(StackFrame& stackFrame)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
PRUNTIME_FUNCTION pRuntimeFunction = NULL;
|
|
DWORD64 ImageBase = 0;
|
|
PUNWIND_HISTORY_TABLE pHistoryTable = NULL;
|
|
|
|
// [0] Sanity check return address.
|
|
if (!stackFrame.returnAddress)
|
|
{
|
|
status = STATUS_INVALID_PARAMETER;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [1] Locate RUNTIME_FUNCTION for given function.
|
|
pRuntimeFunction = RtlLookupFunctionEntry(
|
|
(DWORD64)stackFrame.returnAddress,
|
|
&ImageBase,
|
|
pHistoryTable);
|
|
if (NULL == pRuntimeFunction)
|
|
{
|
|
status = STATUS_ASSERTION_FAILURE;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [2] Recursively calculate the total stack size for
|
|
// the function we are "returning" to.
|
|
status = CalculateFunctionStackSize(pRuntimeFunction, ImageBase, stackFrame);
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Takes a target call stack and configures it ready for use
|
|
// via loading any required dlls, resolving module addresses,
|
|
// and calculating spoofed return addresses.
|
|
//
|
|
NTSTATUS InitialiseSpoofedCallstack(std::vector<StackFrame>& targetCallStack)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
|
|
for (auto stackFrame = targetCallStack.begin(); stackFrame != targetCallStack.end(); stackFrame++)
|
|
{
|
|
// [1] Get image base for current stack frame.
|
|
status = GetImageBase(*stackFrame);
|
|
if (!NT_SUCCESS(status))
|
|
{
|
|
std::cout << "[-] Error: Failed to get image base\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [2] Calculate ret address for current stack frame.
|
|
status = CalculateReturnAddress(*stackFrame);
|
|
if (!NT_SUCCESS(status))
|
|
{
|
|
std::cout << "[-] Error: Failed to calculate ret address\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [3] Calculate the total stack size for ret function.
|
|
status = CalculateFunctionStackSizeWrapper(*stackFrame);
|
|
if (!NT_SUCCESS(status))
|
|
{
|
|
std::cout << "[-] Error: Failed to caluclate total stack size\n";
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Templated wrappers around ReadProcessMemory.
|
|
//
|
|
template<typename T>
|
|
T readProcessMemory(HANDLE hProcess, LPVOID targetAddress)
|
|
{
|
|
T returnValue;
|
|
(void)ReadProcessMemory(hProcess, targetAddress, &returnValue, sizeof(T), NULL);
|
|
return returnValue;
|
|
};
|
|
|
|
//
|
|
// Takes an address and retrieves the base name of the specified module.
|
|
//
|
|
NTSTATUS GetModuleBaseNameWrapper(HANDLE hProcess, PVOID targetAddress, std::string& moduleName)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
char szModuleBaseName[MAX_PATH];
|
|
|
|
if (GetModuleBaseNameA(hProcess, (HMODULE)targetAddress, szModuleBaseName, sizeof(szModuleBaseName)))
|
|
{
|
|
moduleName = szModuleBaseName;
|
|
}
|
|
else
|
|
{
|
|
printf(" [-] GetModuleBaseName returned error : %d\n", GetLastError());
|
|
status = STATUS_ASSERTION_FAILURE;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/*
|
|
CallStackMasker utility functions.
|
|
*/
|
|
|
|
NTSTATUS HandleArgs(int argc, char* argv[], BOOL& bStaticCallStack)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
|
|
if (argc < 2)
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
else
|
|
{
|
|
std::string callstackArg(argv[1]);
|
|
if (callstackArg == "--dynamic")
|
|
{
|
|
bStaticCallStack = false;
|
|
}
|
|
else
|
|
{
|
|
std::cout << "[-] Error: Incorrect argument provided. The options are: --dynamic\n";
|
|
status = ERROR_INVALID_PARAMETER;
|
|
}
|
|
}
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Retrieve Child-SP for caller.
|
|
//
|
|
__declspec(noinline) void* GetChildSP()
|
|
{
|
|
// Add 8 to get correct Child-SP for frame.
|
|
return (PCHAR)_AddressOfReturnAddress() + 8;
|
|
}
|
|
|
|
/*
|
|
Static call stack masking functions.
|
|
*/
|
|
|
|
//
|
|
// Calculates total stack space for a given static call stack.
|
|
//
|
|
ULONG CalculateStaticStackSize(const std::vector<StackFrame>& targetCallStack)
|
|
{
|
|
ULONG totalStackCount = 0x0;
|
|
for (auto entry : targetCallStack)
|
|
{
|
|
totalStackCount += entry.totalStackSize;
|
|
}
|
|
// Add 0x8 so we can write 0x0 as last
|
|
// address and stop stack unwinding.
|
|
totalStackCount += 0x8;
|
|
return totalStackCount;
|
|
}
|
|
|
|
//
|
|
// Creates a buffer containing a fake call stack layout.
|
|
//
|
|
NTSTATUS CreateFakeStackInBuffer(const std::vector<StackFrame>& targetCallStack, const PVOID pSpoofedStack)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
int64_t* index = NULL;
|
|
|
|
// [0] Sanity check incoming ptr.
|
|
if (NULL == pSpoofedStack)
|
|
{
|
|
status = STATUS_INVALID_PARAMETER;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [1] Loop over buffer and create desired stack layout.
|
|
// NB This should be sanity checked for overflows but ignored for PoC.
|
|
index = (int64_t*)pSpoofedStack;
|
|
for (auto entry : targetCallStack)
|
|
{
|
|
// Write ret address.
|
|
*index = (int64_t)entry.returnAddress;
|
|
// Increment index to next Child-SP + 0x8;
|
|
auto offset = entry.totalStackSize / sizeof(int64_t);
|
|
index += offset;
|
|
}
|
|
|
|
// [2] Stop stack unwinding by writing 0x0 at end of buffer.
|
|
*index = 0x0;
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Creates a fake stack layout in memory based on a given *static* call stack.
|
|
//
|
|
NTSTATUS InitialiseStaticCallStackSpoofing(std::vector<StackFrame>& targetCallStack, threadToSpoof& thread)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
HANDLE hHeap = NULL;
|
|
|
|
// [1] Initialise the target call stack.
|
|
if (!NT_SUCCESS(InitialiseSpoofedCallstack(targetCallStack)))
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [2] Calculate total stack space required for fake call stack.
|
|
thread.totalRequiredStackSize = CalculateStaticStackSize(targetCallStack);
|
|
|
|
// [3] Allocate heap memory for required stack size.
|
|
// NB this is currently never freed but it is irrelevant as PoC runs on while true loop.
|
|
hHeap = GetProcessHeap();
|
|
if (!hHeap)
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
thread.pFakeStackBuffer = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, thread.totalRequiredStackSize);
|
|
if (!thread.pFakeStackBuffer)
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [4] Create fake stack.
|
|
if (!NT_SUCCESS(CreateFakeStackInBuffer(targetCallStack, thread.pFakeStackBuffer)))
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
//std::cout << "[+] Fake stack is at: 0x" << std::hex << thread.pFakeStackBuffer << "\n";
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
/*
|
|
Dynamic call stack masking functions.
|
|
*/
|
|
|
|
//
|
|
// Retrieve a thread's starting address.
|
|
//
|
|
NTSTATUS GetThreadStartAddress(const HANDLE hThread, PVOID& startAddress)
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
|
|
pNtQueryInformationThread NtQueryInformationThread = (pNtQueryInformationThread)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationThread");
|
|
if (!NtQueryInformationThread)
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
|
|
if (!NT_SUCCESS(NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress, &startAddress, sizeof(startAddress), NULL)))
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Checks if a ret address found on a stack is located within a specified function.
|
|
//
|
|
BOOL CheckIfAddressIsWithinTargetFunc(const PVOID targetAddress, const std::string targetModule, const std::string targetFunction)
|
|
{
|
|
BOOL bAddressIsWithinTargetFunc = FALSE;
|
|
|
|
HMODULE hModule = NULL;
|
|
PVOID pTargetFunction = NULL;
|
|
PRUNTIME_FUNCTION pRuntimeFunction = NULL;
|
|
PUNWIND_HISTORY_TABLE pHistoryTable = NULL;
|
|
DWORD64 ImageBase = 0;
|
|
void* targetFunctionStart = NULL;
|
|
void* targetFunctionEnd = NULL;
|
|
|
|
// [1] Resolve target function.
|
|
hModule = GetModuleHandleA(targetModule.c_str());
|
|
if (!hModule)
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
pTargetFunction = GetProcAddress(hModule, targetFunction.c_str());
|
|
if (!pTargetFunction)
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [2] Find function limits.
|
|
pRuntimeFunction = RtlLookupFunctionEntry(
|
|
(DWORD64)pTargetFunction,
|
|
&ImageBase,
|
|
pHistoryTable);
|
|
if (!pRuntimeFunction)
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [3] Check if given pointer is within range.
|
|
targetFunctionStart = (PCHAR)hModule + pRuntimeFunction->BeginAddress;
|
|
targetFunctionEnd = (PCHAR)hModule + pRuntimeFunction->EndAddress;
|
|
if ((targetAddress > targetFunctionStart) && (targetAddress < targetFunctionEnd))
|
|
{
|
|
bAddressIsWithinTargetFunc = TRUE;
|
|
}
|
|
|
|
Cleanup:
|
|
return bAddressIsWithinTargetFunc;
|
|
}
|
|
|
|
//
|
|
// Takes an address in a remote process and re-maps it to the local process.
|
|
//
|
|
NTSTATUS NormalizeAddress(const HANDLE hProcess, const PVOID remoteAddress, PVOID& localAddress, const BOOL bIgnoreExe, const imageMap& imageBaseMap = imageMap())
|
|
{
|
|
NTSTATUS status = STATUS_SUCCESS;
|
|
|
|
MEMORY_BASIC_INFORMATION mbi = { 0 };
|
|
std::string moduleName;
|
|
HMODULE hModule = NULL;
|
|
ULONG64 offset = 0;
|
|
|
|
// [1] Query pages at target address.
|
|
if (!VirtualQueryEx(hProcess, (PVOID)remoteAddress, &mbi, sizeof(mbi)))
|
|
{
|
|
std::cout << " [-] VirtualQueryEx failed\n";
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
// Calculate offset of return address.
|
|
offset = (PCHAR)remoteAddress - (PCHAR)mbi.AllocationBase;
|
|
|
|
// [2] Try and resolve module at address.
|
|
if (!NT_SUCCESS(GetModuleBaseNameWrapper(hProcess, mbi.AllocationBase, moduleName)))
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
std::cout << " [+] Module at ret address is: " << moduleName << "\n";
|
|
|
|
// [3] Some matching call stacks will have .exe's in them (e.g. taskhostw.exe).
|
|
// This could look weird in a different process so ignore them here.
|
|
if (bIgnoreExe)
|
|
{
|
|
if ((moduleName.find(".exe") != std::string::npos) || (moduleName.find(".EXE") != std::string::npos))
|
|
{
|
|
std::cout << " [!] Found executable in call stack so rejecting: " << moduleName << "\n";
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
|
|
// [4] If we can't get a handle, load Dll.
|
|
hModule = GetModuleHandleA(moduleName.c_str());
|
|
if (NULL == hModule)
|
|
{
|
|
std::cout << " [+] Loading Library: " << moduleName.c_str() << "\n";
|
|
// NB This uses standard search strategy as opposed to full path
|
|
// so may fail and could be more robust (say by using GetMappedFileName)
|
|
// e.g. clr.dll most obvious example of this.
|
|
hModule = LoadLibraryA(moduleName.c_str());
|
|
if (NULL == hModule)
|
|
{
|
|
std::cout << " [-] Failed to load dll: " << moduleName << "\n";
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
// Add to map so that if we fail later on in the stack unwinding
|
|
// process we can unload any dlls no longer needed.
|
|
(const_cast<imageMap&>(imageBaseMap)).insert({ moduleName, hModule });
|
|
}
|
|
localAddress = (PCHAR)hModule + offset;
|
|
|
|
Cleanup:
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Walks the call stack of the target thread and calculates its total size.
|
|
//
|
|
NTSTATUS CalculateDynamicStackSize(const HANDLE hProcess, const CONTEXT ctx, ULONG& totalStackSize)
|
|
{
|
|
NTSTATUS status = STATUS_UNSUCCESSFUL;
|
|
|
|
PVOID returnAddress = NULL;
|
|
PVOID previousReturnAddress = NULL;
|
|
PVOID currentChildSP = NULL;
|
|
PVOID stackIndex = NULL;
|
|
BOOL bHandledFirstFrame = FALSE;
|
|
BOOL bFinishedUnwinding = FALSE;
|
|
imageMap imageBaseMap = {};
|
|
|
|
currentChildSP = (PVOID)ctx.Rsp;
|
|
stackIndex = (PVOID)ctx.Rsp;
|
|
std::cout << " [+] Child-SP: 0x" << std::hex << currentChildSP << "\n";
|
|
|
|
// [1] Start unwinding the target thread stack.
|
|
while (!bFinishedUnwinding)
|
|
{
|
|
// NB This is a *very* lightweight/hackerman method of unwinding a stack which
|
|
// makes an assumption about the state of the target thread (i.e. it is waiting).
|
|
if (!bHandledFirstFrame)
|
|
{
|
|
// We need to handle the first frame, which we assume
|
|
// will be an ntdll wait function.
|
|
returnAddress = (PVOID)ctx.Rip;
|
|
bHandledFirstFrame = TRUE;
|
|
}
|
|
else
|
|
{
|
|
// [2] Retrieve the ret address at current stack index.
|
|
previousReturnAddress = returnAddress;
|
|
returnAddress = readProcessMemory<PVOID>(hProcess, (LPVOID)stackIndex);
|
|
std::cout << " [+] RetAddr: 0x" << std::hex << returnAddress << "\n";
|
|
std::cout << " [+] Child-SP: " << std::hex << currentChildSP << "\n";
|
|
}
|
|
|
|
// [3] Windows unwinds until it finds ret address of 0x0
|
|
// so if this is true we have finished unwinding the stack.
|
|
if (returnAddress == 0x0)
|
|
{
|
|
// For unknown reasons (exception handlers?) the basic stack unwinding functionality
|
|
// in this PoC seems to have problems handling dbg related functionality
|
|
// (e.g. DbgX.Shell.exe / corecrl.dll / Enghost.exe / dbgeng.dll..).
|
|
// Therefore, as a sanity check, make sure the last address is ntdll!RtlUserThreadStart
|
|
// before reporting success.
|
|
if (!CheckIfAddressIsWithinTargetFunc(previousReturnAddress, "ntdll", "RtlUserThreadStart"))
|
|
{
|
|
std::cout << " [-] Failed to unwind stack properly\n";
|
|
goto Cleanup;
|
|
}
|
|
// NB If you comment out the status below you can enumerate all the thread stacks in the desired state.
|
|
status = STATUS_SUCCESS;
|
|
bFinishedUnwinding = TRUE;
|
|
}
|
|
else
|
|
{
|
|
StackFrame targetFrame = {};
|
|
PRUNTIME_FUNCTION pRuntimeFunction = NULL;
|
|
PUNWIND_HISTORY_TABLE pHistoryTable = NULL;
|
|
DWORD64 ImageBase = 0;
|
|
ULONG functionStackSize = 0;
|
|
|
|
// [4] Normalize address so it is valid in context of local process.
|
|
if (!NT_SUCCESS(NormalizeAddress(hProcess, returnAddress, targetFrame.returnAddress, TRUE, imageBaseMap)))
|
|
{
|
|
std::cout << " [-] Failed to normalize remote address\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [5] Calculate function size.
|
|
pRuntimeFunction = RtlLookupFunctionEntry(
|
|
(DWORD64)targetFrame.returnAddress,
|
|
&ImageBase,
|
|
pHistoryTable);
|
|
if (!NT_SUCCESS(CalculateFunctionStackSize(pRuntimeFunction, ImageBase, targetFrame)))
|
|
{
|
|
std::cout << " [-] Failed to calculate function stack size\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [6] Increment total stack size count and record function stack size count.
|
|
totalStackSize += targetFrame.totalStackSize;
|
|
functionStackSize = targetFrame.totalStackSize;
|
|
|
|
// [7] Find next Child-SP.
|
|
currentChildSP = (PCHAR)currentChildSP + functionStackSize;
|
|
|
|
// [8] Find next return address.
|
|
// Child-SP is value of rsp after stack prologue hence ret
|
|
// address is pushed on immediately after.
|
|
stackIndex = (PCHAR)currentChildSP - 0x8;
|
|
}
|
|
}
|
|
|
|
Cleanup:
|
|
if (!NT_SUCCESS(status))
|
|
{
|
|
// If we failed for w/e reason, unload all the libraries we loaded.
|
|
for (auto const& lib : imageBaseMap)
|
|
{
|
|
(void)FreeLibrary(lib.second);
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
//
|
|
// Checks target thread to see if it is waiting in our desired wait method (e.g. UserRequest + WaitForSingleObjectEx).
|
|
//
|
|
BOOL IsThreadAMatch(const HANDLE hProcess, const DWORD pid, const DWORD tid, threadToSpoof& thread)
|
|
{
|
|
BOOL bMatch = FALSE;
|
|
|
|
HANDLE hThread = INVALID_HANDLE_VALUE;
|
|
HANDLE hHeap = INVALID_HANDLE_VALUE;
|
|
BOOL bIsWow64 = false;
|
|
CONTEXT ctx = { 0 };
|
|
|
|
PVOID returnAddress = NULL;
|
|
PVOID remoteStartAddress = NULL;
|
|
ULONG totalStackSize = 0;
|
|
|
|
// [1] Open handle to thread.
|
|
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
|
|
if (!hThread)
|
|
{
|
|
std::cout << "[-] Failed to open a handle to thread: " << tid << "\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [2] Get thread context.
|
|
std::cout << "[+] Scanning tid: " << std::dec << tid << "\n";
|
|
ctx.ContextFlags = CONTEXT_FULL;
|
|
if (!GetThreadContext(hThread, &ctx))
|
|
{
|
|
std::cout << "[-] Failed to get thread context for: " << tid << "\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [3] Retrieve the last return address on the stack and check if it is
|
|
// our target function to spoof.
|
|
returnAddress = readProcessMemory<PVOID>(hProcess, (LPVOID)ctx.Rsp);
|
|
if (!CheckIfAddressIsWithinTargetFunc(returnAddress, targetWaitModule, targetWaitFunction))
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [4] Now try and confirm we can unwind the stack and calculate total required stack size.
|
|
if (!NT_SUCCESS(CalculateDynamicStackSize(hProcess, ctx, totalStackSize)))
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [5] Lastly, we need to retrieve the threads starting address in order to spoof it.
|
|
if (!NT_SUCCESS(GetThreadStartAddress(hThread, remoteStartAddress)))
|
|
{
|
|
std::cout << "[-] Error retrieving thread start address\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [6] The start address is specific to context of remote process, so ensure the
|
|
// offset is correct for wherever the dll is loaded in our memory space.
|
|
if (!NT_SUCCESS(NormalizeAddress(hProcess, remoteStartAddress, thread.startAddr, FALSE)))
|
|
{
|
|
std::cout << "[-] Error re-calculating thread start address\n";
|
|
goto Cleanup;
|
|
}
|
|
|
|
// [7] At this stage, the thread stack is a match so make a copy.
|
|
// To simplify this PoC (and to avoid any TOCTOU style issues) copy the stack
|
|
// now and use the same buffer repeatedly. NB this is currently never freed,
|
|
// but it is irrelevant as PoC runs on while true loop.
|
|
hHeap = GetProcessHeap();
|
|
thread.pFakeStackBuffer = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, totalStackSize);
|
|
if (!ReadProcessMemory(hProcess, (LPCVOID)ctx.Rsp, thread.pFakeStackBuffer, totalStackSize, NULL))
|
|
{
|
|
HeapFree(hHeap, NULL, thread.pFakeStackBuffer);
|
|
thread.pFakeStackBuffer = NULL;
|
|
goto Cleanup;
|
|
}
|
|
|
|
thread.dwPid = pid;
|
|
thread.dwTid = tid;
|
|
thread.totalRequiredStackSize = totalStackSize;
|
|
|
|
bMatch = TRUE;
|
|
|
|
Cleanup:
|
|
CloseHandle(hThread);
|
|
return bMatch;
|
|
}
|
|
|
|
//
|
|
// Enumerates threads across the system, locates one in a desired wait state, and copies its call stack.
|
|
//
|
|
// Based on: https://github.com/thefLink/Hunt-Sleeping-Beacons/blob/main/source/Hunt-Sleeping-Beacons.c
|
|
NTSTATUS InitialiseDynamicCallStackSpoofing(const ULONG waitReason, threadToSpoof& thread)
|
|
{
|
|
NTSTATUS status = STATUS_UNSUCCESSFUL;
|
|
|
|
ULONG uBufferSize = 0;
|
|
PVOID pBuffer = NULL;
|
|
pNtQuerySystemInformation myNtQuerySystemInformation = NULL;
|
|
SYSTEM_PROCESS_INFORMATION* pSystemProcessInformation = NULL;
|
|
SYSTEM_THREAD_INFORMATION systemThreadInformation = { 0 };
|
|
|
|
// [1] Enumerate threads system wide and locate a thread with desired WaitReason.
|
|
myNtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
|
|
if (!myNtQuerySystemInformation)
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
status = myNtQuerySystemInformation(SystemProcessInformation, pBuffer, uBufferSize, &uBufferSize);
|
|
if (STATUS_INFO_LENGTH_MISMATCH != status)
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
pBuffer = LocalAlloc(LMEM_FIXED, uBufferSize);
|
|
if (!pBuffer)
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
if (!NT_SUCCESS(myNtQuerySystemInformation(SystemProcessInformation, pBuffer, uBufferSize, &uBufferSize)))
|
|
{
|
|
status = STATUS_UNSUCCESSFUL;
|
|
goto Cleanup;
|
|
}
|
|
pSystemProcessInformation = (SYSTEM_PROCESS_INFORMATION*)pBuffer;
|
|
|
|
// [2] Loop over threads and attempt to find one where the last address on the
|
|
// stack is located within out target waiting function (e.g. WaitForSingleObjectEx).
|
|
while (pSystemProcessInformation && pSystemProcessInformation->NextEntryOffset)
|
|
{
|
|
BOOL bEnumThreads = true;
|
|
HANDLE hProcess = INVALID_HANDLE_VALUE;
|
|
BOOL bIsWow64 = false;
|
|
|
|
if (NULL != pSystemProcessInformation->ImageName.Buffer)
|
|
{
|
|
std::wcout << "[+] Searching process: " << pSystemProcessInformation->ImageName.Buffer << " (" << pSystemProcessInformation->ProcessId << ")" << "\n";
|
|
}
|
|
|
|
// [3] Attempt to open a handle to target process.
|
|
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pSystemProcessInformation->ProcessId);
|
|
if (!hProcess)
|
|
{
|
|
std::cout << "[-] Failed to open a handle to process: " << pSystemProcessInformation->ProcessId << "\n";
|
|
bEnumThreads = false;
|
|
}
|
|
|
|
// [4] Ignore WOW64.
|
|
if (bEnumThreads && IsWow64Process(hProcess, &bIsWow64))
|
|
{
|
|
if (bIsWow64)
|
|
{
|
|
std::cout << "[-] Ignoring WOW64\n";
|
|
bEnumThreads = false;
|
|
}
|
|
}
|
|
|
|
// [5] Enumerate threads.
|
|
if (bEnumThreads)
|
|
{
|
|
for (ULONG i = 0; i < pSystemProcessInformation->NumberOfThreads; i++)
|
|
{
|
|
systemThreadInformation = pSystemProcessInformation->ThreadInfos[i];
|
|
|
|
// Ignore any threads not in our desired wait state.
|
|
if (waitReason != systemThreadInformation.WaitReason)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// [6] Attempt to unwind the stack and check if stack is in our desired wait state.
|
|
if (IsThreadAMatch(hProcess, pSystemProcessInformation->ProcessId, (DWORD)systemThreadInformation.ClientId.UniqueThread, thread))
|
|
{
|
|
// We have found a thread to clone!
|
|
std::cout << " [+] Successfully located a thread call stack to clone!" << "\n";
|
|
std::wcout << " [+] Cloning call stack from process: " << pSystemProcessInformation->ImageName.Buffer << "\n";
|
|
std::cout << " [+] Cloning call stack from pid: " << std::dec << pSystemProcessInformation->ProcessId << "\n";
|
|
std::cout << " [+] Cloning call stack from tid: " << std::dec << (DWORD)systemThreadInformation.ClientId.UniqueThread << "\n";
|
|
std::cout << " [+] Target thread start address is: 0x" << std::hex << thread.startAddr << "\n";
|
|
std::cout << " [+] Total stack size required: 0x" << thread.totalRequiredStackSize << "\n";
|
|
status = STATUS_SUCCESS;
|
|
CloseHandle(hProcess);
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
}
|
|
// Avoid leaking handles.
|
|
if (hProcess)
|
|
{
|
|
CloseHandle(hProcess);
|
|
}
|
|
pSystemProcessInformation = (SYSTEM_PROCESS_INFORMATION*)((LPBYTE)pSystemProcessInformation + pSystemProcessInformation->NextEntryOffset);
|
|
}
|
|
|
|
// [7] If we reached here we did not find a suitable thread call stack to spoof.
|
|
std::cout << "[!] Could not find a suitable callstack to clone.\n";
|
|
|
|
Cleanup:
|
|
LocalFree(pBuffer);
|
|
return status;
|
|
}
|