📜 ⬆️ ⬇️

Exceptions for hardcore. Features of Executing Processing in Dynamically Hosted Code

image

Modern versions of the operating system impose restrictions on the executable code associated with security requirements. Under such conditions, the use of an exclusion mechanism in an injected code or, say, in a manually projected image can be a nontrivial task if you are not aware of certain nuances. This article will focus on the internal structure of the user-mode exception manager of the Windows OS for x86 / x64 / IA64 platforms, and will also consider options for implementing the circumvention of system constraints.

__ try


Suppose that in your practice a problem arose that requires the implementation of full-fledged exception handling in the code implemented in a foreign process, or you make another PE-packer / cryptor, which should ensure that the exceptions in the unpacked image work. Anyway, it all comes down to the fact that the code that uses exceptions is executed outside the image projected by the system loader, which will be the main reason for the difficulties. As a demonstration of the problem, consider a simple code example that copies its own image to a new area within the current AP process:

void exceptions_test() { __try { int *i = 0; *i = 0; } __except (EXCEPTION_EXECUTE_HANDLER) { /*       */ MessageBoxA(0, " ", "", 0); } } void main() { /*    */ exceptions_test(); /*        */ PVOID ImageBase = GetModuleHandle(NULL); DWORD SizeOfImage = RtlImageNtHeader(ImageBase)->OptionalHeader.SizeOfImage; PVOID NewImage = VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(NewImage, ImageBase, SizeOfImage); /*   */ ULONG_PTR Delta = (ULONG_PTR) NewImage - ImageBase; RelocateImage(NewImage, Delta); /*  exceptions_test    */ void (*new_exceptions_test)() = (void (*)()) ((ULONG_PTR) &exceptions_test + Delta); new_exceptions_test(); } 

In the procedure exceptions_test, an attempt to access the null pointer is wrapped in the MSVC extension try-except, instead of the exception filter, a stub returning EXCEPTION_EXECUTE_HANDLER, which should immediately lead to the execution of the code in the except block. When you first call exceptions_test, it works as expected: the exception is intercepted, the message box is displayed. But after copying the code to a new location and calling the copy of exceptions_test, the exception is no longer processed, and the application simply “crashes” with the message about the unhandled exception that is specific to the OS version. The specific reason for this behavior will depend on the platform on which the test was conducted, and to determine it, it will be necessary to deal with the mechanism for dispatching exceptions.
')

Raw exception

Scheduling exceptions


Regardless of the platform and type of exception, dispatching for user-mode always starts from the KiUserExceptionDispatcher point in the ntdll module, which is controlled from the KiDispatchException core dispatcher (if the exception was raised from user-mode and was not handled by the debugger). In the example above, control is transferred to the dispatcher for both cases of an exception (during the execution of exceptions_test and its copy at a new address), you can verify this by setting a breakpoint to ntdll! KiUserExceptionDispatcher. The KiUserExceptionDispatcher code is very simple and looks something like this:

 VOID NTAPI KiUserExceptionDispatcher (EXCEPTION_RECORD *ExceptionRecord, CONTEXT *Context) { NTSTATUS Status; if (RtlDispatchException(ExceptionRecord, Context)) { /*  ,    */ Status = NtContinue(Context, FALSE); } else { /*   ,         */ Status = NtRaiseException(ExceptionRecord, Context, FALSE); } ... RtlRaiseException(&NestedException); } 

where EXCEPTION_RECORD is the structure with information about the exception, and CONTEXT is the structure of the stream context state at the time of the exception. Both structures are documented in MSDN, however, you are probably already familiar with them. Pointers to this data are passed to ntdll! RtlDispatchException, where the actual dispatching takes place, and in 32-bit and 64-bit systems the mechanics of exception handling is different.

x86


The main mechanism for the x86 platform is Structured Exception Handling (SEH), based on a single-linked list of exception handlers located on the stack and always accessible from NT_TIB.ExceptionList. The basics of this mechanism have been repeatedly described in a variety of works (see the sidebar “Useful Materials”), so we will not repeat, but only focus on those points that intersect with our task.


SEH chain dump

The fact is that in SEH all elements of the handler list must be on the stack, which means that they are potentially subject to rewriting when the buffer in the stack overflows. That was exploited with success by the creators of the exploits: the pointer to the handler was overwritten by the address needed to execute the shell-code, while the pointer was also rewritten to the next list item, which led to a violation of the handler chain. To increase the resilience of attacks against programs using SEH, Microsoft developed mechanisms such as SafeSEH (a table with addresses of "safe" handlers, located in the IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG PE file), SEHOP (simple check of the integrity of the chain of frames), and also integrated the corresponding system policy DEP checks performed during the dispatching process of an exception.

