mirror of
https://github.com/jdu2600/Get-InjectedThreadEx
synced 2026-06-08 14:59:49 +00:00
BSides Canberra 2023 (#2)
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
// Reference:
|
||||
// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64
|
||||
//
|
||||
// Other useful references:
|
||||
// https://codemachine.com/articles/x64_deep_dive.html
|
||||
// http://www.uninformed.org/?v=4&a=1 Improving Automated Analysis of Windows x64 Binaries
|
||||
// http://www.nynaeve.net/?p=113 Programming against the x64 exception handling support
|
||||
// https://www.sciencedirect.com/science/article/pii/S1742287618300458 Building stack traces from memory dump of Windows x64
|
||||
// https://github.com/reactos/reactos/blob/master/sdk/lib/rtl/amd64/unwind.c
|
||||
// https://auscitte.github.io/posts/Exception-Directory-pefile Boots for Walking Backwards: Teaching pefile How to Understand SEH-Related Data in 64-bit PE Files
|
||||
// https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs
|
||||
|
||||
#include "Get-InjectedThreadEx.h"
|
||||
|
||||
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_EPILOG, /* added in v2. UNDOCUMENTED. */
|
||||
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 dwError-code, 1: dwError-code */
|
||||
} UNWIND_CODE_OPS;
|
||||
|
||||
// from ehdata.h
|
||||
#pragma warning (push)
|
||||
#pragma warning (disable: 4201)
|
||||
typedef union _UNWIND_CODE {
|
||||
struct {
|
||||
unsigned char CodeOffset;
|
||||
unsigned char UnwindOp : 4;
|
||||
unsigned char OpInfo : 4;
|
||||
};
|
||||
unsigned short FrameOffset;
|
||||
} UNWIND_CODE, * PUNWIND_CODE;
|
||||
typedef struct _UNWIND_INFO {
|
||||
unsigned char Version : 3;
|
||||
unsigned char Flags : 5;
|
||||
unsigned char SizeOfProlog;
|
||||
unsigned char CountOfCodes;
|
||||
unsigned char FrameRegister : 4;
|
||||
unsigned char FrameOffset : 4;
|
||||
UNWIND_CODE UnwindCode[1];
|
||||
/* UNWIND_CODE MoreUnwindCode[((CountOfCodes+1)&~1)-1];
|
||||
* union {
|
||||
* OPTIONAL unsigned long ExceptionHandler;
|
||||
* OPTIONAL unsigned long FunctionEntry;
|
||||
* };
|
||||
* OPTIONAL unsigned long ExceptionData[];
|
||||
*/
|
||||
} UNWIND_INFO, * PUNWIND_INFO;
|
||||
#pragma warning (pop)
|
||||
|
||||
constexpr auto W_FPREG = 1; // WARNING - frame pointer in use;
|
||||
static HRESULT CalculateFrameSize_Internal(const PRUNTIME_FUNCTION pRuntimeFunction, const DWORD64 ImageBase, const DWORD rva, PDWORD pFrameSize)
|
||||
{
|
||||
HRESULT status = S_OK;
|
||||
|
||||
const auto pUnwindInfo = (PUNWIND_INFO)(pRuntimeFunction->UnwindInfoAddress + ImageBase);
|
||||
if (!pUnwindInfo || pUnwindInfo->Version > 2) {
|
||||
LogError("UNWIND_INFO v%d not supported", pUnwindInfo->Version);
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
const auto codeOffset = rva - pRuntimeFunction->BeginAddress;
|
||||
|
||||
// Loop over unwind codes and calculate total stack space used by target function.
|
||||
BYTE i = 0;
|
||||
while (i < pUnwindInfo->CountOfCodes)
|
||||
{
|
||||
// Warning: This implementation is not complete and may not work for some edge cases.
|
||||
// For example, it does not handle handle RIP pointing into an epilog.
|
||||
// But that should be rare for our early frames use case.
|
||||
|
||||
const auto unwindOperation = pUnwindInfo->UnwindCode[i].UnwindOp;
|
||||
const auto operationInfo = pUnwindInfo->UnwindCode[i].OpInfo;
|
||||
const auto bApplyOperation = codeOffset > pUnwindInfo->UnwindCode[i].CodeOffset;
|
||||
|
||||
i++;
|
||||
switch (unwindOperation) {
|
||||
case UWOP_PUSH_NONVOL:
|
||||
if (bApplyOperation)
|
||||
(*pFrameSize)++;
|
||||
break;
|
||||
case UWOP_ALLOC_LARGE:
|
||||
switch (operationInfo) {
|
||||
case 0:
|
||||
if (bApplyOperation)
|
||||
*pFrameSize += pUnwindInfo->UnwindCode[i].FrameOffset;
|
||||
i++;
|
||||
break;
|
||||
case 1:
|
||||
if (bApplyOperation)
|
||||
*pFrameSize += *(DWORD*)(&pUnwindInfo->UnwindCode[i]) / sizeof(PVOID);
|
||||
i += 3;
|
||||
break;
|
||||
default:
|
||||
LogError("UWOP_ALLOC_LARGE operationInfo is invalid: (%d)", operationInfo);
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
break;
|
||||
case UWOP_ALLOC_SMALL:
|
||||
if (bApplyOperation)
|
||||
*pFrameSize += operationInfo + 1;
|
||||
break;
|
||||
case UWOP_SET_FPREG:
|
||||
if (bApplyOperation) {
|
||||
// Frame pointer in use. Our frame size calculation is inaccurate if alloca() was used.
|
||||
// https://learn.microsoft.com/en-us/cpp/build/stack-usage
|
||||
// > If space is dynamically allocated (alloca) in a function, then a
|
||||
// > nonvolatile register must be used as a frame pointer to mark the
|
||||
// > base of the fixed part of the stack and that register must be saved
|
||||
// > and initialized in the prolog.
|
||||
*pFrameSize = pUnwindInfo->FrameOffset * 2;
|
||||
status = W_FPREG; // This is a WARNING code - not an ERROR
|
||||
}
|
||||
break;
|
||||
case UWOP_SAVE_NONVOL:
|
||||
case UWOP_SAVE_XMM128:
|
||||
case UWOP_EPILOG:
|
||||
i++;
|
||||
break;
|
||||
case UWOP_SAVE_NONVOL_FAR:
|
||||
case UWOP_SAVE_XMM128_FAR:
|
||||
i += 2;
|
||||
break;
|
||||
case UWOP_PUSH_MACHFRAME:
|
||||
if (bApplyOperation) {
|
||||
*pFrameSize += 5 + operationInfo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (bApplyOperation) {
|
||||
LogError("UNWIND_INFO operation is not implemented: %d", unwindOperation);
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (0 != (UNW_FLAG_CHAININFO & pUnwindInfo->Flags)) {
|
||||
const auto pPrimaryUwindInfo = (PRUNTIME_FUNCTION) & (pUnwindInfo->UnwindCode[(pUnwindInfo->CountOfCodes + 1) & ~1]);
|
||||
return CalculateFrameSize_Internal(pPrimaryUwindInfo, ImageBase, rva, pFrameSize);
|
||||
}
|
||||
|
||||
// Add the size of the return address.
|
||||
*pFrameSize += 1;
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static_assert(!IS_ERROR(W_FPREG));
|
||||
static_assert(!FAILED(W_FPREG));
|
||||
// Calculates the total stack space (in PVOIDs) used by the stack frame.
|
||||
HRESULT CalculateFrameSize(PVOID returnAddress, PDWORD pFrameSize, const std::wstring& filepath, PVOID remoteBase)
|
||||
{
|
||||
// Check a cache of UNWIND_INFO lookups first.
|
||||
static std::unordered_map<PVOID, DWORD> frameSizes;
|
||||
const auto it = frameSizes.find(returnAddress);
|
||||
if (it != frameSizes.end()) {
|
||||
*pFrameSize = it->second;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const DWORD rva = (DWORD)((ULONG_PTR)returnAddress - (ULONG_PTR)remoteBase);
|
||||
PRUNTIME_FUNCTION pRuntimeFunction = NULL;
|
||||
DWORD64 localBase;
|
||||
|
||||
auto hModule = GetModuleHandleW(filepath.c_str());
|
||||
if (hModule == remoteBase) {
|
||||
// Image is already loaded in our process - lock it in memory now.
|
||||
hModule = LoadLibraryW(filepath.c_str());
|
||||
if (NULL == hModule) {
|
||||
LogError("LoadLibrary(%S) failed", filepath.c_str());
|
||||
return E_UNEXPECTED;
|
||||
|
||||
}
|
||||
|
||||
pRuntimeFunction = RtlLookupFunctionEntry((DWORD64)returnAddress, &localBase, NULL);
|
||||
if (NULL == pRuntimeFunction)
|
||||
return E_FAIL;
|
||||
}
|
||||
else {
|
||||
// Image is not loaded - so load it now.
|
||||
// We don't want to execute anything malicious in our process - so map it now as a read-only resource.
|
||||
hModule = LoadLibraryExW(filepath.c_str(), NULL, LOAD_LIBRARY_AS_IMAGE_RESOURCE | LOAD_LIBRARY_AS_DATAFILE);
|
||||
if (NULL == hModule) {
|
||||
LogDebug("LoadLibraryEx(%S, AS_IMAGE_RESOURCE) failed", filepath.c_str());
|
||||
return E_UNEXPECTED;
|
||||
}
|
||||
localBase = (DWORD64)hModule & ~0xFFFF; // The lower bits of resource-only module handles are used as flags.
|
||||
|
||||
// Being resource-only means that we can't use the usual APIs.
|
||||
// Instead, manually walk the UNWIND_INFO in the exception directory of the image.
|
||||
ULONG indexLo = 0;
|
||||
ULONG indexHi;
|
||||
auto pTable = (PRUNTIME_FUNCTION)ImageDirectoryEntryToData((PVOID)localBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXCEPTION, &indexHi);
|
||||
if (!pTable) {
|
||||
(void)FreeLibrary(hModule);
|
||||
const auto error = GetLastError();
|
||||
if (ERROR_SUCCESS != error)
|
||||
LogError("ImageDirectoryEntryToData(%S) failed with %d", filepath.c_str(), error);
|
||||
else
|
||||
LogDebug("ImageDirectoryEntryToData(%S) - Exception Directory not found");
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
indexHi = indexHi / sizeof(RUNTIME_FUNCTION);
|
||||
while (indexHi > indexLo) {
|
||||
const ULONG indexMid = (indexLo + indexHi) / 2;
|
||||
pRuntimeFunction = &pTable[indexMid];
|
||||
if (rva < pRuntimeFunction->BeginAddress) {
|
||||
indexHi = indexMid; // search lower
|
||||
pRuntimeFunction = NULL;
|
||||
continue;
|
||||
}
|
||||
if (rva >= pRuntimeFunction->EndAddress) {
|
||||
indexLo = indexMid + 1; // search higher
|
||||
pRuntimeFunction = NULL;
|
||||
continue;
|
||||
}
|
||||
break; // found
|
||||
}
|
||||
}
|
||||
|
||||
if (!pRuntimeFunction) {
|
||||
(void)FreeLibrary(hModule);
|
||||
return E_FAIL; // leaf functions can't call further functions
|
||||
}
|
||||
|
||||
*pFrameSize = 0;
|
||||
auto status = CalculateFrameSize_Internal(pRuntimeFunction, localBase, rva, pFrameSize);
|
||||
if (S_OK == status)
|
||||
frameSizes.insert(std::make_pair(returnAddress, *pFrameSize));
|
||||
|
||||
if (!FreeLibrary(hModule))
|
||||
LogError("FreeLibrary(%S) failed", filepath.c_str());
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
BOOL IsValidCallSite(
|
||||
HANDLE hProcess,
|
||||
bool bIsWow64,
|
||||
const MEMORY_BASIC_INFORMATION& mbi,
|
||||
PVOID callsite,
|
||||
bool* bCallFound)
|
||||
{
|
||||
// Check a cache of valid callsites first
|
||||
static std::unordered_set<PVOID> validCallSites;
|
||||
if (validCallSites.contains(callsite)) {
|
||||
*bCallFound = true;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Otherwise read the preceding bytes and check for a valid call instruction
|
||||
std::string precedingBytes{};
|
||||
precedingBytes.resize(std::min(11ull, (ULONG_PTR)callsite - (ULONG_PTR)mbi.BaseAddress));
|
||||
if(!ReadProcessMemorySafely(hProcess, (PVOID)((ULONG_PTR)callsite - precedingBytes.size()), precedingBytes, mbi))
|
||||
return FALSE;
|
||||
|
||||
ZydisDecoder decoder;
|
||||
if (bIsWow64)
|
||||
(void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_COMPAT_32, ZYDIS_STACK_WIDTH_32);
|
||||
else
|
||||
(void)ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
|
||||
|
||||
*bCallFound = false;
|
||||
ZydisDecodedInstruction instruction;
|
||||
// call instructions are 2-11 bytes, but check some common lengths first
|
||||
static const std::array<int, 10> callLengthSearchOrder = { 5, 6, 7, 2, 3, 4, 8, 9, 10, 11 };
|
||||
for (const auto& lengthToCheck : callLengthSearchOrder) {
|
||||
if (lengthToCheck <= precedingBytes.size() &&
|
||||
ZYAN_SUCCESS(ZydisDecoderDecodeInstruction(&decoder, nullptr, precedingBytes.data() + precedingBytes.size() - lengthToCheck, lengthToCheck, &instruction)) &&
|
||||
lengthToCheck == instruction.length &&
|
||||
ZYDIS_MNEMONIC_CALL == instruction.mnemonic)
|
||||
{
|
||||
*bCallFound = true;
|
||||
// Note - For direct calls, we could (attempt to) improve this check by validating
|
||||
// that the call target matches the return address.
|
||||
// Though we would need to handle an Tail Call Optimised (TCO) functions...
|
||||
//
|
||||
// In practice this additional validation isn't required for our use case.
|
||||
// False positivies are rare in early stack frames.
|
||||
|
||||
// Cache the result
|
||||
if (MEM_IMAGE == mbi.Type && InSystemImageRange(callsite))
|
||||
validCallSites.insert(callsite);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
Reference in New Issue
Block a user