Shellcode: The hunt for GetProcAddress

Introduction

Recently revealed by Alex Ionescu, future releases of Windows will include Enhanced Mitigation Experience Toolkit (EMET) built into the kernel.

As more mitigation features appear in MSVC and the Windows operating system, the difficulty of locating API to exploit memory corruption vulnerabilities increases.

It got me thinking; If both the Import Address Table (IAT) and Export Address Table (EAT) are unavailable, what other ways can we resolve GetProcAddress for a Position Independent Code (PIC) ?

Surely there must be other ways?

What you see here is just some hours work and not extensive research into obtaining GPA when the IAT and EAT are inaccessible.

I don’t know how practical this idea would be in a real world scenario, but thought it was worth a quick post which might encourage others to find some alternatives.

Signature Detection

Every algorithm used to detect malware can be repurposed to detect arbitrary code including that of GetProcAddress.

Detection of malicious code can range from simple searching of strings, constants (including crypto) or sequences of code bytes to more advanced methods like emulation and statistical analysis.

Locating GetProcAddress by signature is trivial because it’s the only API that will return the error code: STATUS_ORDINAL_NOT_FOUND

From Windows NT up to Windows 10, there’s a high probability simply searching either kernel32.dll or kernelbase.dll for a function with this constant is enough to locate the entry point of GetProcAddress.

Search algorithm

Some pseudocode to describe search pattern:

for each DLL in PEB
  for each executable section in DLL
    scan forward in memory for STATUS_ORDINAL_NOT_FOUND
    if constant found
      scan backward in memory for prolog bytes
        if bytes found
          break
        end if
    end if
  end for
end for

Simple enough in theory and for 32-bit legacy mode is effective.

However, it’s ineffective if GetProcAddress uses function chunking thus will not work for some DLL (specifically 64-bit versions).

On Windows 7, the location of our constant in kernelbase.dll appears inside a chunk of code.

Negating 0x3FFFFEC8 gives us 0xC0000138

On Windows 10, the location of constant is within the same GetProcAddress code thus entrypoint can be easily located via simple search.

To work with Win7, I would suggest a Length Disassembler Engine (LDE) in addition to emulating the relative jumps until you land in GetProcAddress again, but there are no LDE for 64-bit I know which would operate independently of memory. Maybe one isn’t needed?

Since we’re searching the sections of kernel32.dll or kernelbase.dll, let’s examine the PEs section header structure and what members we’re interested in.

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • Misc.VirtualSize

The total size of the section when loaded into memory, in bytes. If this value is greater than the SizeOfRawData member, the section is filled with zeroes. This field is valid only for executable images and should be set to 0 for object files.

  • VirtualAddress

The address of the first byte of the section when loaded into memory, relative to the image base. For object files, this is the address of the first byte before relocation is applied.

  • Characteristics

The characteristics of the image. Since we’re looking for executable code, we can test this value for IMAGE_SCN_CNT_CODE or IMAGE_SCN_MEM_EXECUTE

Once we find an executable section, we perform a simple search for our signature, which in this case would be 4-byte sequence: 0x38, 0x01, 0x00, 0xC0

If found, we presume we’re in GetProcAddress code so we scan back in memory to find the prolog bytes which for 32-bit will be 0x55, 0x8B, 0xEC and 0x48, 0x89, 0x5C, 0x24, 0x08 for 64-bit.

// 32-bit prolog
"\x55"             /* push ebp           */
"\x8b\xec"         /* mov ebp, esp       */

Even though most 32-bit API since XP SP2 have mov edi, edi before prolog which is intended for hotpatching, we can in most cases skip it without any consequences.

// 64-bit prolog
"\x48\x89\x5c\x24\x08" /* mov [rsp+0x8], rbx */

Another potential way to find the entry point on 64-bit once we’ve found the signature is to look for padding added by the compiler. (some compilers differ in what padding is used)

Functions are aligned on a 16-byte boundary; padding is used before and after GetProcAddress.

On Windows 7, 0x90 is used, which is the x86 opcode for No operation (NOP)

On Windows 10, 0xCC is used, which is the opcode for a software interrupt (INT3)

This may be useful for searching with a different algorithm which is the only reason I mention it.

C code

For each DLL in the Process Environment Block (PEB)
Find either kernel32.dll or kernelbase.dll