Simplified pseudo-code for the main dispatching procedure RtlDispatchException for the x86 version of the ntdll.dll library in Windows 8.1 can be represented (with some assumptions) as follows:

 void RtlDispatchException(...) // NT 6.3.9600 { /*   Vectored Exception Handlers */ if (RtlpCallVectoredHandlers(exception, 1)) return 1; ExceptionRegistration = RtlpGetRegistrationHead(); /* ECV (SEHOP) */ if (!DisableExceptionChainValidation && !RtlpIsValidExceptionChain(ExceptionRegistration, ...)) { if (_RtlpProcessECVPolicy != 2) goto final; else RtlReportException(); } /*   ,     */ while (ExceptionRegistration != EXCEPTION_CHAIN_END) { /*    */ if (!STACK_LIMITS(ExceptionRegistration)) { ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID; goto final; } /*   */ if (!RtlIsValidHandler(ExceptionRegistration, ProcessFlags)) goto final; /*    */ RtlpExecuteHandlerForException(..., ExceptionRegistration->Handler); ... ExceptionRegistration = ExceptionRegistration->Next; } ... final: /*   Vectored Continue Handlers */ RtlpCallVectoredHandlers(exception, 1); } 

From the presented pseudo-code, it can be concluded that in order to successfully transfer control to the SEH handler, the following conditions must be met when dispatching an exception:

  1. The chain of SEH-frames must be correct (end with the ntdll! FinalExceptionHandler handler). Verification is performed with SEHOP enabled for the process.
  2. The SEH frame must be on the stack.
  3. The SEH frame must contain a pointer to a valid handler.

INFO


For Vectored Exception Handling, no checks are performed in the manager, which makes VEH a suitable tool when there is no need to bother with SEH support in the program.


Call stack for exception filter

If everything is very clear with the first two points and no additional actions are required to perform them, then we will examine the procedure for checking the handler for “validity” in more detail. Handler checks are performed with the ntdll! RtlIsValidHandler function, whose pseudo code for Vista SP1 was first introduced to the general public back in 2008 at the Black Hat conference in the States. Even though he contained some inaccuracies, this did not prevent him from roaming as a copy-paste from one resource to another for several years. Since then, the code of this function has not undergone significant changes, and the analysis of its version for Windows 8.1 allowed us to compile the following pseudo-code:

 BOOL RtlIsValidHandler(Handler) // NT 6.3.9600 { if (/* Handler    */) { if (DllCharacteristics&IMAGE_DLLCHARACTERISTICS_NO_SEH) goto InvalidHandler; if (/*   .Net ,  ILonly  */) goto InvalidHandler; if (/*   SafeSEH */) { if (/*    LdrpInvertedFunctionTable (  ),      */) { if (/* Handler    SafeSEH */) return TRUE; else goto InvalidHandler; } return TRUE; } else { if (/* ExecuteDispatchEnable  ImageDispatchEnable    ExecuteOptions  */) return TRUE; if (/* Handler      */) { if (ExecuteDispatchEnable) return TRUE; } else if (ImageDispatchEnable) return TRUE; } InvalidHandler: RtlInvalidHandlerDetected(...); return FALSE; } 

In the above pseudocode, the order of checking conditions is changed (in the original, some conditions are checked twice, some are checked in nested functions). After analyzing the pseudo-code, we can conclude that for successful validation, one of the sets of conditions under which the handler belongs must be fulfilled:


At the same time, a memory area is considered to be the case if the flag MEM_IMAGE is set for it in attributes of the region (attributes are obtained by the NtQueryVirtualMemory function), and the content corresponds to the PE structure. Process flags are obtained by the NtQueryInformationProces function from KPROCESS.KEXECUTE_OPTIONS. Based on the information received, at least three ways can be distinguished for the implementation of support for exceptions in dynamically allocated code on the x86 platform:

  1. Setting / changing the ImageDispatchEnable flag for the process.
  2. Replacing the type of memory region with MEM_IMAGE (for a PE image without SafeSEH).
  3. Implementing your own exception manager bypassing all checks.

Each of these options will be discussed in detail below. We should also mention SafeSEH support, which may be necessary if you write, for example, a regular legal PE packer or protector. To implement it, you will have to take care of manually adding the record of the mapped image (with a pointer to SafeSEH) to the global table ntdll! LdrpInvertedFunctionTable, while functions that work with this table directly are not exported by the library ntdll.dll and look for them manually a little: in old OS, they still require a pointer to the table itself. Having found the pointer in any way, you will also have to take care of blocking access to the table for safe changes. An alternative would be to unpack the file into one of the sections of the unpacker and transfer the SafeSEH table from the unpacked file to the main image. Unfortunately, a detailed review of these and other techniques is beyond the scope of this article, here options are considered that do not imply the support of SafeSEH (by the way, this table can always be simply reset).

