Eternal Life?
Abstract
In recent years, there has been a shift from the fork and run model to in-process execution. This includes .NET assemblies, beacon object files, among others. Whilst this has been very beneficial OPSEC wise as you avoid the many detection points in the: spawn another process -> inject -> capture output
model, it comes at a price. A subtle bug in a post-exploitation tool can cause the process hosting our beacon to become unstable or even crash. This leaves us one unhandled exception away from death.
One might be forgiven for adopting a cynical stance and asserting that users are ultimately responsible for the post-exploitation tooling they execute. However, this point of view is arguably reductivistic. The following post will exemplify how one might alleviate these issues by constructing a harness.
Eternal Life?...Probably Not
The kernel dispatches exceptions to user mode by first copying the exception record, exception frame and trap frame contents to the user mode stack. Subsequently, the return address in the trap frame is set to the routine responsible for raising an exception, specifically ntdll!KiUserDispatchException
. This implies a replacement for the conventional return address, which would typically be a system call stub exported by NTDLL. As a result, execution will resume on this routine when the thread returns to user mode.
// Courtesy of the WRK
TrapFrame->Rip = (ULONG64)KeUserExceptionDispatcher;
To intercept this process, we can hook KiUserDispatchException
. The hook in question should be placed on the thread that is responsible for executing the in-process tool. Please refer to this post if you are confused about how you might go about achieving this.
We then of course need to register our exception handler, which will be responsible for unwinding the stack of the thread. At this point, it may be possible to find a handler to mitigate the exception. Unfortunately, this is not a fruitful approach, as it is difficult to exclude "handlers" that result in the process crashing.
Nonetheless, it is possible to free RX memory that may have been allocated and executed along the way. The following example does just that, with the presumption that the stack has not been tampered with... rendering it unreliable.
ULONG EternalLifeHandler(IN PEXCEPTION_POINTERS ExceptionInfo)
{
CONST PCONTEXT Context{ ExceptionInfo->ContextRecord };
if (CONST PEXCEPTION_RECORD Exception = ExceptionInfo->ExceptionRecord
;
Exception->ExceptionCode == static_cast<ULONG>(EXCEPTION_SINGLE_STEP) && Exception->ExceptionAddress == KiUserDispatchException)
{
PVOID HandlerData {};
ULONG64 EstablisherFrame{};
KNONVOLATILE_CONTEXT_POINTERS ContextPointers {};
for (ULONG Frame = 0;
;
Frame++)
{
DWORD64 ImageBase {};
UNWIND_HISTORY_TABLE UnwindHistoryTable{};
if(CONST PRUNTIME_FUNCTION RuntimeFunction{ RtlLookupFunctionEntry(Context->Rip, &ImageBase, &UnwindHistoryTable) }
;
RuntimeFunction == nullptr)
{
//
// The function is either a leaf function, or we are not executing a PE (e.g. shellcode)
//
CONST PVOID ReturnAddress{ *reinterpret_cast<PVOID*>(Context->Rsp) };
Context->Rip = reinterpret_cast<ULONG_PTR>(ReturnAddress);
Context->Rsp += sizeof(PVOID);
}
else
{
RtlVirtualUnwind(UNW_FLAG_NHANDLER, ImageBase, Context->Rip, RuntimeFunction, Context, &HandlerData, &EstablisherFrame, &ContextPointers);
}
if(Context->Rip == 0)
{
//
// All memory has been cleaned on the way, so it's time to terminate the thread
//
NtTerminateThread(SurrogateThread, STATUS_SUCCESS);
break;
}
MEMORY_BASIC_INFORMATION MemoryInfo{};
NTSTATUS Status{ NtQueryVirtualMemory(NtCurrentProcess(), *reinterpret_cast<PVOID*>(Context->Rip), MemoryBasicInformation, &MemoryInfo, sizeof(MEMORY_BASIC_INFORMATION), nullptr) };
if(NT_SUCCESS(Status))
{
continue;
}
if (MemoryInfo.AllocationProtect == PAGE_EXECUTE
&&
MemoryInfo.State == MEM_COMMIT
&&
MemoryInfo.Type == MEM_PRIVATE)
{
NtFreeVirtualMemory(NtCurrentProcess(), &MemoryInfo.AllocationBase, 0, MEM_RELEASE);
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
A better approach
It is possible that the code could create another thread that would obviously not be hooked. Fortunately, we can provide maximum coverage by using RtlSetUnhandledExceptionFilter
to replace the top-level exception handler of each thread in a process. This is optimal, as it:
- Does not involve hooks
- Covers all threads in the process
- Will only be called if the exception makes it to the unhandled exception filter
RtlSetUnhandledExceptionFilter(EternalLifeHandler);
Conclusion
Initially, I planned to free any memory backing the code, as a form of cleanup. This has proved to be unreasonable. Consider the following example: a tool performs module stomping on chakra.dll
. It is exceptionally challenging to ascertain whether:
- The thread called a function from
chakra.dll
, which has been loaded as a result of being a dependency of the main executable - The thread loaded
chakra.dll
as a sacrificial DLL
Such an approach may, conversely, precipitate a greater degree of instability. The crux of the problem is thereby: what is the ideal course of action? An error of this magnitude will no doubt create IoCs in the memory of our process. A particularly elegant solution would be to simply migrate to another process, similar to Nighthawks' extra life.
On reflection, it becomes evident that this post is lacking in clarity in several aspects. This is partly due to the numerous implementation-specific factors involved. For example, in the case of a reflective DLL, one may walk their own DLL, save it on the heap and inject it into another process, or even re-inject it into the same process.