Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Win32 API Obscurity for I/O Blocking and Intrusion Prevention


There may be no better way to prevent computer security intrusions than to prevent malicious code from executing in the first place, but many observers suggest that such a goal is impossible to achieve in practice. Even with a fully patched OS and application code that exposes no exploitable vulnerabilities, the fact remains that code gets installed and executed on a regular basis. It's impossible to differentiate between trustworthy code and malicious code at runtime (it's hard enough to do this at install time). Improvements to architectural defenses are essential.

One idea is to erect better barriers between code and the system APIs that allow code to do anything of substance to a system once it has been installed. Many people approach information security with a fatalistic view of the impossibility of stopping code execution, and the pointlessness of doing anything else to improve security once malicious code has compromised it. This is a somewhat binary view of security, it either exists and is pristine or it has disappeared entirely. What such people often miss is the fact that code execution does not, by itself, result in any harm to a compromised system.

Various techniques can be used to layer-in defensive barriers to keep unwanted code from making use of any important system service, thus rendering unwanted code nearly irrelevant to the security of data stored on or processed by the host. Hooking APIs, in-memory or on-disk binary patching, proxy stub trampolines, wrapper binaries, code injection, virtual device drivers, interrupt chaining, and other approaches have been known for decades and each pose the possibility of introducing improved decisions at runtime about whether to allow particular code to do particular things.

What we may wish to do instead is simply rethink a fundamental premise. Do we really need a Win32 API? Having a well-known API is good for programmer productivity, but bad for security. Why not add one final step to the process of installing and executing code on a host that converts the Win32 API calls written by the programmer into calls that are compatible with an obscure, relatively unknown, or perhaps even entirely unique host API? This is what was proposed in the last article of this column, and here is a proof of concept to demonstrate the idea in practice.

Kernel-mode code running on ring 0 within ntoskrnl.exe provides implementations of many Win32 APIs. User-mode code exposes an interface to the kernel by way of interrupts that are dispatched when corresponding API calls are made. The code that triggers the interrupt for most of the Win32 API calls that require kernel mode work to be performed is found within the NTDLL.DLL module. This module can be called into directly, and has come to be known as the Native API. Blocking calls to the standard Win32 APIs would therefore not be adequate to the objective of preventing unauthorized use of system services, and quite a bit of malware has been written in the last few years that calls into the Native API directly.

The idea of a Win32 API blocker has merit, regardless. Think of it as a lightweight and relatively easy-to-deploy Address Space Layout Randomization (ASLR) technique. When an attacker or would-be intruder knows nothing about the APIs and the address locations, names, and designators used by a host, intrusion becomes far more difficult. In practice, an intruder, should they be able to execute arbitrary code at all, has to find a way to force their code to execute at ring 0 in order to bring their own API and make calls to device drivers or other low-level services that can't easily be obscured due to their close relationship to hardware or dependence on firmware.