Substitution ExecuteOptions process


ExecuteOptions (KEXECUTE_OPTIONS) is the part of the KPROCESS kernel structure that contains the DEP settings for the process. The structure has the form:

 typedef struct _KEXECUTE_OPTIONS { UCHAR ExecuteDisable : 1; UCHAR ExecuteEnable : 1; UCHAR DisableThunkEmulation : 1; UCHAR Permanent : 1; UCHAR ExecuteDispatchEnable : 1; UCHAR ImageDispatchEnable : 1; UCHAR Spare : 2; } KEXECUTE_OPTIONS, PKEXECUTE_OPTIONS; 


Process ExecuteOptions with DEP Enabled

The values ​​of these settings (flags) at the user level are obtained by the NtQueryInformationProcess function with the information class parameter equal to 0x22 (ProcessExecuteFlags). Flags are set in the same way with the NtSetInformationProcess function. Starting with Vista SP1, for processes with DEP enabled, the Permanent flag is set by default, which prohibits making changes to settings after the process is initialized. A fragment of the KeSetExecuteOptions procedure called in kernel mode from NtSetInformationProcess confirms this:

 @PermanentCheck: ; KeSetExecuteOptions +2Fh mov al, [edi+6Ch] ; current KEXECUTE_OPTIONS mov byte ptr [ebp+arg_0+3], al test al, 8 ; test Permanent jnz short @Fail ;  0C0000022h (STATUS_ACCESS_DENIED) 

Thus, being in user-mode, ExecuteOptions with activated DEP will not be able to change. But the only option is to simply “trick” RtlIsValidHandler by setting the hook to NtQueryInformationProcess, where the flags will be replaced with the ones you need. Installing such an interception will make the exceptions in the code located outside the modules loaded by the system workable. Sample interceptor code:

 NTSTATUS __stdcall xNtQueryInformationProcess(HANDLE ProcessHandle, INT ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength) { NTSTATUS Status = org_NtQueryInformationProcess(ProcessHandle, ProcessInformationClass, ProcessInformation, ProcessInformationLength, ReturnLength); if (!Status && ProcessInformationClass == 0x22) /* ProcessExecuteFlags */ *(PDWORD)ProcessInformation |= 0x20; /* ImageDispatchEnable */ return Status; } 

Memory attribute swapping


An alternative option for changing the process flags is to replace the attributes of the memory region in which the handler is located. As already noted, RtlIsValidHandler checks the type of allocated memory area, and, if it matches MEM_IMAGE, the area is considered to be. It is impossible to assign MEM_IMAGE to the selected VirtualAlloc area, this type can only be set to display the section (NtCreateSection) for which the correct file handle is specified. Just as with the ExecuteOptions substitution, you will need interception, this time the NtQueryVirtualMemory functions:

 NTSTATUS NTAPI xNtQueryVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, INT MemoryInformationClass, PMEMORY_BASIC_INFORMATION MemInformation, ULONG Length, PULONG ResultLength) { NTSTATUS Status = org_NtQueryVirtualMemory(ProcessHandle, BaseAddress, MemoryInformationClass, Buffer, Length, ResultLength); if (!Status && !MemoryInformationClass) /* MemoryBasicInformation */ { if((UINT_PTR)MemInformation->AllocationBase == g_ImageBase) MemInformation->Type = MEM_IMAGE; } return Status; } 

The method is suitable for exceptions when injecting a PE image entirely or for manually mapped images. In addition, this option is somewhat more preferable than the previous one, if only because it does not reduce the security of the process by partially disabling DEP (you don’t need additional malware?). As a bonus, this method allows you to pass an internal check of the handler in modern versions of CRT using try-except and try-finally constructs (these constructs can also be used without CRT, for more details see the corresponding box). The CRT check is performed by the __ValidateEH3RN function, called from _except_handler3, it assumes the specified MEM_IMAGE type for the region, as well as the correct PE structure.

Own exception manager


If the hook installation options are unsuitable for any reason or simply do not like, you can go further and completely replace the dispatching of SEH with your own code by implementing all the necessary logic of the SEH controller inside the vector handler. From the above pseudo-code RtlDispatchException, it is clear that VEH is called before the processing of the SEH chain begins. Nothing prevents to take control over the exception by the vector handler and decide for yourself what to do with it and what handlers to call for it. The VEH handler is installed with just one line:

 AddVectoredExceptionHandler(0, (PVECTORED_EXCEPTION_HANDLER) &VectoredSEH); 