LPVOID get_gpa (VOID)
{
  PPEB                  peb;
  PPEB_LDR_DATA         ldr;
  PLDR_DATA_TABLE_ENTRY dte;
  LPVOID                api_adr=NULL;
  DWORD                 i, h;
  BYTE                  c;
  
#if defined(_WIN64)
  peb = (PPEB) __readgsqword(0x60);
#else
  peb = (PPEB) __readfsdword(0x30);
#endif

  ldr = (PPEB_LDR_DATA)peb->Ldr;
  
  // for each DLL loaded
  for (dte=(PLDR_DATA_TABLE_ENTRY)ldr->InLoadOrderModuleList.Flink;
       dte->DllBase != NULL && api_adr == NULL; 
       dte=(PLDR_DATA_TABLE_ENTRY)dte->InLoadOrderLinks.Flink)
  { 
    // is this kernel32.dll or kernelbase.dll?
    for (h=0, i=0; i<dte->BaseDllName.Length/2; i++) {
      c = dte->BaseDllName.Buffer[i];
      h += (c | 0x20);
      h = ROTR32(h, 13);
    }
    if (h != 0xB1FC7F66 && h!= 0x22901A8D) continue;
    
    api_adr = scan_img(dte->DllBase); 
    
    if (api_adr != NULL) {
      printf ("\nGetProcAddress: %p", api_adr);
      printf ("\nGetProcAddress: %p\n", 
        GetProcAddress(dte->DllBase, "GetProcAddress"));      
    }
  }
  return api_adr;
}

For each executable section of this DLL

LPVOID scan_img (LPVOID base)
{
  PIMAGE_DOS_HEADER     dos;
  PIMAGE_NT_HEADERS     nt; 
  PIMAGE_SECTION_HEADER sec;
  BOOL                  is32;  
  PBYTE                 pRawData;
  DWORD                 i, len;
  LPVOID                gpa=NULL;

  dos  = (PIMAGE_DOS_HEADER)base;  
  nt   = RVA2VA(PIMAGE_NT_HEADERS, base, dos->e_lfanew);  
  sec  = (PIMAGE_SECTION_HEADER)((LPBYTE)&nt->OptionalHeader + 
         nt->FileHeader.SizeOfOptionalHeader);  
  is32 = nt->FileHeader.Machine == IMAGE_FILE_MACHINE_I386;
  len  = nt->FileHeader.NumberOfSections;  
  
  // for each section
  for (i=0; i<len && gpa == NULL; i++) {    
    // is it executable?
    if (sec[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) {
      pRawData = RVA2VA (PBYTE, base, sec[i].VirtualAddress);
      
      gpa = scan_section (pRawData, sec[i].Misc.VirtualSize, is32);
    }
  }
  return gpa;
}

Find the signature and scan backwards for the prolog bytes.

LPVOID scan_section (PBYTE memory, DWORD len, BOOL is32)
{
  DWORD  i, j, plen;
  LPVOID ofs=NULL;
  
   // 4-byte signature 
  BYTE   sig[] = { 0x38, 0x01, 0x00, 0xC0 };
  
  // 3-byte prolog for 32-bit   
  BYTE   x32[] = { 0x55, 0x8B, 0xEC };
  
  // 4-byte prolog for 64-bit  
  BYTE   x64[] = { 0x48, 0x89, 0x5C, 0x24 };
  PBYTE  prolog, p;
  
  p      = memory;
  plen   = is32 ? sizeof(x32) : sizeof(x64);
  prolog = is32 ? x32 : x64;

  if (len <= sizeof(sig)) return NULL;
  
  // subtract size of signature 
  // so we don't cause an exception
  len -= sizeof(sig);
  
  for (i=0; i<len; i++) {
    // compare signature with current position
    if ((memcmp(sig, &p[i], sizeof(sig))==0)) {
      // try scanning backwards for prolog bytes
      for (j=i; j>=0; j--) {
        // found prolog bytes?
        if (memcmp(prolog, &p[j], plen)==0) {
          // return address
          ofs = (LPVOID)&p[j];
          break;
        }
      }
    }
    if (ofs != NULL) break;
  }  
  return ofs;
}

Summary

Does it work? For majority of 32-bit mode OS, it works fine because STATUS_ORDINAL_NOT_FOUND is within the prolog and epilog of GetProcAddress code. Where it doesn’t work is on Windows 7 64-bit because the constant is outside.

  • Windows 7 32-bit
  • (good!)

The 1st address is off by 2-bytes, but that’s the MOV EDI, EDI instruction used for hotpatching and can be safely skipped over.

  • Windows 7 64-bit
  • (bad..)

  • Windows 10 64-bit
  • (good!)

Advertisements
This entry was posted in assembly, programming, security, shellcode, windows and tagged , , , , , . Bookmark the permalink.

3 Responses to Shellcode: The hunt for GetProcAddress

  1. Pingback: 2017.06.22 - Daily Security Issue > ReverseNote

  2. Pingback: 【技术分享】Shellcode编程之特征搜索定位GetProcAddress-安全路透社

  3. Pingback: 【技术分享】Shellcode编程之特征搜索定位GetProcAddress – 安百科技

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s