The following code demonstrates an AppInit_DLLs module that could be adapted to expose alternative obscure API entry points for Win32 API functions. The code as-written only goes so far as to block access to chunks of the Win32 API exposed by kernel32.dll through the use of VirtualProtect calls with the PAGE_NOACCESS constant parameter, which causes the memory page containing the virtual address specified to be marked as inaccessible. No further read, write, or execute operations will be possible to the memory pages marked PAGE_NOACCESS. By examining the memory page layout of the Windows XP SP2 version of kernel32.dll it is possible to identify entire pages (typically a PC's memory page size will be 4096 bytes) of virtual memory that could be blocked without interfering with the normal operation of trusted applications. Should such applications be hijacked by the injection of malicious code, the attempts to call into blocked memory pages, to make Win32 API calls not normally made by the trusted application code, will result in the abrupt death of the process.

// Win32APIBlocker.cpp - author Jason Coombs 
// Proof of Concept Win32 API blocker via AppInit_DLLs injected code module
// Obscurity does add security -- at least when it comes to intrusion prevention.
// By making APIs obscure, and providing tools to manage that obscurity, many
// infectrusion incidents that occur in the real world can be prevented.

#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT 0x0500
#include <windows.h>

#ifdef _DEBUG
DWORD WINAPI BlockProcThread(LPVOID lpParameter);
HANDLE hBlockProcThread = 0;
#endif

BOOL APIENTRY DllMain( HANDLE hModule,
  DWORD  ul_reason_for_call,
  LPVOID lpReserved)
{
     DWORD dwBytes = 0, dwOldProtect = 0;
     SYSTEM_INFO si;
     UINT ui = 0;
     int a = 0, b = 0;
     char buf[MAX_PATH];
     char * fname = (char *)NULL;
     bool allowed = false;
     // Ideally the allow list would contain full paths,
     // but this is just a proof of concept
     // Combine this PoC with my "software seatbelt for 
     // Windows" and binary modules' hashes
     // could also be verified for the purpose 
     // of confirming that a binary is truly allowed
     // It would be better not to hard-code the 
     // list of allowed processes, obviously...
     // For this PoC chose to make as few 
     // external API calls as possible.
     #define ALLOWEDPROCS 16
     char * allow[ALLOWEDPROCS] = 
{"csrss.exe","smss.exe","winlogon.exe","logonui.exe",
      
"spoolsv.exe","services.exe","lsass.exe","notepad.exe","alg.exe","explorer.exe",
          "regedit.exe","ntvdm.exe","SYSTRAY.EXE","svchost.exe","dllhost.exe",
          "firefox.exe"};

     if(ul_reason_for_call == DLL_PROCESS_ATTACH)
     {
          dwBytes = GetModuleFileName(0,buf,MAX_PATH);
          if(dwBytes > 0)
          {
               // Block access to memory pages 
               // containing Win32 API functions.
               // Most of the really interesting 
               // I/O APIs can't be blocked at the
               // memory page level due to their 
               // proximity to crucial system APIs, but...

               // Note: Memory pages for Win32 API 
               // functions determined for Windows XP SP2

               // Assuming standard page size of 
               // 4096 bytes ... GetSystemInfo() to confirm.
               GetSystemInfo(&si);
               if(si.dwPageSize == 4096) {

               // locate final backslash
               for(ui = (dwBytes - 1);ui > 0;ui--)
               {
                    if(buf[ui] == '\\')
                    {
                         fname = &buf[ui + 1];
                         break;
               }}
               for(a = 0, b = 0; a < ALLOWEDPROCS;a++)
               {
                    // + 32 gives case insensitive comparison 
                    // on uppercase/mixed-case module names
                    while(fname[b] == allow[a][b] ||
                         ((fname[b] > 64 && fname[b] < 91) && fname[b] + 
                         32 == allow[a][b])) {
                         if(fname[b] == '\0' && allow[a][b] == '\0') {
                              allowed = true;
                              break;
                         }
                         b++;
                    }
               }

               if(!allowed)
               {
               // RVA = Relative Virtual Address 
               // (i.e. offset from runtime base relocation)

               // CreateFileA RVA 00001A24 would block
               //  memory page RVA 00001000 - 00001FFF
               // unfortunately, VirtualProtect, 
               // VirtualProtectEx and ReadFile as well as
               //   LoadLibrary and other APIs that 
               // can't easily be blocked are neighbors on
               //   the same memory page alongside 
               // CreateFileA. In particular, VirtualProtect
               //   will blow itself up trying to 
               // finish processing after blocking itself...
               // Could block this memory page if 
               // VirtualProtect were relocated out first.
//               VirtualProtect(CreateFileA,1,PAGE_NOACCESS,&dwOldProtect);

               // ReadProcessMemory RVA 000021CC 
               // blocks memory page RVA 00002000 - 00002FFF
               // Also on page: WriteProcessMemory, 
               // CreateProcessW, CreateProcessA, SleepEx
               //   Sleep, ReleaseMutex, WaitForSingleObject, 
               // WaitForSingleObjectEx
               // Blocking this page will stop 
               // all unauthorized child process creation,
               // prevent unauthorized multithreaded 
               // applications and block most IPC
//               VirtualProtect(ReadProcessMemory,1,PAGE_NOACCESS,&dwOldProtect);

               // GetProcAddress RVA 0000AC28 blocks 
               // memory page RVA 0000A000 - 0000AFFF
               // Also on page: FreeLibrary, 
               // LoadResource, GetProcessHeap, LoadLibraryW
               //   other Wide (Unicode) API 
               // functions, lstrcmpW, etc.
               // Blocking this page will stop 
               // many processes from shutting down cleanly
               // and will particularly target 
               // Unicode applications. There are other ways
               // to get a handle to the process 
               // heap, but GetProcessHeap and LoadResource
               // may be used more commonly than 
               // should be necessary.
               // To prevent calls to GetProcAddress 
               // by blocking this page would be nice.
//               VirtualProtect(GetProcAddress,1,PAGE_NOACCESS,&dwOldProtect);

               // WriteFile RVA 00010F9F would 
               // block memory page RVA 00010000 - 00010FFF
               // Also on page: CreateRemoteThread, 
               // CreateThread, CreateFileW, lstrcpyn
               //   SetFilePointer, etc. -- blocking 
               // this page would be very effective at
               //   stopping all multithreaded apps 
               // from executing without authorization.
               // However, WriteFile is final API 
               // on page boundary, and may cross over to
               // page containing GetFileType; to 
               // block calls to WriteFile, try it instead.
//               VirtualProtect(WriteFile,1,PAGE_NOACCESS,&dwOldProtect);

               // GetFileType RVA 00011069 would block 
               // memory page RVA 00011000 - 00011FFF
               // WriteFile API most likely overlaps 
               // page, which also contains: GetVersion
               //   IsValidCodePage, HeapDestroy -- 
               // blocking this page may not work as a
               //   way to block WriteFile, but it's 
               // worth a try... Can't really block the
               //   page containing WriteFile unless 
               // we want to block almost everything...
//               VirtualProtect(GetFileType,1,PAGE_NOACCESS,&dwOldProtect);

               // CreatePipe RVA 0001DD9A would block 
               // memory page RVA 0001D000 - 0001DFFF
               // here's the location of CreateProcessInternalA, 
               // making it pretty easy to
               //   block all calls to CreateProcessInternal 
               // if desired. To also block
               //   CreateProcessInternalW we would 
               // have to block RVA 00019000 - 00019FFF
               // Blocking all unauthorized pipes 
               // may be a nice thing, regardless.
//               VirtualProtect(CreatePipe,1,PAGE_NOACCESS,&dwOldProtect);

               // LockFile RVA 0001FE92 would block 
               // memory page RVA 0001F000 - 0001FFFF
               // lots of file APIs on this page, 
               // along with SetFilePointerEx, ConnectNamedPipe
               //   CreateIoCompletionPort, GetOverlappedResult, 
               // DeleteFileW, etc.
               // overlapped I/O and named pipes 
               // could be blocked completely on this one page
               // it's really a shame that there 
               // isn't a better page-level alignment and
               // grouping of Win32 API functions 
               // within kernel32 ...
//               VirtualProtect(LockFile,1,PAGE_NOACCESS,&dwOldProtect);

               // LockFileEx RVA 00024F7F would 
               // block memory page RVA 00024000 - 00024FFF
               // Also on page: GetComputerNameExW, 
               // GetVolumePathNamesForVolumeNameW
               // If blocking processes that 
               // call LockFileEx is desired, 
               // almost nothing
               // else would be blocked since 
               // this page holds only two other uncommon APIs
               VirtualProtect(LockFileEx,1,PAGE_NOACCESS,&dwOldProtect);

               // OpenFile RVA 00026B99 would 
               // block memory page RVA 00026000 - 00026FFF
               // Also on page: GetDateFormatA, 
               // GetTimeFormatA, SetThreadExecutionState
               //   CreateDirectoryA, GetComputerNameA, etc.
//               VirtualProtect(OpenFile,1,PAGE_NOACCESS,&dwOldProtect);

               // RemoveDirectoryW RVA 000328FD b
               // lock memory page RVA 00032000 - 00032FFF
               // Also on page: GetCalendarInfoW, 
               // ConvertDefaultLocale, BaseQueryModuleData
               //   GetCPFileNameFromRegistry, 
               // LCMapStringA
               // This is a good candidate for 
               // blocking unless LCMapStringA is used a lot.
               VirtualProtect(RemoveDirectoryW,1,PAGE_NOACCESS,&dwOldProtect);

               // WriteFileGather RVA 00036DEC 
               // would block page RVA 00036000 - 00036FFF
               // Also on page: HeapSetInformation, 
               // SetConsoleTitleW, ReadFileScatter
               // Blocking this page stops all 
               // scatter/gather overlapped I/O in one place.
               // Really nice if we want to prevent 
               // services and zombie spam relays or
               // warez servers from being 
               // spawned without authorization
               VirtualProtect(WriteFileGather,1,PAGE_NOACCESS,&dwOldProtect);

               // ReadFileEx RVA 000384C5 would 
               // block page RVA 00038000 - 00038FFF
               // Also on page: OpenThread, 
               // GetHandleInformation, ReplaceFileW, ReplaceFile
               //   Beep, GetThreadContext, 
               // SuspendThread, lstrcat, lstrcatA
               // Not being able to Beep may 
               // be a very bad thing.
//               VirtualProtect(ReadFileEx,1,PAGE_NOACCESS,&dwOldProtect);

               // BackupWrite RVA 000571D5 would 
               // block page RVA 00057000 - 00057FFF

               // Also on page: SetComputerNameW, SetComputerNameExW
               // No reason not to block this page.
               VirtualProtect(BackupWrite,1,PAGE_NOACCESS,&dwOldProtect);

               // RemoveDirectoryA RVA 0005B001 
               // would block page RVA 0005B000 - 0005BFFF
               // Also on page: CreateDirectoryExA, 
               // WritePrivateProfileSectionA, GetAtomNameA
               //   and a bunch of other popular 
               // 'profile' APIs like WriteProfileStringA
               // Might be better to block all 
               // calls to RemoveDirectoryA even so...
//               VirtualProtect(RemoveDirectoryA,1,PAGE_NOACCESS,&dwOldProtect);

               // MoveFileExA RVA 0005D2A3 would 
               // block page RVA 0005D000 - 0005DFFF
               // Also on page: GetCompressedFileSizeA, 
               // GetCompressedFileSizeW
               VirtualProtect(MoveFileExA,1,PAGE_NOACCESS,&dwOldProtect);

               // CreateNamedPipeA RVA 0005FA54 
               // would block page RVA 0005F000 - 0005FFFF
               // Also on page: everything else 
               // named pipe-related, and processor node APIs
               VirtualProtect(CreateNamedPipeA,1,PAGE_NOACCESS,&dwOldProtect);

               // WinExec RVA 0006114D would block
               //  memory page RVA 00061000 - 00061FFF
               // Also on page: LoadModule, 
               // CreateWaitableTimerA, OpenWaitableTimerA, etc.
               // Gotta block the WinExec API page 
               // and just see what happens...
               // Stopping calls to WinExec would be great.
//               VirtualProtect(WinExec,1,PAGE_NOACCESS,&dwOldProtect);

               // Every blocked API could first 
               // be CopyMemory'ed into a local stub.
               // The replacement API thus exposed 
               // could be ASLR'ed a la PaX, Exec Shield
               // see: http://en.wikipedia.org/wiki/ASLR
          }}}}
return TRUE; }

The moral of this story, and its proof-of-concept code, is that if we rearrange the kernel32 offsets and its internal layout, we could block or allow access to individual APIs (one per memory page) or group-related APIs so they can all be blocked or allowed collectively (e.g., file I/O APIs) using VirtualProtect alone. Blocking calls to the standard VirtualProtect API could help to prevent malicious code from resetting memory page protection itself to enable the disabled APIs at runtime.

The "AppInit_DLLs" Registry value must be populated with the name of the DLL that you build from the code shown, after which time all new processes that link with user32.dll will include a forced call to load the additional DLL into memory.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows 
NT\CurrentVersion\Windows\AppInit_DLLs

Be very careful not to deploy this code to a production box! This is a proof of concept only, and without extra coding and host analysis, this injected code module will probably prevent your Windows box from rebooting properly. However, with some effort and extra time spent determining which processes your host tries to launch at startup, and which of those processes can be safely prevented from accessing the full Win32 API, versus which must be allowed explicitly using the string array shown in the code, this module could be used to provide a production-quality defense against unauthorized access to the Win32 API. Doing so, however, without a forced rebasing and reoffsetting of kernel32 won't result in a fine degree of control over which Win32 APIs are blocked. With some additional coding, a utility could be created that would split apart each function exported by kernel32 so that the individual APIs do not share memory pages. If this were done first, then a page-level Win32 API blocker like the one shown in this article would become quite useful.


Jason Coombs <[email protected]> works as a freelance computer forensic analyst and security incident response investigator. He also serves as a technical expert witness in civil and criminal court cases. Jason thinks he knows a thing or two about information security and forensics, but he may be mistaken; he may in fact be your typical corporate programmer geek with a slightly unusual résumé, which is mostly the result of a refusal to work in a cubicle and a desire to earn far more than he is probably worth.



Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.