where VectoredSEH is a handler, which is actually an SEH dispatcher. The complete call chain for this handler will look like this: KiUserExceptionDispatcher -> RtlDispatchException -> RtlpCallVectoredHandlers -> VectoredSEH. In this case, the control of the calling function may not be returned, but you can call NtContinue or NtRaiseException yourself, depending on the success of the dispatch. The complete source code for the SEH implementation via VEH can be found in the materials attached to the article, or on GitHub . The implementation code is fully working, and the dispatch logic corresponds to the system one.


SEH dispatcher inside the vector handler

x64 and IA64


In 64-bit versions of Windows, the x64 and Itanium platforms use a completely different way to handle exceptions than the x86 versions. The method is based on tables containing all the information necessary for dispatching an exception, including offsets of the beginning and end of the code block for which the exception is processed. Therefore, in the code compiled for these platforms, there are no operations to install and remove a handler for each try-except block. The static exception table is located in the Exception Directory of the PE file and is an array of elements of the RUNTIME_FUNCTION structures, which look like this:

 typedef struct _RUNTIME_FUNCTION { ULONG BeginAddress; ULONG EndAddress; ULONG UnwindData; } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION; 

Pleasant moment: support for exceptions for dynamic code is implemented at the system level. If the code is in a memory area that is not an image, or there is no exception table generated by the compiler in this image, then the information for exception handling is taken from dynamic exception tables (DynamicFunctionTable). The pointer to the list is stored in ntdll! RtlpDynamicFunctionTable, several functions for working with the list are exported from ntdll.dll. A cursory analysis of the listings of these functions allowed us to obtain the following structure of the elements of the DynamicFunctionTable list:

 struct _DynamicFunctionTable { /* +0h */ PVOID Next; PVOID Prev; //       /* +10h */ PRUNTIME_FUNCTION Table;//   ,      ID|0x03 PVOID TimeCookie; // ZwQuerySystemTime /* +20h */ PVOID RegionStart; //   BaseAddress DWORD RegionLength; //   ()  /* +30h */ DWORD64 BaseAddress; PGET_RUNTIME_FUNCTION_CALLBACK Callback; /* +40h */ PVOID Context; //     DWORD64 CallbackDll; //   +58h,  DLL  /* +50h */ DWORD Type; // 1 — table, 2 — callback DWORD EntryCount; WCHAR DllName[1]; }; 


RUNTIME_FUNCTION search algorithm

Elements are added by the RtlAddFunctionTable and RtlInstallFunctionTableCallback functions, removed by the RtlDeleteFunctionTable. All of these features are well documented in MSDN and are very easy to use. An example of adding a dynamic table for a newly-mapped image

 ULONG Size, Length; /*  ,  ,    */ PRUNTIME_FUNCTION Table = (PRUNTIME_FUNCTION) RtlImageDirectoryEntryToData(NewImage, TRUE, IMAGE_DIRECTORY_ENTRY_EXCEPTION, &Size); Length = Size/sizeof(PRUNTIME_FUNCTION); /*      DynamicFunctionTable */ RtlAddFunctionTable(Table, Length, (UINT_PTR)NewImage); 

That's all, no hooks or own exception controllers, no bypass of system checks. It is only worth noting that the DynamicFunctionTable is global for the process, so if the code for which the record is added has worked and should be deleted, then the corresponding record from the table should also be removed. Instead of adding a table, you can set a callback for a specific address range in the UA, which will receive control every time a RUNTIME_FUNCTION entry is required for code from this area. The version with the installation of the callback, see the source code attached to the article.


Exception processed

__finally


Low-level programming under Windows using the native API does not impose exceptions as an error handling method, and developers of “specific software” are often either simply ignored or limited to installing an unhandled exception filter or simply using VEH. Nevertheless, exceptions still remain a powerful mechanism by which you can extract the greater the gain, the more complex the architecture of your program. And thanks to the methods discussed in the article, you will be able to enjoy exceptions even in the most extraordinary conditions.

Useful materials



Also I recommend to get Windows Research Kernel (the main part of the NT5.2 kernel sources). WRK is distributed to universities and academic organizations, but not for me to teach you how and where to look for such things.

Try-except and try-finally constructs without CRT


If you are going to use constructions of blocks of exceptions and finalization, then you should take care of the presence in the program of a procedure that the compiler substitutes for the real handler: for x86 projects this is __except_handler3, and for x64 it is __C_specific_handler. In these procedures, our own dispatching is performed: the search and calling of the necessary handlers, as well as the promotion of the stack. There is no special need to write them yourself, for the x86 project you can simply connect expsup3.lib from the old DDK (ntdll.lib from the DDK also contains necessary functions), for x64 it is still easier: __C_specific_handler is exported by the 64-bit version of ntdll.dll, just use the correct lib file.

image

First published in Hacker Magazine # 195.
Posted by: Teq

Subscribe to "Hacker"

Source: https://habr.com/ru/post/260577/


All Articles