Saturday, 7 March 2009

Windows 2000 UEF Overwrite Oddity

After firing up an old Windows 2000 SP4 VM during the week to code up a heap overflow PoC, I came across a small oddity when attempting to gain code execution by overwriting kernel32's top level Unhandled Exception Filter (Halvar Flake - Third Generation Exploitation) after I had run Windows Update.

Previously, overwriting kernel32's top level UEF would give you control after an unhandled exception occurred, however this didn't seem to be working as it previously had done.

After digging around kernel32!SetUnhandledExceptionFilter and kernel32!UnhandledExceptionFilter in the current kernel32 version (5.00.2195.7135) and a previous one (5.00.2195.7006) it seems MS changed the way kernel32 accesses the top level UEF. Previously the UEF was set and handled as shown in the summarized snippet below:

// as seen in kernel32.dll (5.00.2195.7006)

LP_UEF pTopLevelExceptionFilter;

LP_UEF SetUnhandledExceptionFilter( LP_UEF lpNewFilter )
{
  pTopLevelExceptionFilter = lpNewFilter;
  return pTopLevelExceptionFilter;
}

DWORD UnhandledExceptionFilter( LP_EXCEPTION_POINTERS lpExceptionInfo )
{
  // ...

  if( NtQueryInformationProcess( GetCurrentProcess(), ProcessDebugPort, &dwDebugged, 4, NULL ) >= NULL )
    return 0;

  if( dwDebugged != NULL )
    return 0;

  if ( pTopLevelExceptionFilter != NULL )
  {
    dwResult = pTopLevelExceptionFilter( lpExceptionInfo );
    if( dwResult == 1 || dwResult == -1 )
      return dwResult;
  }

  // ...
}

We can see that as long as the current process is not being debugged and the top level UEF is not NULL it will be executed, so if we patch the UEF address before an exception we can get control. However the latest kernel32 sets and handles its top level UEF differently as shown in the summarized snippet below:

// as seen in kernel32.dll (5.00.2195.7135)

typedef struct _module_info
{
  HANDLE hModule; // The base address of the module which owns the UEF
  DWORD Type; // The UEF's memory region Type as set by VirtualQuery()
  DWORD RegionSize;// The UEF's memory region size as set by VirtualQuery()
  WCHAR ModuleFileName[260]; // The name of the module which owns the UEF
} ModuleInfo;

LP_UEF pTopLevelExceptionFilter;

ModuleInfo SavedUEF;

ModuleInfo * pCurrentEUF;

LP_UEF SetUnhandledExceptionFilter( LP_UEF lpNewFilter )
{
  ModuleInfo NewUEF;

  // ...

  // get some meta data about this new UEF
  GetExceptionFilterModuleInfo( lpNewFilter, &NewUEF ) );

  // ...

  // alloc this buffer for use later in UnhandledExceptionFilter()
  pCurrentEUF = RtlAllocateHeap( hHeapHandle, dwFlags, 532 );

  // save this meta data for later
  memcpy( &SavedUEF, &NewUEF, 532 );

  // set the new UEF
  pTopLevelExceptionFilter = lpNewFilter;

  // ...
}

DWORD UnhandledExceptionFilter( LP_EXCEPTION_POINTERS lpExceptionInfo )
{
  // ...

  if( NtQueryInformationProcess( GetCurrentProcess(), ProcessDebugPort, &dwDebugged, 4, NULL ) >= NULL )
    return 0;

  if( dwDebugged != NULL )
    return 0;

  if( pTopLevelExceptionFilter != NULL )
  {
    if( pCurrentEUF != NULL )
    {
      // get the meta data for the current UEF
      if( GetExceptionFilterModuleInfo( pTopLevelExceptionFilter, pCurrentEUF ) )
      {
        // compare the current meta data to the saved meta data
        if( pCurrentEUF->hModule == SavedUEF.hModule )
        {
          if( pCurrentEUF->RegionSize == SavedUEF.RegionSize )
          {
            if( pCurrentEUF->Type == SavedUEF.Type )
            {
              if( pCurrentEUF->Type & SEC_IMAGE )
              {
                bExecute = wcscmp( pCurrentEUF->ModuleFileName, SavedUEF.ModuleFileName) == 0;
              }
              else
              {
                bExecute = TRUE;
              }
              if( bExecute )
              {
                result = pTopLevelExceptionFilter( lpExceptionInfo );
                if ( result == 1 || result == -1 )
                  return result;
              }
            }
          }
        }
      }
    }
  }
  // ...
}

We can see that when an application now sets a top level UEF via SetUnhandledExceptionFilter(), some meta data about the new UEF is recorded, including the module name where the new UEF lies, the module handle (equal to the modules base address), as well as the size and type of the memory block where the UEF lives, as returned by a call to VirtualQuery().

Later when UnhandledExceptionFilter() attempts to execute the top level UEF it first double checks that the saved UEF meta data is equal to the current UEF's meta data. If it is the top level UEF is called.

A problem then arises after we patch kernel32's top level UEF. When UnhandledExceptionFilter() is called the meta data won't match up and our patched UEF will not be called. We could look for a suitable address in the same memory region as the original one so as to satisfy these checks, however there is an easier way around it.

By default most processes have their kernel32's top level UEF set to msvcrt!__CxxUnhandledExceptionFilter. This function in turn will call an exception filter whose address is stored in a fixed location (0x7803A148 in msvcrt.dll 6.10.9844.0). Simply patching this location with our arbitrary address will allow us to ignore the checks in kernel32 and gain control after an unhandled exception. For a typical heap overflow we can get back to our shellcode via a 'CALL [ESI+0x4C]'.